├── .github ├── .gitignore ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── question.md │ └── bug_report.md ├── CONTRIBUTING.md ├── pkgdown-pr-preview-build.R ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── pr-commands.yaml ├── pkgdown ├── assets │ ├── snippets │ │ ├── ace │ │ │ ├── mode-text.js │ │ │ ├── mode-plain_text.js │ │ │ └── theme-textmate.js │ │ ├── addition.md │ │ ├── tutorialyaml-allowskip-topic.md │ │ ├── tutorialyaml-progressive-topic.md │ │ ├── exercisecaption.md │ │ ├── runtutorial.md │ │ ├── videos-size.md │ │ ├── tutorialyaml-progressive.md │ │ ├── tutorialyaml.md │ │ ├── exerciselines.md │ │ ├── tutorialtoccollapsed.md │ │ ├── tutorialyaml-allowskip.md │ │ ├── tutorvideos.md │ │ ├── exerciseoptions.md │ │ ├── tutoryamlids.md │ │ ├── tutorids.md │ │ ├── exercisecompletion.md │ │ ├── question.md │ │ ├── exercisetimelimit.md │ │ ├── exerciseblanks.md │ │ ├── exercisesetupsuffix.md │ │ ├── questionretry.md │ │ ├── exercisehintdiv.md │ │ ├── questionrandom.md │ │ ├── exercisesolution.md │ │ ├── exerciseeval.md │ │ ├── questionmath.md │ │ ├── questionmessages.md │ │ ├── tutorrecorder.md │ │ ├── exercisefeedback.md │ │ ├── exercisecheck.md │ │ ├── exercise-tests.md │ │ ├── exercisecodecheck.md │ │ ├── exercisedata.md │ │ ├── hello-learnr.md │ │ ├── exercisesetup.md │ │ ├── questionmultiple.md │ │ ├── exercisehints.md │ │ ├── shiny.md │ │ ├── exercisesolutionhidden.md │ │ ├── exercisesetupshared.md │ │ ├── exercisechained.md │ │ ├── exercisesetupchained.md │ │ ├── questionquiz.md │ │ ├── tutorstorage.md │ │ ├── customchecker.md │ │ ├── codefeedback.md │ │ ├── exerciseevaluator.md │ │ └── snippets.js │ ├── images │ │ └── hello.png │ └── learnr-social.png ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ └── apple-touch-icon-180x180.png ├── extra.css ├── templates │ ├── in-header.html │ └── content-article.html └── index.Rmd ├── revdep ├── failures.md ├── problems.md ├── .gitignore ├── email.yml ├── cran.md └── README.md ├── tests ├── manual │ ├── .gitignore │ ├── tests-chunk.Rmd │ ├── hint-popover.Rmd │ └── ace-mode │ │ └── ace-mode.Rmd ├── testthat │ ├── tutorials │ │ ├── .gitignore │ │ ├── hint-div │ │ │ └── hint-div.Rmd │ │ ├── missing-exercise-checker.Rmd │ │ ├── knitr-hooks_empty-exercise │ │ │ ├── empty-exercise.Rmd │ │ │ ├── full-exercise.Rmd │ │ │ └── duplicate-label.Rmd │ │ ├── exercise-option-is-symbol.Rmd │ │ ├── hint-next │ │ │ └── hint-next.Rmd │ │ ├── hint-copy │ │ │ └── hint-copy.Rmd │ │ ├── basic.Rmd │ │ └── optional-show-solution.Rmd │ ├── test-tutorial-format.R │ ├── test-options.R │ ├── setup-chunks │ │ ├── setup-cycle-self.Rmd │ │ ├── exercise-cycle-self.Rmd │ │ ├── error-check-chunk_bad.Rmd │ │ ├── error-check-chunk_good.Rmd │ │ ├── setup-cycle-two.Rmd │ │ ├── exercise-cycle-two.Rmd │ │ ├── setup-cycle.Rmd │ │ ├── exercise-global-setup.Rmd │ │ ├── exercise-cycle.Rmd │ │ └── exercise-cycle-default-setup.Rmd │ ├── test-storage.R │ ├── test-dependency.R │ ├── test-shinytest2-aaa.R │ ├── test-cookies.R │ ├── _snaps │ │ └── available-tutorials.md │ ├── test-duplicate_env.R │ ├── test-available-tutorials.R │ ├── test-question_radio.R │ ├── test-praise.R │ ├── test-mark_as.R │ ├── test-mock_exercise.R │ ├── helpers.R │ ├── test-feedback.R │ ├── test-install-dependencies.R │ ├── test-events.R │ └── test-mutate_tags.R └── testthat.R ├── vignettes ├── .gitignore ├── articles │ ├── images │ │ ├── slidy.png │ │ ├── question.png │ │ ├── solution.png │ │ ├── exercises.png │ │ ├── questions.png │ │ ├── exercise-code.png │ │ ├── question-math.png │ │ ├── tutorial-sql.png │ │ ├── exercise-blanks.png │ │ ├── exercise-error.png │ │ ├── exercise-result.png │ │ ├── format-tutorial.png │ │ ├── tutorial-hello.png │ │ ├── tutorial-slidy.png │ │ ├── questions-message.png │ │ ├── create-new-tutorial.png │ │ ├── exercise-completion.png │ │ ├── tutorial-ex-setup-r.png │ │ ├── tutorial-ex-data-basics.png │ │ ├── tutorial-ex-data-filter.png │ │ ├── tutorial-ex-data-mutate.png │ │ ├── tutorial-quiz_question.png │ │ ├── example-community-rtledu.png │ │ ├── shinyapps-deploy-faithful.png │ │ ├── example-community-afrilearnr.png │ │ ├── example-community-qsslearnr.png │ │ ├── tutorial-ex-data-summarise.png │ │ ├── tutorial-external-sortable.png │ │ ├── example-community-dosstoolkit.png │ │ └── example-community-learntidymodels.png │ ├── releases │ │ └── images │ │ │ ├── blanks-warning.png │ │ │ ├── error-checker.png │ │ │ ├── syntax-warning.png │ │ │ ├── question-numeric.png │ │ │ ├── r-python-exercise.png │ │ │ ├── blanks-warning-custom.png │ │ │ ├── exercise-tab-example.gif │ │ │ ├── exercise-tab-example.mp4 │ │ │ ├── fancy-quotes-warning.png │ │ │ └── janko-ferlic-sfL_QOnmy00-unsplash.jpg │ ├── snippets.R │ ├── example_cards.R │ └── examples.Rmd └── img │ ├── shinyapps-publishing-advanced.png │ ├── shinyapps-publishing-instances.png │ ├── shinyapps-publishing-metrics.png │ └── shinyapps-publishing-instance-size.png ├── sandbox ├── .gitignore └── autocompletion.Rmd ├── inst ├── rmarkdown │ └── templates │ │ └── tutorial │ │ ├── skeleton │ │ ├── .gitignore │ │ └── skeleton.Rmd │ │ ├── resources │ │ └── images │ │ │ ├── topicProgress.png │ │ │ ├── close.svg │ │ │ └── exerciseDone.svg │ │ └── template.yaml ├── internals │ ├── i18n_random_phrases.rds │ ├── i18n_translations.rds │ ├── templates │ │ └── exercise-setup.Rmd │ └── learnr.py ├── tutorials │ ├── polyglot │ │ ├── portal_mammals.sqlite │ │ └── polyglot.Rmd │ ├── ex-data-basics │ │ └── images │ │ │ └── flights.png │ ├── ex-data-filter │ │ └── images │ │ │ └── transform-logical.png │ ├── hello │ │ └── hello.Rmd │ ├── slidy │ │ └── slidy.Rmd │ └── setup-chunks │ │ └── setup-chunks.Rmd ├── lib │ └── idb-keyval │ │ ├── note.md │ │ ├── LICENSE │ │ └── idb-keyval-iife-compat.min.js ├── staticexports │ ├── assertions.R │ ├── strings.R │ └── knitr_engine_caption.R └── examples │ └── apparmor │ └── apparmor_evaluator.R ├── .eslintignore ├── man ├── figures │ ├── logo.png │ ├── lifecycle-stable.svg │ ├── lifecycle-defunct.svg │ ├── lifecycle-archived.svg │ ├── lifecycle-maturing.svg │ ├── lifecycle-deprecated.svg │ ├── lifecycle-superseded.svg │ ├── lifecycle-experimental.svg │ ├── lifecycle-questioning.svg │ └── lifecycle-soft-deprecated.svg ├── rmd-fragments │ ├── learnr-install.Rmd │ ├── learnr-overview.Rmd │ ├── hello-tutorial.Rmd │ ├── badges.Rmd │ └── learnr-examples-showcase.Rmd ├── tutorial_html_dependency.Rd ├── initialize_tutorial.Rd ├── safe_env.Rd ├── disable_all_tags.Rd ├── knit_print.Rd ├── filesystem_storage.Rd ├── available_tutorials.Rd ├── external_evaluator.Rd ├── random_praise.Rd ├── tutorial_package_dependencies.Rd ├── finalize_question.Rd ├── duplicate_env.Rd ├── event_register_handler.Rd ├── debug_exercise_checker.Rd ├── safe.Rd ├── format_quiz.Rd ├── mark_as_correct_incorrect.Rd ├── learnr-package.Rd ├── question_radio.Rd ├── random_phrases_add.Rd ├── one_time.Rd ├── run_tutorial.Rd ├── tutorial_options.Rd └── get_tutorial_state.Rd ├── .browserslistrc ├── .gitignore ├── tools ├── deploy_tutorials_on_local.R ├── deploy_tutorials_on_ci.R ├── update-ace.R └── deploy_tutorials.R ├── babel.config.json ├── cran-comments.md ├── learnr.Rproj ├── README.Rmd ├── R ├── shinytest.R ├── ace.R ├── events_record.R ├── zzz.R ├── data_dir.R ├── learnr-package.R ├── html_selector.R ├── initialize.R ├── tutorial_package_dependencies.R ├── learnr_messages.R ├── html-dependencies.R └── feedback.R ├── .Rbuildignore ├── package.json ├── learnr-js └── build.js ├── README.md └── DESCRIPTION /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/ace/mode-text.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /revdep/failures.md: -------------------------------------------------------------------------------- 1 | *Wow, no problems at all. :)* -------------------------------------------------------------------------------- /revdep/problems.md: -------------------------------------------------------------------------------- 1 | *Wow, no problems at all. :)* -------------------------------------------------------------------------------- /tests/manual/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | rsconnect/* -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | ./*.R 3 | *.R 4 | -------------------------------------------------------------------------------- /tests/testthat/tutorials/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | rsconnect/* -------------------------------------------------------------------------------- /sandbox/.gitignore: -------------------------------------------------------------------------------- 1 | *_files/ 2 | *.html 3 | programming-basics* 4 | -------------------------------------------------------------------------------- /inst/rmarkdown/templates/tutorial/skeleton/.gitignore: -------------------------------------------------------------------------------- 1 | skeleton.html 2 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/addition.md: -------------------------------------------------------------------------------- 1 | ```{r addition, exercise=TRUE} 2 | 1 + 1 3 | ``` -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | inst/lib/* 2 | inst/rmarkdown/templates/tutorial/resources/tutorial-format.js 3 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/tutorialyaml-allowskip-topic.md: -------------------------------------------------------------------------------- 1 | 2 | ### Exercise 2 {data-allow-skip=TRUE} 3 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/tutorialyaml-progressive-topic.md: -------------------------------------------------------------------------------- 1 | 2 | ## Topic 1 {data-progressive=TRUE} 3 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/man/figures/logo.png -------------------------------------------------------------------------------- /revdep/.gitignore: -------------------------------------------------------------------------------- 1 | checks 2 | library 3 | checks.noindex 4 | library.noindex 5 | data.sqlite 6 | *.html 7 | -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /pkgdown/assets/images/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/pkgdown/assets/images/hello.png -------------------------------------------------------------------------------- /pkgdown/assets/learnr-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/pkgdown/assets/learnr-social.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /revdep/email.yml: -------------------------------------------------------------------------------- 1 | release_date: ??? 2 | rel_release_date: ??? 3 | my_news_url: ??? 4 | release_version: ??? 5 | release_details: ??? 6 | -------------------------------------------------------------------------------- /vignettes/articles/images/slidy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/slidy.png -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | last 2 versions 3 | not dead 4 | > 0.2% 5 | Firefox ESR 6 | phantomjs 2.1 7 | IE 11 # kinda 8 | -------------------------------------------------------------------------------- /inst/internals/i18n_random_phrases.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/inst/internals/i18n_random_phrases.rds -------------------------------------------------------------------------------- /inst/internals/i18n_translations.rds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/inst/internals/i18n_translations.rds -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /vignettes/articles/images/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/question.png -------------------------------------------------------------------------------- /vignettes/articles/images/solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/solution.png -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisecaption.md: -------------------------------------------------------------------------------- 1 | ```{r setup, include=FALSE} 2 | library(learnr) 3 | tutorial_options(exercise.cap = "Sandbox") 4 | ``` -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | 2 | if (requireNamespace("testthat")) { 3 | library(testthat) 4 | library(learnr) 5 | 6 | test_check("learnr") 7 | } 8 | -------------------------------------------------------------------------------- /vignettes/articles/images/exercises.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/exercises.png -------------------------------------------------------------------------------- /vignettes/articles/images/questions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/questions.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /vignettes/articles/images/exercise-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/exercise-code.png -------------------------------------------------------------------------------- /vignettes/articles/images/question-math.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/question-math.png -------------------------------------------------------------------------------- /vignettes/articles/images/tutorial-sql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/tutorial-sql.png -------------------------------------------------------------------------------- /inst/tutorials/polyglot/portal_mammals.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/inst/tutorials/polyglot/portal_mammals.sqlite -------------------------------------------------------------------------------- /pkgdown/assets/snippets/runtutorial.md: -------------------------------------------------------------------------------- 1 | ```r 2 | learnr::run_tutorial("hello", package = "learnr") 3 | learnr::run_tutorial("slidy", package = "learnr") 4 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/videos-size.md: -------------------------------------------------------------------------------- 1 | ![](https://youtu.be/zNzZ1PfUDNk){width="90%"} 2 | 3 | ![](https://youtu.be/zNzZ1PfUDNk){width="560" height="315"} 4 | -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /vignettes/articles/images/exercise-blanks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/exercise-blanks.png -------------------------------------------------------------------------------- /vignettes/articles/images/exercise-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/exercise-error.png -------------------------------------------------------------------------------- /vignettes/articles/images/exercise-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/exercise-result.png -------------------------------------------------------------------------------- /vignettes/articles/images/format-tutorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/format-tutorial.png -------------------------------------------------------------------------------- /vignettes/articles/images/tutorial-hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/tutorial-hello.png -------------------------------------------------------------------------------- /vignettes/articles/images/tutorial-slidy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/tutorial-slidy.png -------------------------------------------------------------------------------- /inst/tutorials/ex-data-basics/images/flights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/inst/tutorials/ex-data-basics/images/flights.png -------------------------------------------------------------------------------- /vignettes/articles/images/questions-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/questions-message.png -------------------------------------------------------------------------------- /vignettes/img/shinyapps-publishing-advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/img/shinyapps-publishing-advanced.png -------------------------------------------------------------------------------- /vignettes/img/shinyapps-publishing-instances.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/img/shinyapps-publishing-instances.png -------------------------------------------------------------------------------- /vignettes/img/shinyapps-publishing-metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/img/shinyapps-publishing-metrics.png -------------------------------------------------------------------------------- /vignettes/articles/images/create-new-tutorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/create-new-tutorial.png -------------------------------------------------------------------------------- /vignettes/articles/images/exercise-completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/exercise-completion.png -------------------------------------------------------------------------------- /vignettes/articles/images/tutorial-ex-setup-r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/tutorial-ex-setup-r.png -------------------------------------------------------------------------------- /vignettes/articles/images/tutorial-ex-data-basics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/tutorial-ex-data-basics.png -------------------------------------------------------------------------------- /vignettes/articles/images/tutorial-ex-data-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/tutorial-ex-data-filter.png -------------------------------------------------------------------------------- /vignettes/articles/images/tutorial-ex-data-mutate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/tutorial-ex-data-mutate.png -------------------------------------------------------------------------------- /vignettes/articles/images/tutorial-quiz_question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/tutorial-quiz_question.png -------------------------------------------------------------------------------- /vignettes/articles/releases/images/blanks-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/releases/images/blanks-warning.png -------------------------------------------------------------------------------- /vignettes/articles/releases/images/error-checker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/releases/images/error-checker.png -------------------------------------------------------------------------------- /vignettes/articles/releases/images/syntax-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/releases/images/syntax-warning.png -------------------------------------------------------------------------------- /vignettes/img/shinyapps-publishing-instance-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/img/shinyapps-publishing-instance-size.png -------------------------------------------------------------------------------- /vignettes/articles/images/example-community-rtledu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/example-community-rtledu.png -------------------------------------------------------------------------------- /vignettes/articles/images/shinyapps-deploy-faithful.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/shinyapps-deploy-faithful.png -------------------------------------------------------------------------------- /vignettes/articles/releases/images/question-numeric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/releases/images/question-numeric.png -------------------------------------------------------------------------------- /inst/tutorials/ex-data-filter/images/transform-logical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/inst/tutorials/ex-data-filter/images/transform-logical.png -------------------------------------------------------------------------------- /vignettes/articles/images/example-community-afrilearnr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/example-community-afrilearnr.png -------------------------------------------------------------------------------- /vignettes/articles/images/example-community-qsslearnr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/example-community-qsslearnr.png -------------------------------------------------------------------------------- /vignettes/articles/images/tutorial-ex-data-summarise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/tutorial-ex-data-summarise.png -------------------------------------------------------------------------------- /vignettes/articles/images/tutorial-external-sortable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/tutorial-external-sortable.png -------------------------------------------------------------------------------- /vignettes/articles/releases/images/r-python-exercise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/releases/images/r-python-exercise.png -------------------------------------------------------------------------------- /vignettes/articles/images/example-community-dosstoolkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/example-community-dosstoolkit.png -------------------------------------------------------------------------------- /vignettes/articles/releases/images/blanks-warning-custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/releases/images/blanks-warning-custom.png -------------------------------------------------------------------------------- /vignettes/articles/releases/images/exercise-tab-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/releases/images/exercise-tab-example.gif -------------------------------------------------------------------------------- /vignettes/articles/releases/images/exercise-tab-example.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/releases/images/exercise-tab-example.mp4 -------------------------------------------------------------------------------- /vignettes/articles/releases/images/fancy-quotes-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/releases/images/fancy-quotes-warning.png -------------------------------------------------------------------------------- /pkgdown/assets/snippets/tutorialyaml-progressive.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello, Tutorial!" 3 | output: 4 | learnr::tutorial: 5 | progressive: true 6 | runtime: shiny_prerendered 7 | --- 8 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/tutorialyaml.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello, Tutorial!" 3 | output: learnr::tutorial 4 | runtime: shiny_prerendered 5 | description: "Welcome to learnr tutorials!" 6 | --- 7 | -------------------------------------------------------------------------------- /vignettes/articles/images/example-community-learntidymodels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/images/example-community-learntidymodels.png -------------------------------------------------------------------------------- /inst/rmarkdown/templates/tutorial/resources/images/topicProgress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/inst/rmarkdown/templates/tutorial/resources/images/topicProgress.png -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exerciselines.md: -------------------------------------------------------------------------------- 1 | ```{r add-function, exercise=TRUE, exercise.lines=15} 2 | # Write a function to add two numbers together 3 | add_numbers <- function(a, b) { 4 | 5 | } 6 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/tutorialtoccollapsed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello, Tutorial!" 3 | output: 4 | learnr::tutorial: 5 | toc_float: 6 | collapsed: true 7 | runtime: shiny_prerendered 8 | --- -------------------------------------------------------------------------------- /inst/rmarkdown/templates/tutorial/template.yaml: -------------------------------------------------------------------------------- 1 | name: Interactive Tutorial 2 | description: > 3 | Interactive tutorial containing figures, videos, R exercises and quiz questions. 4 | create_dir: true 5 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/tutorialyaml-allowskip.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello, Tutorial!" 3 | output: 4 | learnr::tutorial: 5 | progressive: true 6 | allow_skip: true 7 | runtime: shiny_prerendered 8 | --- -------------------------------------------------------------------------------- /vignettes/articles/releases/images/janko-ferlic-sfL_QOnmy00-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/department-for-transport/learnr/main/vignettes/articles/releases/images/janko-ferlic-sfL_QOnmy00-unsplash.jpg -------------------------------------------------------------------------------- /pkgdown/assets/snippets/tutorvideos.md: -------------------------------------------------------------------------------- 1 | ![](https://youtu.be/zNzZ1PfUDNk) 2 | ![](https://www.youtube.com/watch?v=zNzZ1PfUDNk) 3 | 4 | ![](https://vimeo.com/142172484) 5 | ![](https://player.vimeo.com/video/142172484) 6 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exerciseoptions.md: -------------------------------------------------------------------------------- 1 | ```{r setup, include=FALSE} 2 | library(learnr) 3 | tutorial_options(exercise.timelimit = 60) 4 | ``` 5 | 6 | ```{r addition, exercise=TRUE, exercise.timelimit = 60} 7 | 1 + 1 8 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/tutoryamlids.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello, Tutorial!" 3 | tutorial: 4 | id: "com.example.tutorials.my-first-tutorial" 5 | version: 2.1 6 | output: learnr::tutorial 7 | runtime: shiny_prerendered 8 | --- -------------------------------------------------------------------------------- /pkgdown/assets/snippets/tutorids.md: -------------------------------------------------------------------------------- 1 | options( 2 | tutorial.http_header_tutorial_id = "X-Tutorial-ID", 3 | tutorial.http_header_tutorial_version = "X-Tutorial-Version", 4 | tutorial.http_header_user_id = "X-Tutorial-UserID" 5 | }) -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisecompletion.md: -------------------------------------------------------------------------------- 1 | ```{r setup, include=FALSE} 2 | library(learnr) 3 | tutorial_options(exercise.completion = FALSE) 4 | ``` 5 | 6 | ```{r histogram-plot, exercise=TRUE, exercise.completion=TRUE} 7 | 8 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/question.md: -------------------------------------------------------------------------------- 1 | ```{r letter-a, echo=FALSE} 2 | question("What number is the letter A in the English alphabet?", 3 | answer("8"), 4 | answer("14"), 5 | answer("1", correct = TRUE), 6 | answer("23") 7 | ) 8 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisetimelimit.md: -------------------------------------------------------------------------------- 1 | options(tutorial.exercise.timelimit = 10) 2 | 3 | ```{r setup, include=FALSE} 4 | tutorial_options(exercise.timelimit = 10) 5 | ``` 6 | 7 | ```{r exercise1, exercise=TRUE, exercise.timelimit=10} 8 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exerciseblanks.md: -------------------------------------------------------------------------------- 1 | Fill in the blank to create an expression that adds up to **4**. 2 | 3 | ```{r blank, exercise = TRUE, exercise.blanks = "___+"} 4 | 1 + ____ 5 | ``` 6 | 7 | ```{r blank-solution} 8 | 1 + 3 9 | ``` 10 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisesetupsuffix.md: -------------------------------------------------------------------------------- 1 | ```{r filter-setup} 2 | nycflights <- nycflights13::flights 3 | ``` 4 | 5 | ```{r filter, exercise=TRUE} 6 | # Change the filter to select February rather than January 7 | nycflights <- filter(nycflights, month == 1) 8 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/questionretry.md: -------------------------------------------------------------------------------- 1 | ```{r letter-a, echo=FALSE} 2 | question("What number is the letter A in the English alphabet?", 3 | answer("8"), 4 | answer("14"), 5 | answer("1", correct = TRUE), 6 | answer("23"), 7 | allow_retry = TRUE 8 | ) 9 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisehintdiv.md: -------------------------------------------------------------------------------- 1 | ```{r filter, exercise=TRUE} 2 | # filter the flights table to include only United and American flights 3 | flights 4 | ``` 5 | 6 |
7 | **Hint:** You may want to use the dplyr `filter` function. 8 |
9 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/questionrandom.md: -------------------------------------------------------------------------------- 1 | ```{r letter-a, echo=FALSE} 2 | question("What number is the letter A in the English alphabet?", 3 | answer("8"), 4 | answer("14"), 5 | answer("1", correct = TRUE), 6 | answer("23"), 7 | random_answer_order = TRUE 8 | ) 9 | ``` -------------------------------------------------------------------------------- /revdep/cran.md: -------------------------------------------------------------------------------- 1 | ## revdepcheck results 2 | 3 | We checked 14 reverse dependencies (13 from CRAN + 1 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package. 4 | 5 | * We saw 0 new problems 6 | * We failed to check 0 packages 7 | 8 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisesolution.md: -------------------------------------------------------------------------------- 1 | ```{r filter, exercise=TRUE} 2 | # Change the filter to select February rather than January 3 | nycflights <- filter(nycflights, month == 1) 4 | ``` 5 | 6 | ```{r filter-solution} 7 | nycflights <- filter(nycflights, month == 2) 8 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exerciseeval.md: -------------------------------------------------------------------------------- 1 | ```{r setup, include=FALSE} 2 | library(learnr) 3 | tutorial_options(exercise.eval = TRUE) 4 | ``` 5 | 6 | ```{r filter, exercise=TRUE, exercise.eval=FALSE} 7 | # Change the filter to select February rather than January 8 | filter(nycflights, month == 1) 9 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/questionmath.md: -------------------------------------------------------------------------------- 1 | ```{r math, echo=FALSE} 2 | x <- 42 3 | question(sprintf("Suppose $x = %s$. Choose the correct statement:", x), 4 | answer(sprintf("$\\sqrt{x} = %d$", x + 1)), 5 | answer(sprintf("$x ^ 2 = %d$", x^2), correct = TRUE), 6 | answer("$\\sin x = 1$") 7 | ) 8 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/questionmessages.md: -------------------------------------------------------------------------------- 1 | ```{r letter-a, echo=FALSE} 2 | question("What number is the letter A in the *English* alphabet?", 3 | answer("8"), 4 | answer("1", correct = TRUE), 5 | answer("2", message = "2 is close but it's the letter B rather than A."), 6 | answer("26") 7 | ) 8 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/tutorrecorder.md: -------------------------------------------------------------------------------- 1 | tutorial_event_recorder <- function(tutorial_id, tutorial_version, user_id, 2 | event, data) { 3 | cat(tutorial_id, " (", tutorial_version, "): ", user_id , "\n", sep = "") 4 | cat("event: ", event, "\n", sep = "") 5 | } 6 | -------------------------------------------------------------------------------- /inst/lib/idb-keyval/note.md: -------------------------------------------------------------------------------- 1 | [Original File Location](https://github.com/jakearchibald/idb-keyval/blob/master/dist/idb-keyval-iife-compat.min.js) 2 | [New File Location](https://github.com/schloerke/idb-keyval/blob/idb-keyval/dist/idb-keyval-iife-compat.min.js) 3 | [PR](https://github.com/jakearchibald/idb-keyval/pull/73) 4 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisefeedback.md: -------------------------------------------------------------------------------- 1 | ```r 2 | checker <- function( 3 | label, 4 | user_code, 5 | solution_code, 6 | check_code, 7 | envir_result, 8 | evaluate_result, 9 | envir_prep, 10 | ... 11 | ) { 12 | list(message = "Great job!", correct = TRUE, location = "append") 13 | } 14 | ``` 15 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisecheck.md: -------------------------------------------------------------------------------- 1 | ```{r setup, include=FALSE} 2 | library(learnr) 3 | gradethis::gradethis_setup() 4 | ``` 5 | 6 | * Submit `1+1` to receive a correct grade. 7 | 8 | ```{r exercise1, exercise = TRUE} 9 | 10 | ``` 11 | 12 | ```{r exercise1-check} 13 | grade_result( 14 | pass_if(~identical(.result, 2)) 15 | ) 16 | ``` 17 | -------------------------------------------------------------------------------- /inst/staticexports/assertions.R: -------------------------------------------------------------------------------- 1 | is_AsIs <- function(x) { 2 | inherits(x, "AsIs") 3 | } 4 | 5 | is_html_tag <- function(x) { 6 | inherits(x, c("shiny.tag", "shiny.tag.list")) 7 | } 8 | 9 | is_html_chr <- function(x) { 10 | is.character(x) && inherits(x, "html") 11 | } 12 | 13 | is_html_any <- function(x) { 14 | is_html_tag(x) || is_html_chr(x) 15 | } 16 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercise-tests.md: -------------------------------------------------------------------------------- 1 | ```{r addition, exercise = TRUE} 2 | 3 | ``` 4 | 5 | ```{r addition-solution} 6 | 1 + 1 7 | ``` 8 | 9 | ```{r addition-tests} 10 | 1 + 1 11 | 12 | # one plus two ---- 13 | 1 + 2 14 | 15 | # one plus three ---- 16 | 1 + 3 17 | 18 | # one equals three ---- 19 | 1 = 3 20 | 21 | # 2 minus one ---- 22 | 2 - 1 23 | ``` 24 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisecodecheck.md: -------------------------------------------------------------------------------- 1 | ```{r setup, include=FALSE} 2 | library(learnr) 3 | gradethis::gradethis_setup() 4 | ``` 5 | 6 | * Submit `1+1` to receive a correct grade. 7 | 8 | ```{r exercise1, exercise = TRUE} 9 | 10 | ``` 11 | 12 | ```{r exercise1-solution} 13 | 1+1 14 | ``` 15 | 16 | ```{r exercise1-code-check} 17 | grade_code() 18 | ``` 19 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisedata.md: -------------------------------------------------------------------------------- 1 | ```{r setup} 2 | library(tidyverse) 3 | 4 | flights_jan <- 5 | nycflights13::flights %>% 6 | filter(month == 2) 7 | 8 | dir.create("data", showWarnings = FALSE) 9 | write_csv(flights_jan, "data/flights_jan.csv") 10 | ``` 11 | 12 | ```{r read-flights, exercise=TRUE} 13 | read_csv("data/flights_jan.csv") 14 | ``` 15 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/hello-learnr.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello, Tutorial!" 3 | output: learnr::tutorial 4 | runtime: shiny_prerendered 5 | --- 6 | 7 | ```{r setup, include=FALSE} 8 | library(learnr) 9 | ``` 10 | 11 | This code computes the answer to one plus one, 12 | change it so it computes two plus two: 13 | 14 | ```{r addition, exercise=TRUE} 15 | 1 + 1 16 | ``` -------------------------------------------------------------------------------- /inst/staticexports/strings.R: -------------------------------------------------------------------------------- 1 | 2 | str_trim <- function(x, side = "both", character = "\\s") { 3 | if (side %in% c("both", "left", "start")) { 4 | rgx <- sprintf("^%s+", character) 5 | x <- sub(rgx, "", x) 6 | } 7 | if (side %in% c("both", "right", "end")) { 8 | rgx <- sprintf("%s+$", character) 9 | x <- sub(rgx, "", x) 10 | } 11 | x 12 | } 13 | -------------------------------------------------------------------------------- /tests/testthat/test-tutorial-format.R: -------------------------------------------------------------------------------- 1 | test_that("tutorial() returns an rmarkdown format", { 2 | expect_true(inherits(tutorial(), "rmarkdown_output_format")) 3 | }) 4 | 5 | test_that("tutorial() does not support anchor_sections", { 6 | expect_error(tutorial(anchor_sections = TRUE), "do not support") 7 | expect_error(tutorial(anchor_sections = FALSE), "do not support") 8 | }) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | .DS_Store 6 | .vscode 7 | __pycache__ 8 | Untitled 9 | issues 10 | node_modules 11 | 12 | # atom config folder 13 | .atom/ 14 | 15 | # html tutorial output 16 | inst/tutorials/*/*.html 17 | inst/tutorials/*/*.sqlite 18 | 19 | # deployment output 20 | rsconnect/ 21 | inst/doc 22 | doc 23 | Meta 24 | reference/ 25 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisesetup.md: -------------------------------------------------------------------------------- 1 | ```{r setup, include=FALSE} 2 | nycflights <- nycflights13::flights 3 | ``` 4 | 5 | ```{r filter, exercise=TRUE} 6 | # Change the filter to select February rather than January 7 | filter(nycflights, month == 1) 8 | ``` 9 | 10 | ```{r arrange, exercise=TRUE} 11 | # Change the sort order to Ascending 12 | arrange(nycflights, desc(arr_delay)) 13 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/questionmultiple.md: -------------------------------------------------------------------------------- 1 | ```{r where-am-i, echo=FALSE} 2 | question("Where are you right now? (select ALL that apply)", 3 | answer("Planet Earth", correct = TRUE), 4 | answer("Pluto"), 5 | answer("At a computing device", correct = TRUE), 6 | answer("In the Milky Way", correct = TRUE), 7 | incorrect = "Incorrect. You're on Earth, in the Milky Way, at a computer.") 8 | ) 9 | ``` -------------------------------------------------------------------------------- /tools/deploy_tutorials_on_local.R: -------------------------------------------------------------------------------- 1 | 2 | 3 | if (!requireNamespace("remotes")) { 4 | install.packages("remotes") 5 | } 6 | remotes::install_cran("callr") 7 | 8 | # call in separate / non-interactive process 9 | # to avoid local dev version to be loaded and confuse packrat 10 | callr::r( 11 | function() { 12 | source("tools/deploy_tutorials.R") 13 | }, 14 | show = TRUE 15 | ) 16 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": "auto", 7 | "spec": true, 8 | "useBuiltIns": "usage", 9 | "forceAllTransforms": true, 10 | "corejs": { 11 | "version": "3.21", 12 | "proposals": false 13 | } 14 | } 15 | ] 16 | ], 17 | "comments": false 18 | } 19 | 20 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisehints.md: -------------------------------------------------------------------------------- 1 | ```{r filter, exercise=TRUE} 2 | # filter the flights table to include only United and American flights 3 | flights 4 | ``` 5 | 6 | ```{r filter-hint-1} 7 | filter(flights, ...) 8 | ``` 9 | 10 | ```{r filter-hint-2} 11 | filter(flights, UniqueCarrier=="AA") 12 | ``` 13 | 14 | ```{r filter-hint-3} 15 | filter(flights, UniqueCarrier=="AA" | UniqueCarrier=="UA") 16 | ``` -------------------------------------------------------------------------------- /pkgdown/assets/snippets/shiny.md: -------------------------------------------------------------------------------- 1 | ```{r, echo=FALSE} 2 | sliderInput("bins", "Number of bins:", min = 1, max = 50, value = 30) 3 | plotOutput("distPlot") 4 | ``` 5 | 6 | ```{r, context="server"} 7 | output$distPlot <- renderPlot({ 8 | x <- faithful[, 2] # Old Faithful Geyser data 9 | bins <- seq(min(x), max(x), length.out = input$bins + 1) 10 | hist(x, breaks = bins, col = 'darkgray', border = 'white') 11 | }) 12 | ``` -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | This is a patch release containing a few small bug fixes and minor features. 2 | 3 | ## R CMD check results 4 | 5 | 0 errors | 0 warnings | 0 notes 6 | 7 | ## revdepcheck results 8 | 9 | We checked 14 reverse dependencies (13 from CRAN + 1 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package. 10 | 11 | * We saw 0 new problems 12 | * We failed to check 0 packages 13 | -------------------------------------------------------------------------------- /inst/internals/templates/exercise-setup.Rmd: -------------------------------------------------------------------------------- 1 | ```{r learnr-setup, include=FALSE} 2 | # hack the pager function so that we can print help with custom pager function 3 | # http://stackoverflow.com/questions/24146843/including-r-help-in-knitr-output 4 | options(pager = function(files, header, title, delete.file) { 5 | cat(do.call('c', lapply(files, readLines)), sep = '\n') 6 | }) 7 | knitr::opts_chunk$set(echo = FALSE, comment = NA, error = FALSE) 8 | ``` 9 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisesolutionhidden.md: -------------------------------------------------------------------------------- 1 | ```{r filter, exercise=TRUE} 2 | # filter the flights table to include only United and American flights 3 | flights 4 | ``` 5 | 6 | ```{r filter-hint-1} 7 | filter(flights, ...) 8 | ``` 9 | 10 | ```{r filter-hint-2} 11 | filter(flights, UniqueCarrier=="AA") 12 | ``` 13 | 14 | ```{r filter-solution, exercise.reveal_solution = FALSE} 15 | filter(flights, UniqueCarrier=="AA" | UniqueCarrier=="UA") 16 | ``` -------------------------------------------------------------------------------- /tests/testthat/test-options.R: -------------------------------------------------------------------------------- 1 | 2 | context("options") 3 | 4 | test_that("tutor options set knitr options", { 5 | tutorial_options(exercise.cap = "Caption") 6 | expect_equal(knitr::opts_chunk$get("exercise.cap"), "Caption") 7 | }) 8 | 9 | test_that("tutor options don't set knitr options when excluded from the call", { 10 | tutorial_options(exercise.cap = "Caption") 11 | expect_equal(knitr::opts_chunk$get("exercise.eval"), NULL) 12 | }) 13 | 14 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisesetupshared.md: -------------------------------------------------------------------------------- 1 | ```{r prepare-flights} 2 | nycflights <- nycflights13::flights 3 | ``` 4 | 5 | ```{r filter, exercise=TRUE, exercise.setup = "prepare-flights"} 6 | # Change the filter to select February rather than January 7 | filter(nycflights, month == 1) 8 | ``` 9 | 10 | ```{r arrange, exercise=TRUE, exercise.setup = "prepare-flights"} 11 | # Change the sort order to Ascending 12 | arrange(nycflights, desc(arr_delay)) 13 | ``` -------------------------------------------------------------------------------- /tests/testthat/setup-chunks/setup-cycle-self.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Chained setup chunks: Cycles" 3 | author: "Nischal Shrestha" 4 | output: learnr::tutorial 5 | runtime: shiny_prerendered 6 | description: > 7 | This is a demo of catching cyclic dependencies with chained setup chunks starting from a setup chunk. 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | library(learnr) 12 | ``` 13 | 14 | ```{r dataA, exercise.setup = "dataA"} 15 | 1 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- /inst/internals/learnr.py: -------------------------------------------------------------------------------- 1 | 2 | class __learnr__: 3 | '''An internal class to provide Python utility functions''' 4 | 5 | @staticmethod 6 | def deep_copy(dict, deep=True): 7 | import copy 8 | from types import ModuleType 9 | new_dict = {} 10 | for k, v in dict.items(): 11 | if (k == "r" or isinstance(v, ModuleType)): 12 | new_dict[k] = v 13 | else: 14 | new_dict[k] = copy.deepcopy(v) if deep else v 15 | return new_dict 16 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisechained.md: -------------------------------------------------------------------------------- 1 | ```{r prepare-flights} 2 | nycflights <- nycflights13::flights 3 | ``` 4 | 5 | ```{r filtered-flights, exercise=TRUE, exercise.setup = "prepare-flights"} 6 | # Filter to select February rather than January 7 | flights_february <- filter(nycflights, month == 2) 8 | ``` 9 | 10 | ```{r arrange, exercise=TRUE, exercise.setup = "filtered-flights"} 11 | # Change the sort order to Ascending 12 | arrange(flights_february, desc(arr_delay)) 13 | ``` 14 | -------------------------------------------------------------------------------- /tests/testthat/setup-chunks/exercise-cycle-self.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Chained setup chunks: Cycles" 3 | author: "Nischal Shrestha" 4 | output: learnr::tutorial 5 | runtime: shiny_prerendered 6 | description: > 7 | This is a demo of catching cyclic dependencies with chained setup chunks starting from an exercise. 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | library(learnr) 12 | ``` 13 | 14 | ```{r data1, exercise=TRUE, exercise.setup = "data1"} 15 | 1 16 | 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /pkgdown/extra.css: -------------------------------------------------------------------------------- 1 | /* examples gallery */ 2 | a.thumbnail.active, 3 | a.thumbnail:focus, 4 | a.thumbnail:hover { 5 | border-color: #FEDB00; 6 | } 7 | 8 | .contents.gallery { 9 | max-width: min(100%, 72rem); 10 | } 11 | 12 | #banner { 13 | margin-top: 90px; 14 | height: 300px; 15 | width: 100%; 16 | z-index: 0; 17 | background-size: cover; 18 | background-position: 50%; 19 | background-repeat: no-repeat; 20 | } 21 | 22 | #banner + .row > main { 23 | margin-top: 0; 24 | } 25 | -------------------------------------------------------------------------------- /tests/testthat/tutorials/hint-div/hint-div.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Next hint tests" 3 | output: learnr::tutorial 4 | runtime: shiny_prerendered 5 | --- 6 | 7 | ## Tests 8 | 9 | ### HTML hint {#html} 10 | 11 | ```{r html, exercise=TRUE} 12 | # html 13 | ``` 14 | 15 |
16 | This is the HTML hint. 17 |
18 | 19 | ### Markdown hint {#md} 20 | 21 | ```{r md, exercise=TRUE} 22 | # md 23 | ``` 24 | 25 | ::: {#md-hint} 26 | This is the **md hint**. 27 | ::: 28 | -------------------------------------------------------------------------------- /tests/testthat/tutorials/missing-exercise-checker.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Missing Exercise Checker" 3 | output: learnr::tutorial 4 | runtime: shiny_prerendered 5 | --- 6 | 7 | ```{r setup, include=FALSE} 8 | library(learnr) 9 | ``` 10 | 11 | ## Topic 1 12 | 13 | 14 | 15 | ```{r two-plus-two, exercise=TRUE} 16 | 17 | ``` 18 | 19 | ```{r two-plus-two-check} 20 | # presence of a check chunk implies exercise.checker is required 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exercisesetupchained.md: -------------------------------------------------------------------------------- 1 | ```{r prepare-flights} 2 | nycflights <- nycflights13::flights 3 | ``` 4 | 5 | ```{r filtered-flights, exercise.setup = "prepare-flights"} 6 | # Prepare a filtered nycflights to select February rather than January 7 | flights_february <- filter(nycflights, month == 2) 8 | ``` 9 | 10 | ```{r arrange, exercise=TRUE, exercise.setup = "filtered-flights"} 11 | # Change the sort order to Ascending 12 | arrange(flights_february, desc(arr_delay)) 13 | ``` 14 | -------------------------------------------------------------------------------- /learnr.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source --no-byte-compile 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /pkgdown/templates/in-header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | {{#includes}}{{{in_header}}}{{/includes}} 13 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/ace/mode-plain_text.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/mode/plain_text",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/text_highlight_rules","ace/mode/behaviour"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text").Mode,s=e("./text_highlight_rules").TextHighlightRules,o=e("./behaviour").Behaviour,u=function(){this.HighlightRules=s,this.$behaviour=new o};r.inherits(u,i),function(){this.type="text",this.getNextLineIndent=function(e,t,n){return""},this.$id="ace/mode/plain_text"}.call(u.prototype),t.Mode=u}) -------------------------------------------------------------------------------- /tests/testthat/setup-chunks/error-check-chunk_bad.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Error checking chunks require standard check chunks" 3 | author: "Garrick Aden-Buie" 4 | output: learnr::tutorial 5 | runtime: shiny_prerendered 6 | description: > 7 | Error check chunks, -error-check, require a standard -check chunk 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | library(learnr) 12 | tutorial_options(exercise.checker = identity) 13 | ``` 14 | 15 | ```{r ex, exercise = TRUE} 16 | 1 17 | ``` 18 | 19 | ```{r ex-error-check} 20 | 2 21 | ``` 22 | -------------------------------------------------------------------------------- /tests/testthat/setup-chunks/error-check-chunk_good.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Error checking chunks require standard check chunks" 3 | author: "Garrick Aden-Buie" 4 | output: learnr::tutorial 5 | runtime: shiny_prerendered 6 | description: This example should render 7 | --- 8 | 9 | ```{r setup, include = FALSE} 10 | library(learnr) 11 | tutorial_options(exercise.checker = identity) 12 | ``` 13 | 14 | ```{r ex, exercise = TRUE} 15 | 1 16 | ``` 17 | 18 | ```{r ex-error-check} 19 | 2 20 | ``` 21 | 22 | ```{r ex-check} 23 | 3 24 | ``` 25 | -------------------------------------------------------------------------------- /tests/testthat/setup-chunks/setup-cycle-two.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Chained setup chunks: Cycles" 3 | author: "Nischal Shrestha" 4 | output: learnr::tutorial 5 | runtime: shiny_prerendered 6 | description: > 7 | This is a demo of catching cyclic dependencies with chained setup chunks starting from a setup chunk. 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | library(learnr) 12 | ``` 13 | 14 | ```{r dataA, exercise.setup = "dataB"} 15 | 1 16 | ``` 17 | 18 | ```{r dataB, exercise.setup = "dataA"} 19 | 2 20 | ``` 21 | 22 | 23 | -------------------------------------------------------------------------------- /inst/tutorials/hello/hello.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello, Tutorial!" 3 | author: "J.J. Allaire" 4 | date: "March 1st, 2017" 5 | output: learnr::tutorial 6 | runtime: shiny_prerendered 7 | # Do not index/display tutorial by setting `private: true` 8 | # private: true 9 | description: > 10 | This is a demo tutorial. 11 | --- 12 | 13 | ```{r setup, include=FALSE} 14 | library(learnr) 15 | ``` 16 | 17 | The following code computes the answer to 1+1. Change it so it computes 2 + 2: 18 | 19 | ```{r addition, exercise=TRUE} 20 | 1 + 1 21 | ``` 22 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "learnr" 3 | output: md_document 4 | --- 5 | 6 | # learnr 7 | 8 | ```{r child="man/rmd-fragments/badges.Rmd"} 9 | ``` 10 | 11 | ```{r child="man/rmd-fragments/learnr-overview.Rmd"} 12 | ``` 13 | 14 | Learn more about the **learnr** package and try example tutorials online at . 15 | 16 | ## Installation 17 | 18 | ```{r child="man/rmd-fragments/learnr-install.Rmd"} 19 | ``` 20 | -------------------------------------------------------------------------------- /man/rmd-fragments/learnr-install.Rmd: -------------------------------------------------------------------------------- 1 | Install the latest official learnr release from CRAN: 2 | 3 | ``` r 4 | install.packages("learnr") 5 | ``` 6 | 7 | Or you can install the most recent version in-development from GitHub with the [remotes package](https://remotes.r-lib.org): 8 | 9 | ``` r 10 | # install.packages("remotes") 11 | remotes::install_github("rstudio/learnr") 12 | ``` 13 | 14 | learnr works best with a recent [version of RStudio](https://posit.co/download/rstudio-desktop/) (v1.0.136 or later) which includes tools for easily running and previewing tutorials. 15 | -------------------------------------------------------------------------------- /tests/testthat/setup-chunks/exercise-cycle-two.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Chained setup chunks: Cycles" 3 | author: "Nischal Shrestha" 4 | output: learnr::tutorial 5 | runtime: shiny_prerendered 6 | description: > 7 | This is a demo of catching cyclic dependencies with chained setup chunks starting from an exercise chunk. 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | library(learnr) 12 | ``` 13 | 14 | ```{r data1, exercise=TRUE, exercise.setup = "data2"} 15 | 1 16 | 17 | ``` 18 | 19 | ```{r data2, exercise=TRUE, exercise.setup = "data1"} 20 | 2 21 | ``` 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/testthat/setup-chunks/setup-cycle.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Chained setup chunks: Cycles" 3 | author: "Nischal Shrestha" 4 | output: learnr::tutorial 5 | runtime: shiny_prerendered 6 | description: > 7 | This is a demo of catching cyclic dependencies with chained setup chunks starting from a setup chunk. 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | library(learnr) 12 | ``` 13 | 14 | ```{r dataA, exercise.setup = "dataC"} 15 | 1 16 | ``` 17 | 18 | ```{r dataB, exercise.setup = "dataA"} 19 | 2 20 | ``` 21 | 22 | ```{r dataC, exercise.setup = "dataB"} 23 | 3 24 | ``` 25 | -------------------------------------------------------------------------------- /tools/deploy_tutorials_on_ci.R: -------------------------------------------------------------------------------- 1 | 2 | if (!requireNamespace("remotes")) { 3 | install.packages("remotes") 4 | } 5 | remotes::install_cran("rsconnect") 6 | 7 | # Set the account info for deployment. 8 | rsconnect::setAccountInfo( 9 | name = Sys.getenv("SHINYAPPS_NAME"), # learnr-examples 10 | token = Sys.getenv("SHINYAPPS_TOKEN"), 11 | secret = Sys.getenv("SHINYAPPS_SECRET") 12 | ) 13 | 14 | # deploy all tutorials 15 | # deploy using callr with `show = TRUE` 16 | # to avoid "no output to travis console for 10 mins" error 17 | source("tools/deploy_tutorials_on_local.R") 18 | -------------------------------------------------------------------------------- /tests/testthat/test-storage.R: -------------------------------------------------------------------------------- 1 | 2 | context("storage") 3 | 4 | test_that("filesystem storage can be created", { 5 | fs <- filesystem_storage(tempfile()) 6 | expect_equal(fs$type, "local") 7 | }) 8 | 9 | test_that("objects cna be saved into filesystem storage", { 10 | fs <- filesystem_storage(tempfile()) 11 | fs$save_object("tutorial_id", "tutorial_version", "user_id", "object_id", "data") 12 | obj <- fs$get_object("tutorial_id", "tutorial_version", "user_id", "object_id") 13 | expect_equal(obj, "data") 14 | fs$remove_all_objects("tutorial_id", "tutorial_version", "user_id") 15 | }) -------------------------------------------------------------------------------- /R/shinytest.R: -------------------------------------------------------------------------------- 1 | # Input processor, for generating code in shinytest Recorder app 2 | register_shinytest_inputprocessor <- function() { 3 | if (is_installed("shinytest2", "0.1.0")) { 4 | shinytest2::register_input_processor("learnr.exercise", function(value) { 5 | # Drop all information from `value` except `code`. 6 | value <- value["code"] 7 | dput_to_string(value) 8 | }) 9 | } 10 | } 11 | 12 | # Snapshot preprocessor, for massaging input value before taking snapshot. 13 | snapshotPreprocessorLearnrExercise <- function(value) { 14 | value$timestamp <- NULL 15 | value 16 | } 17 | -------------------------------------------------------------------------------- /tests/testthat/setup-chunks/exercise-global-setup.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Exercise-specific global setup chunk" 3 | output: learnr::tutorial 4 | runtime: shiny_prerendered 5 | description: > 6 | A demo tutorial with a separate global exercise setup chunk 7 | --- 8 | 9 | ```{r setup, include = FALSE} 10 | library(learnr) 11 | ``` 12 | 13 | ```{r setup-global-exercise} 14 | global <- 0 15 | ``` 16 | 17 | 18 | 19 | ```{r data1, exercise = TRUE} 20 | 1 21 | ``` 22 | 23 | ```{r data1-setup} 24 | 2 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /tests/testthat/test-dependency.R: -------------------------------------------------------------------------------- 1 | 2 | context("dependency") 3 | 4 | test_that("tutor html dependencies can be retreived", { 5 | dep <- tutorial_html_dependency() 6 | expect_equal(dep$name, "tutorial") 7 | }) 8 | 9 | test_that("tutorial package dependencies can be enumerated", { 10 | packages <- tutorial_package_dependencies("ex-data-summarise", "learnr") 11 | expect_true("tidyverse" %in% packages) 12 | }) 13 | test_that("Per package, tutorial package dependencies can be enumerated", { 14 | packages <- tutorial_package_dependencies(package = "learnr") 15 | expect_true("tidyverse" %in% packages) 16 | }) 17 | -------------------------------------------------------------------------------- /man/tutorial_html_dependency.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/html-dependencies.R 3 | \name{tutorial_html_dependency} 4 | \alias{tutorial_html_dependency} 5 | \title{Tutorial HTML dependency} 6 | \usage{ 7 | tutorial_html_dependency() 8 | } 9 | \value{ 10 | \pkg{learnr}'s HTML dependencies 11 | } 12 | \description{ 13 | HTML dependency for core tutorial JS and CSS. This should be included as a 14 | dependency for custom tutorial formats that wish to ensure that that 15 | \code{tutorial.js} and \code{tutorial.css} are loaded prior their own scripts and 16 | stylesheets. 17 | } 18 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/questionquiz.md: -------------------------------------------------------------------------------- 1 | ```{r quiz1, echo=FALSE} 2 | quiz(caption = "Quiz 1", 3 | question("What number is the letter A in the *English* alphabet?", 4 | answer("8"), 5 | answer("14"), 6 | answer("1", correct = TRUE), 7 | answer("23") 8 | ), 9 | question("Where are you right now? (select ALL that apply)", 10 | answer("Planet Earth", correct = TRUE), 11 | answer("Pluto"), 12 | answer("At a computing device", correct = TRUE), 13 | answer("In the Milky Way", correct = TRUE), 14 | incorrect = "Incorrect. You're on Earth, in the Milky Way, at a computer." 15 | ) 16 | ) 17 | ``` -------------------------------------------------------------------------------- /inst/lib/idb-keyval/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016, Jake Archibald 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /inst/tutorials/slidy/slidy.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: slidy_presentation 3 | runtime: shiny_prerendered 4 | title: "Slidy demo" 5 | description: > 6 | This tutorial demonstrates how one can use dygraphs (with the dygraph package) 7 | as part of a learnr tutorial. 8 | --- 9 | 10 | ```{r setup, include=FALSE} 11 | library(learnr) 12 | library(dygraphs) 13 | ``` 14 | 15 | ## Time Series with dygraphs 16 | 17 | Modify the dyOptions to customize the graph's appearance: 18 | 19 | ```{r dygraph-options, exercise=TRUE, exercise.eval=TRUE, fig.height=5.5} 20 | dygraph(ldeaths) %>% 21 | dyOptions(fillGraph = TRUE, drawGrid = TRUE) 22 | ``` 23 | -------------------------------------------------------------------------------- /tests/testthat/setup-chunks/exercise-cycle.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Chained setup chunks: Cycles" 3 | author: "Nischal Shrestha" 4 | output: learnr::tutorial 5 | runtime: shiny_prerendered 6 | description: > 7 | This is a demo of catching cyclic dependencies with chained setup chunks starting from an exercise chunk. 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | library(learnr) 12 | ``` 13 | 14 | ```{r data1, exercise=TRUE, exercise.setup = "data3"} 15 | 1 16 | 17 | ``` 18 | 19 | ```{r data2, exercise=TRUE, exercise.setup = "data1"} 20 | 2 21 | ``` 22 | 23 | ```{r data3, exercise=as.logical(1), exercise.setup = "data2"} 24 | 3 25 | 26 | ``` 27 | -------------------------------------------------------------------------------- /tests/testthat/tutorials/knitr-hooks_empty-exercise/empty-exercise.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Empty Exercise Code 3 | output: learnr::tutorial 4 | runtime: shinyrmd 5 | --- 6 | 7 | ```{r setup, include = FALSE} 8 | library(learnr) 9 | tutorial_options(exercise.lines = 4L) 10 | 11 | knitr::opts_chunk$set( 12 | echo = FALSE, 13 | custom_chunk_opt = "default", 14 | fig.path = "figures", 15 | cache.path = "cache" 16 | ) 17 | ``` 18 | 19 | ## Test 20 | 21 | ### Empty 22 | 23 | ```{r empty, exercise = TRUE, custom_chunk_opt = "custom"} 24 | ``` 25 | 26 | ```{r empty-hint} 27 | # hint code 28 | ``` 29 | 30 | ```{r empty-solution} 31 | mtcars 32 | ``` 33 | -------------------------------------------------------------------------------- /tests/testthat/setup-chunks/exercise-cycle-default-setup.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Chained setup chunks: Cycles" 3 | author: "Nischal Shrestha" 4 | output: learnr::tutorial 5 | runtime: shiny_prerendered 6 | description: > 7 | This is a demo of catching cyclic dependencies with setup chunks starting from an exercise chunk with no exercise.setup set. 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | library(learnr) 12 | ``` 13 | 14 | 15 | 16 | ```{r data1, exercise = TRUE} 17 | 1 18 | ``` 19 | 20 | ```{r data1-setup, exercise.setup = "data1"} 21 | 2 22 | ``` 23 | 24 | -------------------------------------------------------------------------------- /tests/testthat/test-shinytest2-aaa.R: -------------------------------------------------------------------------------- 1 | # https://github.com/rstudio/shinytest2/blob/c29b78e9/tests/testthat/test-aaa.R 2 | skip_on_cran() # Uses chromote 3 | skip_on_ci_if_not_pr() 4 | 5 | # Try to warm up chromote. IDK why it fails on older versions of R. 6 | test_that("Chromote loads", { 7 | on_ci <- isTRUE(as.logical(Sys.getenv("CI"))) 8 | skip_if(!on_ci, "Not on CI") 9 | 10 | # Wrap in a `try()` as the test doesn't matter 11 | # Only the action of trying to open chromote matters 12 | try({ 13 | chromote <- utils::getFromNamespace("default_chromote_object", "chromote")() 14 | chromote$new_session() 15 | }) 16 | 17 | expect_true(TRUE) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/testthat/tutorials/knitr-hooks_empty-exercise/full-exercise.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Empty Exercise Code 3 | output: learnr::tutorial 4 | runtime: shinyrmd 5 | --- 6 | 7 | ```{r setup, include = FALSE} 8 | library(learnr) 9 | tutorial_options(exercise.lines = 4L) 10 | 11 | knitr::opts_chunk$set( 12 | echo = FALSE, 13 | custom_chunk_opt = "default", 14 | fig.path = "figures", 15 | cache.path = "cache" 16 | ) 17 | ``` 18 | 19 | ## Test 20 | 21 | ### Empty 22 | 23 | ```{r empty, exercise = TRUE, custom_chunk_opt = "custom"} 24 | 25 | 26 | 27 | 28 | ``` 29 | 30 | ```{r empty-hint} 31 | # hint code 32 | ``` 33 | 34 | ```{r empty-solution} 35 | mtcars 36 | ``` 37 | -------------------------------------------------------------------------------- /inst/rmarkdown/templates/tutorial/resources/images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/testthat/tutorials/exercise-option-is-symbol.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Test: Exercise Option is a Symbol" 3 | output: learnr::tutorial 4 | runtime: shiny_prerendered 5 | --- 6 | 7 | ```{r setup, include=FALSE} 8 | library(learnr) 9 | do_exercise <- TRUE 10 | knitr::opts_chunk$set(echo = FALSE) 11 | ``` 12 | 13 | 14 | ## Topic 15 | 16 | ### Exercise T 17 | 18 | *Here's a simple exercise with an empty code chunk provided for entering the answer.* 19 | 20 | Write the R code required to add two plus two: 21 | 22 | ```{r symbol, exercise = T} 23 | "symbol" 24 | ``` 25 | 26 | ### Exercise Variable 27 | 28 | ```{r variable, exercise = do_exercise} 29 | "variable (also a symbol)" 30 | ``` 31 | -------------------------------------------------------------------------------- /inst/rmarkdown/templates/tutorial/resources/images/exerciseDone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/tutorstorage.md: -------------------------------------------------------------------------------- 1 | options(tutorial.storage = list( 2 | 3 | # save an arbitrary R object "data" to storage 4 | save_object = function(tutorial_id, tutorial_version, user_id, object_id, data) { 5 | 6 | }, 7 | 8 | # retreive a single R object from storage 9 | get_object = function(tutorial_id, tutorial_version, user_id, object_id) { 10 | NULL 11 | }, 12 | 13 | # retreive a list of all R objects stored 14 | get_objects = function(tutorial_id, tutorial_version, user_id) { 15 | list() 16 | }, 17 | 18 | # remove all stored R objects 19 | remove_all_objects = function(tutorial_id, tutorial_version, user_id) { 20 | 21 | } 22 | ) 23 | 24 | -------------------------------------------------------------------------------- /man/initialize_tutorial.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/initialize.R 3 | \name{initialize_tutorial} 4 | \alias{initialize_tutorial} 5 | \title{Initialize tutorial R Markdown extensions} 6 | \usage{ 7 | initialize_tutorial() 8 | } 9 | \value{ 10 | If not previously run, initializes knitr hooks and provides the 11 | required \code{\link[rmarkdown:shiny_prerendered_chunk]{rmarkdown::shiny_prerendered_chunk()}}s to initialize \pkg{learnr}. 12 | } 13 | \description{ 14 | One time initialization of R Markdown extensions required by the 15 | \pkg{learnr} package. This function is typically called automatically 16 | as a result of using exercises or questions. 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name : Feature request 3 | about : Request a new feature. 4 | --- 5 | 6 | 15 | -------------------------------------------------------------------------------- /man/safe_env.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/run.R 3 | \name{safe_env} 4 | \alias{safe_env} 5 | \title{Safe R CMD environment} 6 | \usage{ 7 | safe_env() 8 | } 9 | \value{ 10 | A list of envvars, modified from \code{\link[callr:rcmd_safe_env]{callr::rcmd_safe_env()}}. 11 | } 12 | \description{ 13 | By default, \code{callr::\link[callr]{rcmd_safe_env}} suppresses the ability 14 | to open a browser window. This is the default execution environment within 15 | \code{callr::\link[callr]{r}}. However, opening a browser is expected 16 | behavior within the learnr package and should not be suppressed. 17 | } 18 | \examples{ 19 | safe_env() 20 | 21 | } 22 | \keyword{internal} 23 | -------------------------------------------------------------------------------- /vignettes/articles/snippets.R: -------------------------------------------------------------------------------- 1 | insert_snippet <- function(key) { 2 | #
3 | # 4 | snippet <- readLines(fs::path("../../pkgdown/assets/snippets", key, ext = "md"), warn = FALSE) 5 | snippet <- paste(snippet, collapse = "\n") 6 | snippet <- gsub("`", "`", snippet) 7 | snippet <- htmltools::HTML(snippet) 8 | 9 | htmltools::withTags( 10 | htmltools::tagList( 11 | div( 12 | id = key, 13 | class = "sourceCode", 14 | pre(class = "markdown", code(snippet)), 15 | ), 16 | script(type = "text/javascript", htmltools::HTML(sprintf("loadSnippet('%s')", key))) 17 | ) 18 | ) 19 | } -------------------------------------------------------------------------------- /R/ace.R: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by 'tools/update-ace.R' 2 | ACE_VERSION <- "1.10.1" 3 | ACE_THEMES <- c("ambiance", "chaos", "chrome", "cloud9_day", "cloud9_night_low_color", "cloud9_night", "clouds_midnight", "clouds", "cobalt", "crimson_editor", "dawn", "dracula", "dreamweaver", "eclipse", "github", "gob", "gruvbox_dark_hard", "gruvbox_light_hard", "gruvbox", "idle_fingers", "iplastic", "katzenmilch", "kr_theme", "kuroir", "merbivore_soft", "merbivore", "mono_industrial", "monokai", "nord_dark", "one_dark", "pastel_on_dark", "solarized_dark", "solarized_light", "sqlserver", "terminal", "textmate", "tomorrow_night_blue", "tomorrow_night_bright", "tomorrow_night_eighties", "tomorrow_night", "tomorrow", "twilight", "vibrant_ink", "xcode") 4 | -------------------------------------------------------------------------------- /man/disable_all_tags.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/mutate_tags.R 3 | \name{disable_all_tags} 4 | \alias{disable_all_tags} 5 | \title{Disable all html tags} 6 | \usage{ 7 | disable_all_tags(ele) 8 | } 9 | \arguments{ 10 | \item{ele}{html tag element} 11 | } 12 | \value{ 13 | An \pkg{htmltools} HTML object with appended \code{class = "disabled"} and 14 | \code{disabled} attributes on all tags. 15 | } 16 | \description{ 17 | Method to disable all html tags to not allow users to interact with the html. 18 | } 19 | \examples{ 20 | # add an href to all a tags 21 | disable_all_tags( 22 | htmltools::tagList( 23 | htmltools::a(), 24 | htmltools::a() 25 | ) 26 | ) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name : Ask a Question 3 | about : The issue tracker is not for general help questions -- please ask general learnr help questions at https://community.rstudio.com/c/shiny. 4 | --- 5 | 6 | The issue tracker is not for questions. If you have a question, please feel free to ask learnr help questions on our community site under the teaching category, at https://community.rstudio.com/c/teaching. Please tag your post with a learnr tag to increase visibility. 7 | 8 | Open a new post [here (teaching `category` with `learnr` tag)](https://community.rstudio.com/new-topic?title=&category_id=13&tags=learnr&body=%0A%0A%0A%20%20--------%0A%20%20%0A%20%20%3Csup%3EReferred%20here%20by%20%60learnr%60%27s%20GitHub%3C/sup%3E%0A&u=barret). 9 | -------------------------------------------------------------------------------- /tests/testthat/tutorials/hint-next/hint-next.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Next hint tests" 3 | output: learnr::tutorial 4 | runtime: shiny_prerendered 5 | --- 6 | 7 | ## Tests 8 | 9 | ### One hint 10 | 11 | ```{r one, exercise = TRUE} 12 | 1+1 13 | ``` 14 | 15 | ```{r one-hint} 16 | # one hint 17 | ``` 18 | 19 | ### Two hints 20 | 21 | ```{r two, exercise = TRUE} 22 | 1+1 23 | ``` 24 | 25 | ```{r two-hint-1} 26 | # first hint 27 | ``` 28 | 29 | ```{r two-hint-2} 30 | # second hint 31 | ``` 32 | 33 | ### Two hints plus solution 34 | 35 | ```{r three, exercise = TRUE} 36 | 1+1 37 | ``` 38 | 39 | ```{r three-hint-1} 40 | # 3 - first hint 41 | ``` 42 | 43 | ```{r three-hint-2} 44 | # 3 - second hint 45 | ``` 46 | 47 | ```{r three-solution} 48 | 2 + 2 49 | ``` 50 | -------------------------------------------------------------------------------- /man/knit_print.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/quiz.R 3 | \name{knit_print.tutorial_question} 4 | \alias{knit_print.tutorial_question} 5 | \alias{knit_print.tutorial_quiz} 6 | \title{Knitr quiz print methods} 7 | \usage{ 8 | \method{knit_print}{tutorial_question}(x, ...) 9 | 10 | \method{knit_print}{tutorial_quiz}(x, ...) 11 | } 12 | \arguments{ 13 | \item{x}{An R object to be printed} 14 | 15 | \item{...}{Additional arguments passed to the S3 method. Currently ignored, 16 | except two optional arguments \code{options} and \code{inline}; see 17 | the references below.} 18 | } 19 | \description{ 20 | \code{knitr::\link[knitr]{knit_print}} methods for \code{\link{question}} and 21 | \code{\link{quiz}} 22 | } 23 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^README.R?md$ 4 | ^dependencies$ 5 | ^design$ 6 | ^docs$ 7 | ^examples$ 8 | ^sandbox$ 9 | ^tools$ 10 | ^issues$ 11 | ^.*/skeleton/skeleton.html 12 | 13 | ^\.travis\.yml$ 14 | ^\.atom 15 | 16 | rsconnect/ 17 | ^\.github/ 18 | ^inst/tutorials/.*\.html$ 19 | ^inst/tutorials/.*\.sqlite$ 20 | ^revdep$ 21 | ^LICENSE\.md$ 22 | ^cran-comments\.md$ 23 | ^CRAN-RELEASE$ 24 | ^doc$ 25 | ^Meta$ 26 | ^\.github 27 | ^data-raw$ 28 | ^\.github$ 29 | ^vignettes/articles$ 30 | ^reference$ 31 | ^pkgdown$ 32 | ^_dev$ 33 | ^learnr-js$ 34 | ^node_modules$ 35 | ^package\.json$ 36 | ^package-lock\.json$ 37 | ^\.browserslistrc$ 38 | ^babel\.config\.json$ 39 | ^\.eslintignore$ 40 | ^\.vscode$ 41 | ^inst/tutorials/*/*_files$ 42 | ^tests/manual$ 43 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/customchecker.md: -------------------------------------------------------------------------------- 1 | ```r 2 | custom_checker <- function(label, user_code, check_code, envir_result, evaluate_result, last_value, stage, ...) { 3 | # this is a code check 4 | if (stage == "code_check") { 5 | if (is_bad_code(user_code, check_code)) { 6 | return(list(message = "I wasn't expecting that code", correct = FALSE)) 7 | } 8 | return(list(message = "Nice code!", correct = TRUE)) 9 | } 10 | # this is a fully evaluated chunk check 11 | if (is_bad_result(last_value, check_code)) { 12 | return(list(message = "I wasn't expecting that result", correct = FALSE)) 13 | } 14 | list(message = "Great job!", correct = TRUE, location = "append") 15 | } 16 | 17 | tutorial_options(exercise.checker = custom_checker) 18 | ``` 19 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/codefeedback.md: -------------------------------------------------------------------------------- 1 | ```r 2 | custom_checker <- function(label, user_code, check_code, envir_result, evaluate_result, last_value, ...) { 3 | if (is.null(envir_result)) { 4 | # check_code contains `*-code-check` code 5 | if (is_bad_code(user_code, check_code)) { 6 | return(list(message = "I wasn't expecting that code", correct = FALSE)) 7 | } 8 | return(list(message = "Nice code!", correct = TRUE)) 9 | } 10 | 11 | # check_code contains `*-check` code 12 | if (is_bad_result(last_value, check_code)) { 13 | return(list(message = "I wasn't expecting that result", correct = FALSE)) 14 | } 15 | list(message = "Great job!", correct = TRUE, location = "append") 16 | } 17 | 18 | tutorial_options(exercise.checker = custom_checker) 19 | ``` -------------------------------------------------------------------------------- /tests/testthat/tutorials/knitr-hooks_empty-exercise/duplicate-label.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Empty Exercise Code with Duplicate 3 | output: learnr::tutorial 4 | runtime: shinyrmd 5 | --- 6 | 7 | ```{r setup, include = FALSE} 8 | library(learnr) 9 | tutorial_options(exercise.lines = 4L) 10 | 11 | knitr::opts_chunk$set( 12 | echo = FALSE, 13 | custom_chunk_opt = "default", 14 | fig.path = "figures", 15 | cache.path = "cache" 16 | ) 17 | ``` 18 | 19 | ## Test 20 | 21 | ### First empty 22 | 23 | This tutorial should fail to parse. 24 | 25 | ```{r empty} 26 | 27 | 28 | ``` 29 | 30 | ### Empty 31 | 32 | ```{r empty, exercise = TRUE, custom_chunk_opt = "custom"} 33 | ``` 34 | 35 | ```{r empty-hint} 36 | # hint code 37 | ``` 38 | 39 | ```{r empty-solution} 40 | mtcars 41 | ``` 42 | -------------------------------------------------------------------------------- /man/filesystem_storage.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/storage.R 3 | \name{filesystem_storage} 4 | \alias{filesystem_storage} 5 | \title{Filesystem-based storage for tutor state data} 6 | \usage{ 7 | filesystem_storage(dir, compress = TRUE) 8 | } 9 | \arguments{ 10 | \item{dir}{Directory to store state data within} 11 | 12 | \item{compress}{Should \code{.rds} files be compressed?} 13 | } 14 | \value{ 15 | Storage handler suitable for \code{options(tutorial.storage = ...)} 16 | } 17 | \description{ 18 | Tutorial state storage handler that uses the filesystem as a backing store. 19 | The directory will contain tutorial state data partitioned by \code{user_id}, 20 | \code{tutorial_id}, and \code{tutorial_version} (in that order) 21 | } 22 | -------------------------------------------------------------------------------- /tests/testthat/test-cookies.R: -------------------------------------------------------------------------------- 1 | context("cookies") 2 | 3 | test_that("cookies are properly serialized", { 4 | cookies <- structure(list( 5 | domain = c("httpbin.org", "httpbin.org"), 6 | flag = c(FALSE, FALSE), 7 | path = c("/", "/"), 8 | secure = c(FALSE, FALSE), 9 | expiration = c(1587586247L, 0L), 10 | name = c("foo", "bar"), 11 | value = c("123", "ftw") 12 | ), 13 | row.names = c(NA, -2L), 14 | class = "data.frame") 15 | 16 | f <- tempfile() 17 | on.exit({unlink(f)}) 18 | 19 | write_cookies(cookies, f) 20 | txt <- readLines(f) 21 | expect_equal(length(txt), 2) 22 | expect_equal(txt[1], "httpbin.org\tFALSE\t/\tFALSE\t1587586247\tfoo\t123") 23 | expect_equal(txt[2], "httpbin.org\tFALSE\t/\tFALSE\t0\tbar\tftw") 24 | }) 25 | 26 | -------------------------------------------------------------------------------- /man/available_tutorials.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/available_tutorials.R 3 | \name{available_tutorials} 4 | \alias{available_tutorials} 5 | \title{List available tutorials} 6 | \usage{ 7 | available_tutorials(package = NULL) 8 | } 9 | \arguments{ 10 | \item{package}{Name of package} 11 | } 12 | \value{ 13 | \code{available_tutorials()} returns a \code{data.frame} containing "package", 14 | "name", "title", "description", "package_dependencies", "private", and 15 | "yaml_front_matter". 16 | } 17 | \description{ 18 | List the tutorials that are currently available via installed R packages. 19 | Or list the specific tutorials that are contained within a given R package. 20 | } 21 | \examples{ 22 | available_tutorials(package = "learnr") 23 | 24 | } 25 | -------------------------------------------------------------------------------- /man/rmd-fragments/learnr-overview.Rmd: -------------------------------------------------------------------------------- 1 | The **learnr** package makes it easy to turn any [R Markdown](https://rmarkdown.rstudio.com/) document into an interactive tutorial. Tutorials consist of content along with interactive components for checking and reinforcing understanding. Tutorials can include any or all of the following: 2 | 3 | 1. Narrative, figures, illustrations, and equations. 4 | 5 | 2. Code exercises (R code chunks that users can edit and execute directly). 6 | 7 | 3. Quiz questions. 8 | 9 | 4. Videos (supported services include YouTube and Vimeo). 10 | 11 | 5. Interactive Shiny components. 12 | 13 | Tutorials automatically preserve work done within them, so if a user works on a few exercises or questions and returns to the tutorial later they can pick up right where they left off. 14 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/available-tutorials.md: -------------------------------------------------------------------------------- 1 | # Tutorial names are retrieved 2 | 3 | Code 4 | available_tutorials("learnr") 5 | Output 6 | Available tutorials: 7 | * learnr 8 | - ex-data-basics : "Data basics" 9 | - ex-data-filter : "Filter observations" 10 | - ex-data-mutate : "Create new variables" 11 | - ex-data-summarise : "Summarise Tables" 12 | - ex-setup-r : "Set Up" 13 | - hello : "Hello, Tutorial!" 14 | - polyglot : "Multi-language exercises" 15 | - quiz_question : "Tutorial Quiz Questions in `learnr`" 16 | - setup-chunks : "Chained setup chunks" 17 | - slidy : "Slidy demo" 18 | - sql-exercise : "Interactive SQL Exercises" 19 | 20 | -------------------------------------------------------------------------------- /tests/testthat/test-duplicate_env.R: -------------------------------------------------------------------------------- 1 | 2 | context("duplicate_env") 3 | 4 | test_that("it duplicates", { 5 | 6 | e <- new.env(parent = baseenv()) 7 | e$x <- 1 8 | e$.key <- "value" 9 | 10 | new_envir <- duplicate_env(e) 11 | 12 | # check parent is equivalent 13 | expect_true(identical(parent.env(new_envir), baseenv())) 14 | 15 | # check keys exist 16 | test_names <- c("x", ".key") 17 | envir_names <- ls(envir = new_envir, all.names = TRUE) 18 | expect_equal(sort(test_names), sort(envir_names)) 19 | }) 20 | 21 | test_that("does not fail when the envir has a class", { 22 | e <- new.env() 23 | e$x <- 1 24 | class(e) <- "foo" 25 | 26 | expect_error({ 27 | as.list(e) 28 | }) 29 | 30 | expect_silent({ 31 | new_envir <- duplicate_env(e) 32 | }) 33 | 34 | expect_equal(new_envir$x, 1) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/testthat/test-available-tutorials.R: -------------------------------------------------------------------------------- 1 | 2 | context("available tutorials") 3 | 4 | test_that("Tutorial names are retrieved", { 5 | local_edition(3) 6 | 7 | expect_error(available_tutorials("not a package"), "No package found") 8 | expect_error(available_tutorials("base"), "No tutorials found") 9 | expect_true("hello" %in% available_tutorials("learnr")$name) 10 | expect_true("hello" %in% suppressMessages(run_tutorial(package = "learnr")$name)) 11 | expect_s3_class(available_tutorials("learnr"), "learnr_available_tutorials") 12 | 13 | expect_error(run_tutorial("helloo", package = "learnr"), "\"hello\"") 14 | expect_error(run_tutorial("doesn't exist", package = "learnr"), "Available ") 15 | expect_message(run_tutorial(package = "learnr"), "Available ") 16 | 17 | expect_snapshot(available_tutorials("learnr")) 18 | }) 19 | -------------------------------------------------------------------------------- /man/external_evaluator.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/evaluators.R 3 | \name{external_evaluator} 4 | \alias{external_evaluator} 5 | \title{External execution evaluator} 6 | \usage{ 7 | external_evaluator( 8 | endpoint = getOption("tutorial.external.host", 9 | Sys.getenv("TUTORIAL_EXTERNAL_EVALUATOR_HOST", NA)), 10 | max_curl_conns = 50 11 | ) 12 | } 13 | \arguments{ 14 | \item{endpoint}{The HTTP(S) endpoint to POST the exercises to} 15 | 16 | \item{max_curl_conns}{The maximum number of simultaneous HTTP requests to the 17 | endpoint.} 18 | } 19 | \value{ 20 | A function that takes an expression (\code{expr}), \code{timelimit}, \code{exercise} 21 | and \code{session}. 22 | } 23 | \description{ 24 | \href{https://lifecycle.r-lib.org/articles/stages.html}{Lifecycle: experimental} 25 | } 26 | -------------------------------------------------------------------------------- /tests/manual/tests-chunk.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Test Cases" 3 | output: learnr::tutorial 4 | runtime: shiny_prerendered 5 | --- 6 | 7 | ```{r setup, include=FALSE} 8 | library(learnr) 9 | tutorial_options(exercise.checker = function(user_code, ...) return(user_code)) 10 | knitr::opts_chunk$set(echo = FALSE) 11 | ``` 12 | 13 | ## Test Cases 14 | 15 | ### Exercise 16 | 17 | ```{r addition-setup} 18 | x <- 1 + 1 19 | ``` 20 | 21 | ```{r addition, exercise = TRUE} 22 | 23 | ``` 24 | 25 | ```{r addition-solution} 26 | 1 + 1 27 | ``` 28 | 29 | ```{r addition-check} 30 | check_this_exercise(user_code, solution_code) 31 | ``` 32 | 33 | ```{r addition-tests} 34 | 1 + 1 35 | 36 | # one plus two ---- 37 | 1 + 2 38 | 39 | # one plus three ---- 40 | 1 + 3 41 | 42 | # one equals three ---- 43 | 1 = 3 44 | 45 | # 2 minus one ---- 46 | 2 - 1 47 | ``` 48 | -------------------------------------------------------------------------------- /R/events_record.R: -------------------------------------------------------------------------------- 1 | record_event <- function(session, event, data) { 2 | recorder <- getOption("tutorial.event_recorder", default = NULL) 3 | if (!is.null(recorder)) { 4 | recorder(tutorial_id = read_request(session, "tutorial.tutorial_id"), 5 | tutorial_version = read_request(session, "tutorial.tutorial_version"), 6 | user_id = read_request(session, "tutorial.user_id"), 7 | event = event, 8 | data = data) 9 | } 10 | invisible(NULL) 11 | } 12 | 13 | 14 | debug_event_recorder <- function( 15 | tutorial_id, 16 | tutorial_version, 17 | user_id, 18 | event, 19 | data 20 | ) { 21 | cat(tutorial_id, " (", tutorial_version, "): ", user_id, "\n", sep = "") 22 | cat("event: ", event, "\n", sep = "") 23 | if (is.character(data)) cat(data) else utils::str(data) 24 | cat("\n") 25 | } 26 | -------------------------------------------------------------------------------- /man/rmd-fragments/hello-tutorial.Rmd: -------------------------------------------------------------------------------- 1 | To create a tutorial, set `runtime: shiny_prerendered` in the YAML frontmatter of your `.Rmd` file to turn your R Markdown document into an [interactive app](https://rmarkdown.rstudio.com/lesson-14.html). 2 | 3 | Then, call `library(learnr)` within your Rmd file to activate tutorial mode, and use the `exercise = TRUE` chunk option to turn code chunks into exercises. Users can edit and execute the R code and see the results right within their browser. 4 | 5 | For example, here's a very simple tutorial: 6 | 7 | ```{r snippet-hello-learnr, echo = FALSE} 8 | source("../../vignettes/articles/snippets.R") 9 | insert_snippet("hello-learnr") 10 | ``` 11 | 12 | This is what the running tutorial document looks like after the user has entered their answer: 13 | 14 | 15 | -------------------------------------------------------------------------------- /man/random_praise.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/praise.R 3 | \name{random_praise} 4 | \alias{random_praise} 5 | \alias{random_encouragement} 6 | \title{Random praise and encouragement} 7 | \usage{ 8 | random_praise(language = NULL) 9 | 10 | random_encouragement(language = NULL) 11 | } 12 | \arguments{ 13 | \item{language}{The language for the random phrase. The currently supported 14 | languages include: \code{en}, \code{es}, \code{pt}, \code{pl}, \code{tr}, \code{de}, \code{emo}, and \code{testing} 15 | (static phrases).} 16 | } 17 | \value{ 18 | Character string with a random saying 19 | } 20 | \description{ 21 | Random praises and encouragements sayings to compliment your question and 22 | quiz experience. 23 | } 24 | \examples{ 25 | random_praise() 26 | random_praise() 27 | 28 | random_encouragement() 29 | random_encouragement() 30 | 31 | } 32 | -------------------------------------------------------------------------------- /man/tutorial_package_dependencies.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tutorial_package_dependencies.R 3 | \name{tutorial_package_dependencies} 4 | \alias{tutorial_package_dependencies} 5 | \title{List tutorial dependencies} 6 | \usage{ 7 | tutorial_package_dependencies(name = NULL, package = NULL) 8 | } 9 | \arguments{ 10 | \item{name}{The tutorial name. If \code{name} is \code{NULL}, then all 11 | tutorials within \code{package} will be searched.} 12 | 13 | \item{package}{The \R package providing the tutorial. If \code{package} is 14 | \code{NULL}, then all tutorials will be searched.} 15 | } 16 | \value{ 17 | A character vector of package names that are required for execution. 18 | } 19 | \description{ 20 | List the \R packages required to run a particular tutorial. 21 | } 22 | \examples{ 23 | tutorial_package_dependencies(package = "learnr") 24 | 25 | } 26 | -------------------------------------------------------------------------------- /inst/staticexports/knitr_engine_caption.R: -------------------------------------------------------------------------------- 1 | 2 | knitr_engine_caption <- function(engine = NULL) { 3 | if (is.null(engine)) { 4 | engine <- "r" 5 | } 6 | 7 | switch( 8 | tolower(engine), 9 | "bash" = "Bash", 10 | "c" = "C", 11 | "coffee" = "CoffeeScript", 12 | "cc" = "C++", 13 | "css" = "CSS", 14 | "go" = "Go", 15 | "groovy" = "Groovy", 16 | "haskell" = "Haskell", 17 | "js" = "JavaScript", 18 | "mysql" = "MySQL", 19 | "node" = "Node.js", 20 | "octave" = "Octave", 21 | "psql" = "PostgreSQL", 22 | "python" = "Python", 23 | "r" = "R", 24 | "rcpp" = "Rcpp", 25 | "cpp11" = "cpp11", 26 | "rscript" = "Rscript", 27 | "ruby" = "Ruby", 28 | "perl" = "Perl", 29 | "sass" = "Sass", 30 | "scala" = "Scala", 31 | "scss" = "SCSS", 32 | "sql" = "SQL", 33 | # else, return as the user provided 34 | engine 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions to the **learnr** package! 4 | 5 | To submit a contribution: 6 | 7 | 1. [Fork](https://github.com/rstudio/learnr/fork) the repository and make your changes. 8 | 9 | 2. Ensure that you have signed the [individual](https://www.rstudio.com/wp-content/uploads/2014/06/rstudioindividualcontributoragreement.pdf) or [corporate](https://www.rstudio.com/wp-content/uploads/2014/06/rstudiocorporatecontributoragreement.pdf) contributor agreement as appropriate. You can send the signed copy to contribute@rstudio.com, cc'ing barret@rstudio.com. 10 | 11 | 3. Submit a [pull request](https://help.github.com/articles/using-pull-requests). 12 | 13 | We generally do not merge pull requests that update included web libraries (such as Bootstrap or jQuery) because it is difficult for us to verify that the update is done correctly; we prefer to update these libraries ourselves. 14 | -------------------------------------------------------------------------------- /man/finalize_question.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/mutate_tags.R 3 | \name{finalize_question} 4 | \alias{finalize_question} 5 | \title{Finalize a question} 6 | \usage{ 7 | finalize_question(ele) 8 | } 9 | \arguments{ 10 | \item{ele}{html tag element} 11 | } 12 | \value{ 13 | An \pkg{htmltools} HTML object with appropriately appended classes 14 | such that a tutorial question is marked as the final answer. 15 | } 16 | \description{ 17 | Mark a question as finalized by adding a \code{question-final} class to the HTML 18 | output at the top level, in addition to disabling all tags with 19 | \code{\link[=disable_all_tags]{disable_all_tags()}}. 20 | } 21 | \examples{ 22 | # finalize the question UI 23 | finalize_question( 24 | htmltools::div( 25 | class = "custom-question", 26 | htmltools::div("answer 1"), 27 | htmltools::div("answer 2") 28 | ) 29 | ) 30 | 31 | } 32 | -------------------------------------------------------------------------------- /pkgdown/index.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "learnr" 3 | output: md_document 4 | --- 5 | 6 | # learnr 7 | 8 | ```{r child="../man/rmd-fragments/badges.Rmd"} 9 | ``` 10 | 11 | ```{r child="../man/rmd-fragments/learnr-overview.Rmd"} 12 | ``` 13 | 14 | ## Examples 15 | 16 | Here are some examples of tutorials created with the **learnr** package. 17 | 18 | ```{r example-showcase, echo=FALSE} 19 | source("../vignettes/articles/example_cards.R") 20 | ex <- yaml::read_yaml("../vignettes/articles/examples.yml") 21 | ex <- lapply(ex, function(x) { 22 | x$image <- paste0("articles/", x$image) 23 | x 24 | }) 25 | example_cards(ex, "showcase") 26 | ``` 27 | 28 | ## Installation 29 | 30 | ```{r child="../man/rmd-fragments/learnr-install.Rmd"} 31 | ``` 32 | 33 | ## Hello, Tutorial! 34 | 35 | ```{r child="../man/rmd-fragments/hello-tutorial.Rmd"} 36 | ``` 37 | -------------------------------------------------------------------------------- /man/figures/lifecycle-stable.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclestablestable -------------------------------------------------------------------------------- /man/figures/lifecycle-defunct.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycledefunctdefunct -------------------------------------------------------------------------------- /pkgdown/assets/snippets/exerciseevaluator.md: -------------------------------------------------------------------------------- 1 | options(tutorial.exercise.evaluator.onstart = function(pid) { 2 | 3 | # import RAppArmor 4 | require(RAppArmor, quietly = TRUE) 5 | 6 | # set process group to pid (allows kill of entire subtree in cleanup) 7 | setpgid(); 8 | 9 | # set nice priority 10 | setpriority(10) 11 | 12 | # set rlimits as appropriate 13 | rlimit_nproc(1000) 14 | rlimit_as(1024*1024*1024) 15 | 16 | # change to r-user profile (see note above on required edit to r-user) 17 | aa_change_profile("r-user") 18 | }) 19 | 20 | options(tutorial.exercise.evaluator.oncleanup = function(pid) { 21 | 22 | # import RAppArmor 23 | require(RAppArmor, quietly = TRUE) 24 | 25 | # kill entire process subtree. note that the second call works 26 | # because the call to setpgid above sets our pgid (process group id) 27 | # to our pid (process id) 28 | kill(pid, tools::SIGKILL) 29 | kill(-1 * pid, tools::SIGKILL) 30 | }) 31 | -------------------------------------------------------------------------------- /man/figures/lifecycle-archived.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclearchivedarchived -------------------------------------------------------------------------------- /man/figures/lifecycle-maturing.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclematuringmaturing -------------------------------------------------------------------------------- /man/figures/lifecycle-deprecated.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycledeprecateddeprecated -------------------------------------------------------------------------------- /man/figures/lifecycle-superseded.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclesupersededsuperseded -------------------------------------------------------------------------------- /man/figures/lifecycle-experimental.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycleexperimentalexperimental -------------------------------------------------------------------------------- /man/figures/lifecycle-questioning.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclequestioningquestioning -------------------------------------------------------------------------------- /man/figures/lifecycle-soft-deprecated.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclesoft-deprecatedsoft-deprecated -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | 2 | # install knitr hooks when package is attached to search path 3 | .onAttach <- function(libname, pkgname) { 4 | install_knitr_hooks() 5 | initialize_tutorial() 6 | } 7 | 8 | # remove knitr hooks when package is detached from search path 9 | .onDetach <- function(libpath) { 10 | remove_knitr_hooks() 11 | } 12 | 13 | .onLoad <- function(libname, pkgname) { 14 | register_default_event_handlers() 15 | 16 | # We need an input handler for learnr.exercise, only so that we can call 17 | # shinytest::registerInputProcessor(), below. 18 | removeInputHandler("learnr.exercise") 19 | registerInputHandler("learnr.exercise", function(x, shinysession, name) { 20 | snapshotPreprocessInput(name, snapshotPreprocessorLearnrExercise) 21 | x 22 | }) 23 | 24 | 25 | if ("shinytest2" %in% loadedNamespaces()) { 26 | register_shinytest_inputprocessor() 27 | } 28 | setHook( 29 | packageEvent("shinytest2", "onLoad"), 30 | function(...) register_shinytest_inputprocessor() 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /man/duplicate_env.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{duplicate_env} 4 | \alias{duplicate_env} 5 | \title{Create a duplicate of an environment} 6 | \usage{ 7 | duplicate_env(envir, parent = parent.env(envir)) 8 | } 9 | \arguments{ 10 | \item{envir}{environment to duplicate} 11 | 12 | \item{parent}{parent environment to set for the new environment. Defaults to 13 | the parent environment of \code{envir}.} 14 | } 15 | \value{ 16 | A duplicated copy of \code{envir} whose parent env is \code{parent}. 17 | } 18 | \description{ 19 | Copy all items from the environment to a new environment. By default, the new 20 | environment will share the same parent environment. 21 | } 22 | \examples{ 23 | # Make a new environment with the object 'key' 24 | envir <- new.env() 25 | envir$key <- "value" 26 | "key" \%in\% ls() # FALSE 27 | "key" \%in\% ls(envir = envir) # TRUE 28 | 29 | # Duplicate the envir and show it contains 'key' 30 | new_envir <- duplicate_env(envir) 31 | "key" \%in\% ls(envir = new_envir) # TRUE 32 | 33 | } 34 | -------------------------------------------------------------------------------- /R/data_dir.R: -------------------------------------------------------------------------------- 1 | copy_data_dir <- function(source_dir, exercise_dir) { 2 | if (is.null(source_dir)) { 3 | # First check options(), then environment variable, then default to "data/" 4 | source_dir <- getOption( 5 | "tutorial.data_dir", 6 | Sys.getenv("TUTORIAL_DATA_DIR", if (dir.exists("data")) "data" else "") 7 | ) 8 | } 9 | 10 | if (identical(source_dir, "")) { 11 | return(invisible(NULL)) 12 | } 13 | 14 | if (!dir.exists(source_dir)) { 15 | rlang::abort( 16 | "An error occurred with the tutorial: the data directory does not exist.", 17 | class = "learnr_missing_source_data_dir" 18 | ) 19 | } 20 | 21 | dest_dir <- file.path(exercise_dir, "data") 22 | dir.create(dest_dir) 23 | 24 | if (!dir.exists(dest_dir)) { 25 | rlang::abort( 26 | "An error occurred with the tutorial: we weren't able to create the data directory for this exercise.", 27 | class = "learnr_missing_dest_data_dir" 28 | ) 29 | } 30 | 31 | file.copy(dir(source_dir, full.names = TRUE), dest_dir, recursive = TRUE) 32 | 33 | return(invisible(dest_dir)) 34 | } 35 | -------------------------------------------------------------------------------- /inst/examples/apparmor/apparmor_evaluator.R: -------------------------------------------------------------------------------- 1 | 2 | # Note: To use the "r-user" AppArmor profile you should add the following line 3 | # to /etc/apparmor.d/rapparmor.d/r-user: 4 | # 5 | # /usr/lib/rstudio/bin/pandoc/* rix, 6 | # 7 | 8 | options(tutorial.exercise.evaluator.onstart = function(pid) { 9 | 10 | # import RAppArmor 11 | require(RAppArmor, quietly = TRUE) 12 | 13 | # set process group to pid (allows kill of entire subtree in cleanup) 14 | setpgid(); 15 | 16 | # set nice priority 17 | setpriority(10) 18 | 19 | # set rlimits as appropriate 20 | rlimit_nproc(1000) 21 | rlimit_as(1024*1024*1024) 22 | 23 | # change to r-user profile (see note above on required edit to r-user) 24 | aa_change_profile("r-user") 25 | }) 26 | 27 | options(tutorial.exercise.evaluator.oncleanup = function(pid) { 28 | 29 | # import RAppArmor 30 | require(RAppArmor, quietly = TRUE) 31 | 32 | # kill entire process subtree. note that the second call works 33 | # because the call to setpgid above sets our pgid (process group id) 34 | # to our pid (process id) 35 | kill(pid, tools::SIGKILL) 36 | kill(-1 * pid, tools::SIGKILL) 37 | }) 38 | 39 | -------------------------------------------------------------------------------- /tests/manual/hint-popover.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hint and Solution Popovers" 3 | output: learnr::tutorial 4 | runtime: shiny_prerendered 5 | --- 6 | 7 | ## Test Cases 8 | 9 | ### Hint Only 10 | 11 | The hint in this exercise should have three lines. 12 | 13 | ```{r ex1, exercise = TRUE} 14 | 1+1 15 | ``` 16 | 17 | ```{r ex1-hint} 18 | line1 19 | line2 20 | line3 21 | ``` 22 | 23 | ### Solution Only 24 | 25 | The hint in this exercise should have six lines. 26 | 27 | ```{r ex2, exercise = TRUE} 28 | 1+1 29 | ``` 30 | 31 | ```{r ex2-solution} 32 | line1 33 | line2 34 | line3 35 | line4 36 | line5 37 | line6 38 | ``` 39 | 40 | ### Solution and Hints 41 | 42 | 1. Hint with 5 lines 43 | 2. Hint with 15 lines 44 | 3. Solution with 3 lines 45 | 46 | ```{r ex3, exercise = TRUE} 47 | 1+1 48 | ``` 49 | 50 | ```{r ex3-hint-1} 51 | line1 52 | line2 53 | line3 54 | line4 55 | line5 56 | ``` 57 | 58 | ```{r ex3-hint-2} 59 | line1 60 | line2 61 | line3 62 | line4 63 | line5 64 | line6 65 | line7 66 | line8 67 | line9 68 | line10 69 | line11 70 | line12 71 | line13 72 | line14 73 | line15 74 | ``` 75 | 76 | ```{r ex3-solution} 77 | line1 78 | line2 79 | line3 80 | ``` 81 | -------------------------------------------------------------------------------- /man/rmd-fragments/badges.Rmd: -------------------------------------------------------------------------------- 1 | 2 | [![R build status](https://github.com/rstudio/learnr/workflows/R-CMD-check/badge.svg)](https://github.com/rstudio/learnr) 3 | [![CRAN status](https://www.r-pkg.org/badges/version/learnr)](https://CRAN.R-project.org/package=learnr) 4 | [![learnr downloads per month](http://cranlogs.r-pkg.org/badges/learnr)](http://www.rpackages.io/package/learnr) 5 | [![DOI](https://zenodo.org/badge/71377580.svg)](https://zenodo.org/badge/latestdoi/71377580) 6 |
7 | [![GitHub Discussions](https://img.shields.io/github/discussions/rstudio/learnr?logo=github&style=social)](https://github.com/rstudio/learnr/discussions) 8 | [![RStudio community](https://img.shields.io/badge/community-teaching-blue?style=social&logo=rstudio&logoColor=75AADB)]( https://community.rstudio.com/c/teaching/13) 9 | [![RStudio community](https://img.shields.io/badge/community-learnr-blue?style=social&logo=rstudio&logoColor=75AADB)](https://community.rstudio.com/new-topic?title=&category_id=13&tags=learnr&body=%0A%0A%0A%20%20--------%0A%20%20%0A%20%20%3Csup%3EReferred%20here%20by%20%60learnr%60%27s%20GitHub%3C/sup%3E%0A&u=barret) 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learnr", 3 | "private": true, 4 | "engines": { 5 | "node": ">= 14" 6 | }, 7 | "scripts": { 8 | "build": "npm run lint && npm run build:esbuild", 9 | "build:esbuild": "node learnr-js/build.js", 10 | "lint": "standard --fix learnr-js", 11 | "copy": "npm run copy:clipboard && npm run copy:bootbox && npm run copy:i18next", 12 | "copy:clipboard": "cpy 'clipboard.min.js' ../../../inst/lib/clipboardjs --cwd node_modules/clipboard/dist/", 13 | "copy:bootbox": "cpy 'bootbox.min.js' ../../../inst/lib/bootbox --cwd node_modules/bootbox/dist/", 14 | "copy:i18next": "cpy 'i18next.min.js' ../../inst/lib/i18next --cwd node_modules/i18next/" 15 | }, 16 | "devDependencies": { 17 | "@babel/cli": "^7.16.8", 18 | "@babel/core": "^7.16.12", 19 | "@babel/preset-env": "^7.16.11", 20 | "bootbox": "^5.5.2", 21 | "browserslist": "^4.19.1", 22 | "clipboard": "^2.0.10", 23 | "core-js": "^3.21.0", 24 | "cpy-cli": "^4.1.0", 25 | "esbuild": "^0.14.18", 26 | "esbuild-plugin-babel": "https://github.com/schloerke/esbuild-plugin-babel#patch-2", 27 | "i18next": "^21.6.10", 28 | "standard": "^16.0.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /R/learnr-package.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | "_PACKAGE" 3 | 4 | ## usethis namespace: start 5 | #' @importFrom evaluate evaluate 6 | #' @importFrom htmltools attachDependencies 7 | #' @importFrom htmltools div 8 | #' @importFrom htmltools HTML 9 | #' @importFrom htmltools htmlDependency 10 | #' @importFrom htmltools tags 11 | #' @importFrom htmlwidgets createWidget 12 | #' @importFrom jsonlite base64_dec 13 | #' @importFrom jsonlite base64_enc 14 | #' @importFrom knitr all_labels 15 | #' @importFrom knitr knit_hooks 16 | #' @importFrom knitr knit_meta_add 17 | #' @importFrom knitr opts_chunk 18 | #' @importFrom knitr opts_hooks 19 | #' @importFrom knitr opts_knit 20 | #' @importFrom knitr spin 21 | #' @importFrom lifecycle deprecated 22 | #' @importFrom rprojroot find_root 23 | #' @importFrom rprojroot is_r_package 24 | #' @importFrom shiny invalidateLater 25 | #' @importFrom shiny isolate 26 | #' @importFrom shiny observe 27 | #' @importFrom shiny observeEvent 28 | #' @importFrom shiny reactive 29 | #' @importFrom shiny reactiveValues 30 | #' @importFrom shiny req 31 | #' @importFrom stats runif 32 | #' @importFrom withr with_envvar 33 | ## usethis namespace: end 34 | NULL 35 | 36 | # @staticimports pkg:staticimports 37 | # compact 38 | # imap_lgl 39 | -------------------------------------------------------------------------------- /tests/testthat/test-question_radio.R: -------------------------------------------------------------------------------- 1 | test_that("question_radio() throws an error when using only answer_fn() answers", { 2 | expect_error( 3 | question_radio( 4 | "test", 5 | answer_fn(~ "one"), 6 | answer_fn(~ "two") 7 | ) 8 | ) 9 | }) 10 | 11 | test_that("question_radio() throws an error if it doesn't include a correct answer, even with answer_fn()", { 12 | expect_error( 13 | question_radio( 14 | "test", 15 | answer_fn(~ "one"), 16 | answer("two", correct = FALSE) 17 | ) 18 | ) 19 | 20 | expect_error( 21 | question_radio( 22 | "test", 23 | answer("two", correct = FALSE) 24 | ) 25 | ) 26 | }) 27 | 28 | test_that("question_radio() warns when using answer_fn() answers", { 29 | expect_warning( 30 | q_fn <- question_radio( 31 | "test", 32 | answer_fn(~ "one"), 33 | answer("two", correct = TRUE) 34 | ) 35 | ) 36 | 37 | q_no_fn <- question_radio( 38 | "test", 39 | answer("two", correct = TRUE) 40 | ) 41 | 42 | clean_question <- function(q) { 43 | # strip random bits 44 | q$ids <- NULL 45 | q$seed <- 42L 46 | q$answers[[1]]$id <- "answer-id" 47 | q 48 | } 49 | 50 | expect_equal(clean_question(q_fn), clean_question(q_no_fn)) 51 | }) 52 | -------------------------------------------------------------------------------- /pkgdown/assets/snippets/snippets.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function loadSnippet(snippet, mode) { 4 | mode = mode || "r"; 5 | $("#" + snippet).addClass("snippet").removeClass('sourceCode'); 6 | const editor = ace.edit(snippet); 7 | editor.setHighlightActiveLine(false); 8 | editor.setShowPrintMargin(false); 9 | editor.setReadOnly(true); 10 | editor.setShowFoldWidgets(false); 11 | editor.renderer.setDisplayIndentGuides(false); 12 | editor.renderer.setShowGutter(true); 13 | editor.renderer.setOption('showLineNumbers', true); 14 | editor.setTheme("ace/theme/textmate"); 15 | editor.$blockScrolling = Infinity; 16 | editor.session.setMode("ace/mode/" + mode); 17 | editor.session.getSelection().clearSelection(); 18 | 19 | const root = document.querySelector('meta[name="pkgdown-site-root"]').content; 20 | $.get(root + "snippets/" + snippet + ".md", function(data) { 21 | // Write the snippet into the editor 22 | editor.setValue(data, -1); 23 | editor.setOptions({ 24 | maxLines: editor.session.getLength() 25 | }); 26 | 27 | // and write the snippet into an element for screen readers 28 | const pre = $('
')
29 |       .append($('').text(data));
30 |     $(editor.container).prepend(pre);
31 |   });
32 | }
33 | 


--------------------------------------------------------------------------------
/sandbox/autocompletion.Rmd:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: "Autocompletion"
 3 | output: learnr::tutorial
 4 | runtime: shiny_prerendered
 5 | ---
 6 | 
 7 | ```{r setup, include=FALSE}
 8 | library(learnr)
 9 | library(nycflights13)
10 | options(tutorial.event_recorder = learnr:::debug_event_recorder)
11 | tutorial_options(
12 |   exercise.eval = FALSE, 
13 |   exercise.checker = function(label, user_code, envir_result, ...)  {
14 |     if (is.null(envir_result))
15 |       list(message = "Bad code!",
16 |            correct = FALSE)
17 |     else
18 |       list(message = "Nice job!", 
19 |            correct = TRUE, 
20 |            location = "append")
21 |   },
22 |   exercise.completion = TRUE,
23 |   exercise.diagnostics = TRUE
24 | )
25 | ```
26 | 
27 | ## Autocompletion Sandbox
28 | 
29 | ```{r autocompletion-setup}
30 | autocompletion <- list(apple = 1, banana = 2, cherry = 3)
31 | ```
32 | 
33 | ```{r autocompletion, exercise=TRUE, exercise.completion = TRUE, exercise.setup="autocompletion-setup"}
34 | # Try typing here
35 | ```
36 | 
37 | ```{r autocompletion2, exercise=TRUE}
38 | # Try typing here
39 | ```
40 | 
41 | ```{r no-diagnostics, exercise=TRUE, exercise.diagnostics=FALSE}
42 | # no diagnostics here
43 | ```
44 | 
45 | ```{r diagnostics, exercise=TRUE}
46 | # diagnostics here
47 | ```
48 | 
49 | 


--------------------------------------------------------------------------------
/tests/testthat/test-praise.R:
--------------------------------------------------------------------------------
 1 | test_that("random_phrases()", {
 2 |   expect_error(random_phrases("foo"), "should be one of")
 3 |   expect_warning(
 4 |     expect_equal(random_phrases("praise", "foo"), random_phrases("praise", "en"))
 5 |   )
 6 | 
 7 |   knitr::opts_knit$set("tutorial.language" = "en")
 8 |   expect_equal(random_phrases("praise"), random_phrases("praise", "en"))
 9 |   expect_equal(random_phrases("encouragement"), random_phrases("encouragement", "en"))
10 |   knitr::opts_knit$set("tutorial.language" = NULL)
11 | 
12 |   expect_equal(random_phrases("praise", "testing"), "RANDOM PRAISE.")
13 |   expect_equal(random_phrases("encouragement", "testing"), "RANDOM ENCOURAGEMENT.")
14 | })
15 | 
16 | test_that("random_phrases_add()", {
17 |   random_phrases_add(
18 |     language = "bogus",
19 |     praise = "Praise here!",
20 |     encouragement = c("Go 1", "Go 2")
21 |   )
22 | 
23 |   expect_equal(random_phrases("praise", "bogus"), "Praise here!")
24 |   expect_equal(random_phrases("encouragement", "bogus"), c("Go 1", "Go 2"))
25 | 
26 |   random_phrases_add("bogus", encouragement = "Go 3")
27 |   expect_equal(random_phrases("encouragement", "bogus"), c("Go 1", "Go 2", "Go 3"))
28 | 
29 |   expect_error(random_phrases_add("bogus", list("bad")))
30 |   expect_error(random_phrases_add("bogus", 1:4))
31 | })
32 | 


--------------------------------------------------------------------------------
/tests/testthat/tutorials/hint-copy/hint-copy.Rmd:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: "Hint and Solution Copy and Paste"
 3 | output: learnr::tutorial
 4 | runtime: shiny_prerendered
 5 | ---
 6 | 
 7 | ```{r setup, include=FALSE}
 8 | knitr::opts_chunk$set(comment = "")
 9 | report_code_event <- function(tutorial_id, tutorial_version, user_id, event, data) {
10 |   if (is.null(data$code)) return()
11 |   rlang::inform(sprintf("---- %s ----\n%s", event, capture.output(dput(data$code))))
12 | }
13 | options(tutorial.event_recorder = report_code_event)
14 | ```
15 | 
16 | ## Test Cases
17 | 
18 | 
19 | 
20 | ### Hint
21 | 
22 | Reveal the hint, copy and paste into the exercise, and run.
23 | 
24 | ```{r ex1, exercise = TRUE}
25 | 
26 | ```
27 | 
28 | ```{r ex1-hint}
29 | c(
30 |   1,
31 |   2,
32 |   3
33 | )
34 | ```
35 | 
36 | Expected output:
37 | 
38 | ::: {#ex1-expected-output}
39 | ```{r ref.label="ex1-hint", echo=FALSE}
40 | ```
41 | :::
42 | 
43 | ### Solution
44 | 
45 | Reveal the solution, copy and paste into the exercise, and run.
46 | 
47 | ```{r ex2, exercise = TRUE}
48 | 
49 | ```
50 | 
51 | ```{r ex2-solution}
52 | c(
53 |   "apple",
54 |   "banana",
55 |   "coconut"
56 | )
57 | ```
58 | 
59 | Expected output:
60 | 
61 | ::: {#ex2-expected-output}
62 | ```{r ref.label="ex2-solution", echo=FALSE}
63 | ```
64 | :::
65 | 


--------------------------------------------------------------------------------
/tests/testthat/test-mark_as.R:
--------------------------------------------------------------------------------
 1 | context("mark as")
 2 | 
 3 | test_that("correct", {
 4 |   ans <- correct()
 5 |   expect_s3_class(ans, "learnr_mark_as")
 6 |   expect_equal(ans$correct, TRUE)
 7 |   expect_equal(ans$messages, NULL)
 8 | })
 9 | 
10 | test_that("correct passes messages", {
11 |   msg <- quiz_text("**msg**")
12 |   ans <- correct(msg)
13 |   expect_s3_class(ans, "learnr_mark_as")
14 |   expect_equal(ans$correct, TRUE)
15 |   expect_equal(ans$messages, msg)
16 | 
17 |   msgs <- htmltools::tagList(
18 |     quiz_text("**msg 1**"),
19 |     quiz_text("_msg 2_")
20 |   )
21 |   ans <- correct(msgs)
22 |   expect_s3_class(ans, "learnr_mark_as")
23 |   expect_equal(ans$correct, TRUE)
24 |   expect_equal(ans$messages, msgs)
25 | })
26 | 
27 | 
28 | test_that("incorrect", {
29 |   ans <- incorrect()
30 |   expect_s3_class(ans, "learnr_mark_as")
31 |   expect_equal(ans$correct, FALSE)
32 |   expect_equal(ans$messages, NULL)
33 | })
34 | 
35 | test_that("incorrect passes messages", {
36 |   msg <- quiz_text("**msg**")
37 |   ans <- incorrect(msg)
38 |   expect_s3_class(ans, "learnr_mark_as")
39 |   expect_equal(ans$correct, FALSE)
40 |   expect_equal(ans$messages, msg)
41 | 
42 |   msgs <- htmltools::tagList(
43 |     quiz_text("**msg 1**"),
44 |     quiz_text("_msg 2_")
45 |   )
46 |   ans <- incorrect(msgs)
47 |   expect_s3_class(ans, "learnr_mark_as")
48 |   expect_equal(ans$correct, FALSE)
49 |   expect_equal(ans$messages, msgs)
50 | })
51 | 


--------------------------------------------------------------------------------
/man/event_register_handler.Rd:
--------------------------------------------------------------------------------
 1 | % Generated by roxygen2: do not edit by hand
 2 | % Please edit documentation in R/events.R
 3 | \name{event_register_handler}
 4 | \alias{event_register_handler}
 5 | \title{Register an event handler callback}
 6 | \usage{
 7 | event_register_handler(event, callback)
 8 | }
 9 | \arguments{
10 | \item{event}{The name of an event.}
11 | 
12 | \item{callback}{A function to be invoked when an event with a specified name
13 | occurs. The callback must take parameters \code{session}, \code{event}, and \code{data}.}
14 | }
15 | \value{
16 | A function which, if invoked, will remove the callback.
17 | }
18 | \description{
19 | Register an event handler on a per-tutorial basis. Handlers for an event will
20 | be fired in the order that they were registered.
21 | }
22 | \details{
23 | In most cases, this will be called within a learnr document. If that is the
24 | case, then the handler will exist as long as the document (that is, the Shiny
25 | application) is running.
26 | 
27 | If this function is called in a learnr .Rmd document, it should be in a chunk
28 | with \code{context="server-start"}. If it is called with \code{context="server"}, the
29 | handler will be registered at least two times (once for the application as a
30 | whole, and once per user session).
31 | 
32 | If this function is called outside of a learnr document, then the handler
33 | will persist until the learnr package is unloaded, typically when the R
34 | session is stopped.
35 | }
36 | 


--------------------------------------------------------------------------------
/inst/tutorials/polyglot/polyglot.Rmd:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: "Multi-language exercises"
 3 | author: "Cass (Z.) Wilkinson Saldaña"
 4 | output: learnr::tutorial
 5 | runtime: shiny_prerendered
 6 | ---
 7 | 
 8 | ```{r setup, include = FALSE}
 9 | library(learnr)
10 | 
11 | # altered from https://datacarpentry.org/R-ecology-lesson/05-r-and-databases.html
12 | if (!file.exists("portal_mammals.sqlite")) {
13 |   download.file(url = "https://ndownloader.figshare.com/files/2292171",
14 |                 destfile = "portal_mammals.sqlite", mode = "wb")
15 | }
16 | mammals <- DBI::dbConnect(RSQLite::SQLite(), "portal_mammals.sqlite")
17 | ```
18 | 
19 | ### Python or R?
20 | 
21 | The following two code exercises run language agnostic code.  It will finally evaluate to either 5 or 10.
22 | 
23 | ```{r r-not-python, exercise=TRUE, exercise.lines = 5}
24 | # if running in R, this should return 10
25 | # if running in Python, this should return FALSE and 5
26 | x = 5
27 | x <-10
28 | print(x)
29 | ```
30 | 
31 | ```{python python-not-r, exercise=TRUE, exercise.lines = 5}
32 | # if running in R, this should return 10
33 | # if running in Python, this should return FALSE and 5
34 | x = 5
35 | x <-10
36 | print(x)
37 | ```
38 | 
39 | ### Bash
40 | 
41 | Execute exercises in bash
42 | 
43 | ```{bash bash-not-r, exercise = TRUE}
44 | echo "hello world"
45 | date
46 | ```
47 | 
48 | ### SQL
49 | 
50 | Use a SQL connection defined in a setup chunk.
51 | 
52 | ```{sql sql-not-r, exercise = TRUE, connection="mammals"}
53 | SELECT *
54 | FROM `surveys`
55 | LIMIT 10
56 | ```
57 | 


--------------------------------------------------------------------------------
/tests/manual/ace-mode/ace-mode.Rmd:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: "Ace Language Mode"
 3 | output: learnr::tutorial
 4 | runtime: shiny_prerendered
 5 | ---
 6 | 
 7 | ```{r setup, include=FALSE}
 8 | library(learnr)
 9 | ```
10 | 
11 | ## Test Cases
12 | 
13 | These exercises don't have checks.
14 | 
15 | ### R
16 | 
17 | An exercise using R.
18 | 
19 | ```{r ex1, exercise = TRUE}
20 | stem(log(rivers))
21 | ```
22 | 
23 | ```{r ex1-hint}
24 | stem(rivers)
25 | ```
26 | 
27 | ### Python
28 | 
29 | ```{python ex-python, exercise = TRUE}
30 | [x for x in [3, 4, 5, 6, 7] if x > 5]
31 | ```
32 | 
33 | ```{python ex-python-hint}
34 | [x for x in [3, 4, 5, 6, 7] if x > 2]
35 | ```
36 | 
37 | ### JavaScript
38 | 
39 | *Note: syntax highlighting works but no output will be shown unless you run the code in an IIFE and use `console.log()` or `window.alert()`.*
40 | 
41 | ```{js ex-js, exercise = TRUE, results = "asis"}
42 | (function() {
43 |   const value = Math.min.apply(Math, [42, 6, 27])
44 |   window.alert(`The value is ${value}`)
45 | })()
46 | ```
47 | 
48 | ```{js ex-js-hint}
49 | console.log(Math.min(42, 6, 27))
50 | ```
51 | 
52 | ### Julia
53 | 
54 | ```{julia ex-julia, exercise = TRUE}
55 | # While loops loop while a condition is true
56 | let x = 0
57 |     while x < 4
58 |         println(x)
59 |         x += 1  # Shorthand for in place increment: x = x + 1
60 |     end
61 | end
62 | ```
63 | 
64 | ```{julia ex-julia-hint}
65 | for pair in Dict("dog" => "mammal", "cat" => "mammal", "mouse" => "mammal")
66 |     from, to = pair
67 |     println("$from is a $to")
68 | end
69 | ```
70 | 


--------------------------------------------------------------------------------
/tests/testthat/test-mock_exercise.R:
--------------------------------------------------------------------------------
 1 | test_that("exercise mocks: mock_prep_setup()", {
 2 |   chunks <- list(
 3 |     mock_chunk("setup-1", "x <- 1"),
 4 |     mock_chunk("setup-2", "y <- 2", exercise.setup = "setup-1"),
 5 |     mock_chunk("setup-3", "z <- 3", exercise.setup = "setup-2")
 6 |   )
 7 |   expect_equal(mock_prep_setup(chunks, "setup-3"), "x <- 1\ny <- 2\nz <- 3")
 8 |   expect_equal(mock_prep_setup(chunks, "setup-2"), "x <- 1\ny <- 2")
 9 |   expect_equal(mock_prep_setup(chunks, "setup-1"), "x <- 1")
10 | 
11 |   # random order
12 |   expect_equal(mock_prep_setup(chunks[3:1], "setup-3"), "x <- 1\ny <- 2\nz <- 3")
13 |   expect_equal(mock_prep_setup(chunks[c(1, 3, 2)], "setup-3"), "x <- 1\ny <- 2\nz <- 3")
14 |   expect_equal(mock_prep_setup(chunks[c(2, 3, 1)], "setup-3"), "x <- 1\ny <- 2\nz <- 3")
15 |   expect_equal(mock_prep_setup(chunks[c(2, 1, 3)], "setup-3"), "x <- 1\ny <- 2\nz <- 3")
16 | 
17 |   # checks that setup chunk is in chunks
18 |   expect_error(mock_prep_setup(chunks, "setup-Z"), "setup-Z")
19 | 
20 |   # cycles
21 |   chunks[[1]]$opts$exercise.setup = "setup-3"
22 |   expect_error(mock_prep_setup(chunks, "setup-3"), "-> setup-3$")
23 | 
24 |   # duplicate labels
25 |   expect_error(mock_prep_setup(chunks[c(1, 1)], "setup-1"), "Duplicated")
26 | })
27 | 
28 | test_that("mock_exercise() errors for duplicated chunks", {
29 |   expect_error(
30 |     mock_exercise(
31 |       chunks = list(
32 |         mock_chunk("ex1", "1 + 1", exercise = TRUE),
33 |         mock_chunk("ex1", "2 + 2", exercise = TRUE)
34 |       ),
35 |       label = "ex"
36 |     )
37 |   )
38 | })
39 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name  : Bug report
 3 | about : Report a bug in learnr.
 4 | ---
 5 | 
 6 | 
17 | 
18 | ### System details
19 | 
20 | Output of `sessioninfo::session_info()()`:
21 | 
22 | ```
23 | # sessioninfo::session_info() output goes here
24 | ```
25 | 
26 | ### Example application *or* steps to reproduce the problem
27 | 
28 | 
29 | 
30 | 
31 | ```r
32 | library(learnr)
33 | # Minimal, self-contained example code or markdown file goes here
34 | ```
35 | 
36 | ### Describe the problem in detail
37 | 


--------------------------------------------------------------------------------
/.github/pkgdown-pr-preview-build.R:
--------------------------------------------------------------------------------
 1 | # ---- pkgdown::deploy_site_github() ----
 2 | #
 3 | # Follows the steps of deploy_site_github() but renders into
 4 | # `preview/pr#` of `gh-pages` branch.
 5 | 
 6 | # Pull gh-pages branch
 7 | callr::run("git", c("remote", "set-branches", "--add", "origin", "gh-pages"), echo_cmd = TRUE)
 8 | callr::run("git", c("fetch", "origin", "gh-pages"), echo_cmd = TRUE)
 9 | 
10 | local({
11 |   # Setup worktree in tempdir
12 |   dest_dir <- fs::dir_create(fs::file_temp())
13 |   on.exit(unlink(dest_dir, recursive = TRUE), add = TRUE)
14 | 
15 |   callr::run("git", c("worktree", "add", "--track", "-B", "gh-pages", dest_dir, "origin/gh-pages"), echo_cmd = TRUE)
16 |   on.exit(add = TRUE, {
17 |     callr::run("git", c("worktree", "remove", dest_dir), echo_cmd = TRUE)
18 |   })
19 | 
20 |   # PR preview is in a preview/pr# subdirectory of gh-pages branch
21 |   dest_preview <- file.path("preview", paste0("pr", Sys.getenv("PR_NUMBER")))
22 |   dest_dir_preview <- fs::dir_create(fs::path(dest_dir, dest_preview))
23 | 
24 |   url_base <- yaml::read_yaml("pkgdown/_pkgdown.yml")$url
25 | 
26 |   # Build the preview site in the /preview/pr#/ directory
27 |   pkgdown:::build_site_github_pages(
28 |     dest_dir = dest_dir_preview,
29 |     override = list(
30 |       url = file.path(url_base, dest_preview)
31 |     ),
32 |     clean = TRUE
33 |   )
34 | 
35 |   msg <- paste("[preview]", pkgdown:::construct_commit_message("."))
36 |   pkgdown:::github_push(dest_dir, msg, "origin", "gh-pages")
37 | 
38 |   message(
39 |     "::notice title=pkgdown preview::",
40 |     file.path(url_base, dest_preview)
41 |   )
42 | })
43 | 
44 | 


--------------------------------------------------------------------------------
/man/debug_exercise_checker.Rd:
--------------------------------------------------------------------------------
 1 | % Generated by roxygen2: do not edit by hand
 2 | % Please edit documentation in R/debug_exercise_checker.R
 3 | \name{debug_exercise_checker}
 4 | \alias{debug_exercise_checker}
 5 | \title{An Exercise Checker for Debugging}
 6 | \usage{
 7 | debug_exercise_checker(
 8 |   label,
 9 |   user_code,
10 |   solution_code,
11 |   check_code,
12 |   envir_result,
13 |   evaluate_result,
14 |   envir_prep,
15 |   last_value,
16 |   engine,
17 |   ...
18 | )
19 | }
20 | \arguments{
21 | \item{label}{Exercise label}
22 | 
23 | \item{user_code}{Submitted user code}
24 | 
25 | \item{solution_code}{The code in the \verb{*-solution} chunk}
26 | 
27 | \item{check_code}{The checking code that originates from the \verb{*-check} chunk,
28 | the \verb{*-code-check} chunk, or the \verb{*-error-check} chunk.}
29 | 
30 | \item{evaluate_result}{The return value from \code{evaluate::evaluate()}, called
31 | on \code{user_code}}
32 | 
33 | \item{envir_prep, envir_result}{The environment before running user code
34 | (\code{envir_prep}) and the environment just after running the user's code
35 | (\code{envir_result}).}
36 | 
37 | \item{last_value}{The last value after evaluating \code{user_code}}
38 | 
39 | \item{engine}{The engine of the exercise chunk}
40 | 
41 | \item{...}{Not used (future compatibility)}
42 | }
43 | \value{
44 | Feedback for use in exercise debugging.
45 | }
46 | \description{
47 | An exercise checker for debugging that renders all of the expected arguments
48 | of the \code{exercise.checker} option into HTML. Additionally, this function is
49 | used in testing  of \code{evaluate_exercise()}.
50 | }
51 | \keyword{internal}
52 | 


--------------------------------------------------------------------------------
/man/safe.Rd:
--------------------------------------------------------------------------------
 1 | % Generated by roxygen2: do not edit by hand
 2 | % Please edit documentation in R/run.R
 3 | \name{safe}
 4 | \alias{safe}
 5 | \title{Execute R code in a safe R environment}
 6 | \usage{
 7 | safe(expr, ..., show = TRUE, env = safe_env())
 8 | }
 9 | \arguments{
10 | \item{expr}{expression that contains all the necessary library calls to
11 | execute.  Expressions within callr do not inherit the existing,
12 | loaded libraries.}
13 | 
14 | \item{...}{parameters passed to \code{callr::\link[callr]{r}}}
15 | 
16 | \item{show}{Logical that determines if output should be displayed}
17 | 
18 | \item{env}{Environment to evaluate the document in}
19 | }
20 | \value{
21 | The result of \code{expr}.
22 | }
23 | \description{
24 | When rendering (or running) a document with R markdown, it inherits the
25 | current R Global environment.  This will produce unexpected behaviors,
26 | such as poisoning the R Global environment with existing variables.  By
27 | rendering the document in a new, safe R environment, a \emph{vanilla},
28 | rendered document is produced.
29 | }
30 | \details{
31 | The environment variable \code{LEARNR_INTERACTIVE} will be set to \code{"1"}
32 | or \code{"0"} depending on if the calling session is interactive or not.
33 | 
34 | Using \code{safe} should only be necessary when locally deployed.
35 | }
36 | \examples{
37 | \dontrun{
38 | # Direct usage
39 | safe(run_tutorial("hello", package = "learnr"))
40 | 
41 | # Programmatic usage
42 | library(rlang)
43 | 
44 | expr <- quote(run_tutorial("hello", package = "learnr"))
45 | safe(!!expr)
46 | 
47 | tutorial <- "hello"
48 | safe(run_tutorial(!!tutorial, package = "learnr"))
49 | }
50 | 
51 | }
52 | 


--------------------------------------------------------------------------------
/vignettes/articles/example_cards.R:
--------------------------------------------------------------------------------
 1 | card <- function(
 2 |   ...,
 3 |   class_card = NULL,
 4 |   class_card_body = NULL,
 5 |   class_card_title = NULL,
 6 |   class_card_footer = "text-end"
 7 | ) {
 8 |   card_data <- list(...)
 9 |   htmltools::withTags(
10 |     div(
11 |       class = "col",
12 |       div(
13 |         class = c("card h-100 shadow-sm", class_card),
14 |         a(
15 |           href = card_data$link,
16 |           img(
17 |             src = card_data$image,
18 |             class = "card-img-top",
19 |             alt = paste0("Preview image of ", card_data$title)
20 |           )
21 |         ),
22 |         div(
23 |           class = c("card-body", class_card_body),
24 |           h5(class = c("card-title", class_card_title), a(href = card_data$link, card_data$title)),
25 |           if (!is.null(card_data$text)) div(
26 |             class = "card-text text-muted fs-6",
27 |             htmltools::HTML(commonmark::markdown_html(card_data$text))
28 |           ),
29 |         ),
30 |         if (!is.null(card_data$footer)) div(
31 |           class = c("card-footer", class_card_footer),
32 |           htmltools::HTML(card_data$footer)
33 |         )
34 |       )
35 |     )
36 |   )
37 | }
38 | 
39 | example_cards <- function(yml, group = NULL, class_row = "row-cols-1 row-cols-md-2 row-cols-lg-3") {
40 |   examples <- if (is.list(yml)) {
41 |     yml
42 |   } else {
43 |     yaml::read_yaml(yml)
44 |   }
45 | 
46 |   examples <- purrr::keep(examples, function(x) x[["group"]] %in% group)
47 |   examples <- purrr::transpose(examples)
48 | 
49 |   htmltools::tags$div(
50 |     class = paste("row g-4", class_row),
51 |     purrr::pmap(examples, card)
52 |   )
53 | }
54 | 


--------------------------------------------------------------------------------
/man/format_quiz.Rd:
--------------------------------------------------------------------------------
 1 | % Generated by roxygen2: do not edit by hand
 2 | % Please edit documentation in R/quiz_print.R
 3 | \name{format.tutorial_question_answer}
 4 | \alias{format.tutorial_question_answer}
 5 | \alias{format.tutorial_question}
 6 | \alias{format.tutorial_quiz}
 7 | \alias{print.tutorial_question}
 8 | \alias{print.tutorial_question_answer}
 9 | \alias{print.tutorial_quiz}
10 | \title{Formatting and printing quizzes, questions, and answers}
11 | \usage{
12 | \method{format}{tutorial_question_answer}(x, ..., spacing = "")
13 | 
14 | \method{format}{tutorial_question}(x, ..., spacing = "")
15 | 
16 | \method{format}{tutorial_quiz}(x, ...)
17 | 
18 | \method{print}{tutorial_question}(x, ...)
19 | 
20 | \method{print}{tutorial_question_answer}(x, ...)
21 | 
22 | \method{print}{tutorial_quiz}(x, ...)
23 | }
24 | \arguments{
25 | \item{x}{object of interest}
26 | 
27 | \item{...}{ignored}
28 | 
29 | \item{spacing}{Text to be placed at the beginning of each new line}
30 | }
31 | \description{
32 | Notes:
33 | \itemize{
34 | \item If custom question types are created, custom s3 formating methods may be implemented as well.
35 | \item Due to the shiny runtime of questions, a text representation of quizzes, questions, and answers will be presented.
36 | }
37 | }
38 | \examples{
39 | ex_question <- question("What number is the letter A in the alphabet?",
40 |   answer("8"),
41 |   answer("14"),
42 |   answer("1", correct = TRUE),
43 |   answer("23"),
44 |   incorrect = "See [here](https://en.wikipedia.org/wiki/English_alphabet) and try again.",
45 |   allow_retry = TRUE
46 | )
47 | cat(format(ex_question), "\n")
48 | }
49 | \seealso{
50 | \code{\link{quiz}}, \code{\link{question}}, \code{\link{answer}}
51 | }
52 | 


--------------------------------------------------------------------------------
/man/rmd-fragments/learnr-examples-showcase.Rmd:
--------------------------------------------------------------------------------
 1 | Here are some simple examples of tutorials created with the **learnr** package:
 2 | 
 3 | ```{r, eval=FALSE, echo=FALSE}
 4 | ## Not included to avoid rcmdcheck warnings about dependency on {quillt}
 5 | # cat(format(quillt::examples("examples.yml", showcaseOnly = TRUE)))
 6 | ```
 7 | 
 8 | 
45 | 


--------------------------------------------------------------------------------
/learnr-js/build.js:
--------------------------------------------------------------------------------
 1 | const { readFileSync } = require('fs')
 2 | const { build } = require('esbuild')
 3 | const babelPlugin = require('esbuild-plugin-babel')
 4 | 
 5 | const excludeVendorFromSourceMapPlugin = ({ filter }) => ({
 6 |   name: 'excludeVendorFromSourceMap',
 7 |   setup (build) {
 8 |     build.onLoad({ filter }, args => {
 9 |       if (args.path.endsWith('.js')) {
10 |         return {
11 |           contents:
12 |             readFileSync(args.path, 'utf8') +
13 |             '\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJtYXBwaW5ncyI6IkEifQ==',
14 |           loader: 'default'
15 |         }
16 |       }
17 |     })
18 |   }
19 | })
20 | 
21 | const excludeNodeModules = excludeVendorFromSourceMapPlugin({
22 |   filter: /node_modules/
23 | })
24 | 
25 | const buildConfig = {
26 |   sourcemap: true,
27 |   bundle: true,
28 |   minify: true,
29 |   allowOverwrite: true,
30 |   logLevel: 'info',
31 |   target: ['es5'],
32 |   plugins: [excludeNodeModules, babelPlugin()]
33 | }
34 | 
35 | const buildTutorial = {
36 |   entryPoints: ['learnr-js/tutorial/tutorial.js'],
37 |   outfile: 'inst/lib/tutorial/tutorial.js',
38 |   ...buildConfig
39 | }
40 | 
41 | const buildTutorialFormat = {
42 |   entryPoints: ['learnr-js/format/tutorial-format.js'],
43 |   outfile: 'inst/rmarkdown/templates/tutorial/resources/tutorial-format.js',
44 |   ...buildConfig
45 | }
46 | 
47 | const buildI18N = {
48 |   entryPoints: ['learnr-js/i18n/tutorial-i18n-init.js'],
49 |   outfile: 'inst/lib/i18n/tutorial-i18n-init.js',
50 |   ...buildConfig
51 | }
52 | 
53 | build(buildTutorial).catch(() => process.exit(1))
54 | build(buildTutorialFormat).catch(() => process.exit(1))
55 | build(buildI18N).catch(() => process.exit(1))
56 | 


--------------------------------------------------------------------------------
/vignettes/articles/examples.Rmd:
--------------------------------------------------------------------------------
 1 | ---
 2 | title: "learnr Tutorial Examples"
 3 | description: >
 4 |   A collection of example interactive tutorials that use the learnr package.
 5 | toc: false
 6 | output:
 7 |   html_document:
 8 |     toc_float: false
 9 | editor_options: 
10 |   chunk_output_type: console
11 | ---
12 | 
13 | ```{r setup, include=FALSE}
14 | knitr::opts_chunk$set(echo = FALSE)
15 | source("example_cards.R", local = TRUE)
16 | ```
17 | 
18 | ```{css echo=FALSE}
19 | .row > main { max-width: 100%; }
20 | ```
21 | 
22 | ## Complete Tutorials
23 | 
24 | These tutorials were designed to showcase the features of the learnr package --- and to teach learners how to use R!
25 | Try working through these tutorials to get a sense of the look and feel of a learnr tutorial.
26 | Then check out the source code to see how these tutorials were made!
27 | 
28 | ```{r example-cards-showcase}
29 | example_cards("examples.yml", "showcase")
30 | ```
31 | 
32 | ## Demo Tutorials
33 | 
34 | There's a lot that learnr can do.
35 | These tutorials showcase specific learnr features or show you some possibilities that build on other packages designed to work with R Markdown and learnr.
36 | 
37 | ```{r example-cards-demo}
38 | example_cards("examples.yml", "demo")
39 | ```
40 | 
41 | ## Community Showcase
42 | 
43 | learnr's community has built some excellent tutorials and resources
44 | for learning data science and programming.
45 | Here are just a few highlights.
46 | (Please [suggest your favorite learnr resources](https://github.com/rstudio/learnr/discussions/categories/show-and-tell).)
47 | 
48 | ```{r example-cards-community}
49 | example_cards("examples.yml", "community")
50 | ```
51 | 
52 | ```{js echo=FALSE}
53 | document.getElementById('main').classList = ''
54 | ```
55 | 


--------------------------------------------------------------------------------
/tests/testthat/helpers.R:
--------------------------------------------------------------------------------
 1 | 
 2 | # https://github.com/rstudio/rmarkdown/blob/2faee0040a39008a47bdf1ba840bf402cba15a65/tests/testthat/helpers.R
 3 | 
 4 | skip_if_not_pandoc <- function(ver = NULL) {
 5 |   if (!rmarkdown::pandoc_available(ver)) {
 6 |     msg <- if (is.null(ver)) {
 7 |       "Pandoc is not available"
 8 |     } else {
 9 |       sprintf("Version of Pandoc is lower than %s.", ver)
10 |     }
11 |     skip(msg)
12 |   }
13 | }
14 | 
15 | skip_if_pandoc <- function(ver = NULL) {
16 |   if (rmarkdown::pandoc_available(ver)) {
17 |     msg <- if (is.null(ver)) {
18 |       "Pandoc is available"
19 |     } else {
20 |       sprintf("Version of Pandoc is greater than %s.", ver)
21 |     }
22 |     skip(msg)
23 |   }
24 | }
25 | 
26 | skip_on_ci_if_not_pr <- function() {
27 |   # Don't skip locally
28 |   if (!nzchar(Sys.getenv("CI", ""))) return()
29 |   # If on CI, don't skip if envvar set by workflow is present
30 |   if (nzchar(Sys.getenv("CI_IN_PR", ""))) return()
31 |   # If on CI and not in a PR branch workflow... skip these tests
32 |   skip("Skipping on CI, tests run in PR checks only")
33 | }
34 | 
35 | skip_if_not_py_available <- function() {
36 |   skip_if_not(reticulate::py_available(initialize = TRUE), "Python not available on this system")
37 | }
38 | 
39 | expect_marked_as <- function(object, correct, messages = NULL) {
40 |   if (is.null(messages)) {
41 |     expect_equal(object, mark_as(correct))
42 |     return()
43 |   }
44 | 
45 |   if (length(messages) > 1) {
46 |     messages_orig <- messages
47 |     messages <- quiz_text(messages_orig[[1]])
48 |     for (i in seq_along(messages_orig)[-1]) {
49 |       messages <- htmltools::tagList(messages, messages_orig[[i]])
50 |     }
51 |   }
52 | 
53 |   expect_equal(object, mark_as(correct, messages))
54 | }
55 | 


--------------------------------------------------------------------------------
/pkgdown/templates/content-article.html:
--------------------------------------------------------------------------------
 1 | {{^as_is}}$for(header-includes)$
 2 | $header-includes$
 3 | $endfor${{/as_is}}
 4 | 
 5 | $if(banner)$$endif$
 6 | 
 7 | 
8 |
9 | 37 | 38 | $for(include-before)$ 39 | $include-before$ 40 | $endfor$ 41 | 42 | $if(abstract)$ 43 |
44 |

Abstract

45 | $abstract$ 46 |
47 | $endif$ 48 | 49 | $body$ 50 |
51 | {{#toc}} 52 | 57 | {{/toc}} 58 | 59 |
60 | 61 | $for(include-after)$ 62 | $include-after$ 63 | $endfor$ 64 | -------------------------------------------------------------------------------- /man/mark_as_correct_incorrect.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/question_answers.R 3 | \name{correct} 4 | \alias{correct} 5 | \alias{incorrect} 6 | \alias{mark_as} 7 | \title{Mark submission as correct or incorrect} 8 | \usage{ 9 | correct(messages = NULL) 10 | 11 | incorrect(messages = NULL) 12 | 13 | mark_as(correct, messages = NULL) 14 | } 15 | \arguments{ 16 | \item{messages}{A vector of messages to be displayed. The type of message 17 | will be determined by the \code{correct} value. Note that markdown messages are 18 | not rendered into HTML, but you may provide HTML using \code{\link[htmltools:HTML]{htmltools::HTML()}} 19 | or \link[htmltools:builder]{htmltools::tags}.} 20 | 21 | \item{correct}{Logical: is the question answer is correct} 22 | } 23 | \value{ 24 | Returns a list with class \code{learnr_mark_as} to be returned from the 25 | \code{\link[=question_is_correct]{question_is_correct()}} method for the learnr question type. 26 | } 27 | \description{ 28 | Helper method to communicate that the user's submission was correct or 29 | incorrect. These functions were originally designed for developers to create 30 | \code{\link[=question_is_correct]{question_is_correct()}} methods for custom question types, but they can also 31 | be called inside the functions created by \code{\link[=answer_fn]{answer_fn()}} to dynamically 32 | determine the result and message provided to the user. 33 | } 34 | \examples{ 35 | # Radio button question implementation of `question_is_correct` 36 | question_is_correct.radio <- function(question, value, ...) { 37 | for (ans in question$answers) { 38 | if (as.character(ans$option) == value) { 39 | return(mark_as(ans$correct, ans$message)) 40 | } 41 | } 42 | mark_as(FALSE, NULL) 43 | } 44 | 45 | } 46 | \seealso{ 47 | \code{\link[=answer_fn]{answer_fn()}} 48 | } 49 | -------------------------------------------------------------------------------- /tools/update-ace.R: -------------------------------------------------------------------------------- 1 | 2 | (function() { 3 | owd <- getwd() 4 | on.exit({setwd(owd)}) 5 | 6 | 7 | ROOT <- rprojroot::find_package_root_file() 8 | ACE_VERSION <- "1.10.1" 9 | ACE_FILES <- c( 10 | "ace.js", 11 | "ext-language_tools.js", 12 | "mode-css.js", 13 | "mode-html.js", 14 | "mode-javascript.js", 15 | "mode-julia.js", 16 | "mode-plain_text.js", 17 | "mode-python.js", 18 | "mode-r.js", 19 | "mode-rdoc.js", 20 | "mode-rhtml.js", 21 | "mode-sql.js", 22 | "mode-text.js", 23 | "mode-xml.js" 24 | ) 25 | ACE_THEME_PREFIX <- "theme-" 26 | 27 | url <- sprintf( 28 | "https://github.com/ajaxorg/ace-builds/archive/v%s.tar.gz", 29 | ACE_VERSION 30 | ) 31 | 32 | destfile <- tempfile("ace-tarball-") 33 | on.exit({unlink(destfile)}, add = TRUE) 34 | download.file(url, destfile = destfile) 35 | 36 | 37 | exdir <- tempfile("ace-") 38 | on.exit({unlink(exdir, recursive = TRUE)}, add = TRUE) 39 | dir.create(exdir) 40 | untar(tarfile = destfile, exdir = exdir) 41 | 42 | setwd(exdir) 43 | setwd(sprintf("ace-builds-%s", ACE_VERSION)) 44 | 45 | 46 | source <- c( 47 | file.path("src-min", ACE_FILES), 48 | dir("src-min", pattern = ACE_THEME_PREFIX, full.names = TRUE) 49 | ) 50 | contents <- paste(lapply(source, function(file) { 51 | readChar(file, file.info(file)$size, TRUE) 52 | }), collapse = "\n") 53 | 54 | target <- file.path(ROOT, "inst/lib/ace/ace.js") 55 | writeLines(contents, con = target, sep = "\n", useBytes = TRUE) 56 | 57 | themes <- sub("^theme-", "", sub("\\.js$", "", dir("src", "^theme-"))) 58 | 59 | metadata <- c( 60 | "# This file was autogenerated by 'tools/update-ace.R'", 61 | paste0("ACE_VERSION <- ", shQuote(ACE_VERSION, "cmd")), 62 | paste0("ACE_THEMES <- c(", paste0(shQuote(themes, "cmd"), collapse = ", "), ")") 63 | ) 64 | 65 | cat("Saving metadata:\n\n", paste0(metadata, collapse = '\n'), "\n") 66 | writeLines(metadata, con = file.path(ROOT, "R/ace.R")) 67 | })() 68 | -------------------------------------------------------------------------------- /tests/testthat/test-feedback.R: -------------------------------------------------------------------------------- 1 | fdbck <- function(message = "a", correct = TRUE, type = NULL, location = NULL) { 2 | feedback(message, correct, type, location) 3 | } 4 | 5 | test_that("feedback_validated() doesn't validate length-0 objects", { 6 | expect_null(feedback_validated(NULL)) 7 | expect_equal(feedback_validated(list()), list()) 8 | }) 9 | 10 | test_that("feedback must be a list with $message and $correct", { 11 | expect_error(feedback_validated("no"), "must be a list") 12 | expect_error(feedback_validated(list(correct = FALSE)), "message") 13 | expect_error(feedback_validated(list(message = "foo")), "correct") 14 | }) 15 | 16 | test_that("feedback message must be character or tag or tagList", { 17 | expect_error(feedback_validated(fdbck(list())), "character") 18 | expect_error(feedback_validated(fdbck(2)), "character") 19 | expect_error(feedback_validated(fdbck(list(a = 1, b = 2))), "character") 20 | 21 | expect_silent(feedback_validated(fdbck("good"))) 22 | expect_silent(feedback_validated(fdbck(htmltools::HTML("good")))) 23 | expect_silent(feedback_validated(fdbck(htmltools::p("good")))) 24 | expect_silent(feedback_validated(fdbck(htmltools::tagList(htmltools::p("good"))))) 25 | }) 26 | 27 | test_that("feedback type must be one of the acceptable values", { 28 | expect_error(feedback_validated(fdbck(type = "--bad--")), "type") 29 | 30 | expect_equal(feedback_validated(fdbck(correct = TRUE))$type, "success") 31 | expect_equal(feedback_validated(fdbck(correct = FALSE))$type, "error") 32 | expect_equal(feedback_validated(fdbck(type = c("info", "error")))$type, "info") 33 | }) 34 | 35 | test_that("feedback location must be one of the acceptable values", { 36 | expect_error(feedback_validated(fdbck(location = "--bad--")), "location") 37 | 38 | expect_equal(feedback_validated(fdbck())$location, "append") 39 | expect_equal(feedback_validated(fdbck(location = c("replace", "prepend")))$location, "replace") 40 | }) 41 | -------------------------------------------------------------------------------- /inst/tutorials/setup-chunks/setup-chunks.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Chained setup chunks" 3 | author: "Nischal Shrestha" 4 | output: 5 | learnr::tutorial: 6 | progressive: true 7 | allow_skip: true 8 | runtime: shiny_prerendered 9 | description: > 10 | This is a demo of chained setup chunks and how it can be used to build up / share code. 11 | --- 12 | 13 | ```{r setup, include = FALSE} 14 | library(learnr) 15 | d <- 3 16 | ``` 17 | 18 | ## Addition 19 | 20 | ### 21 | 22 | We'll start the setup chunk chain with a regular chunk called `setupA`: 23 | 24 | ````{verbatim} 25 | ```{r setupA} 26 | a <- 5 27 | ``` 28 | ```` 29 | 30 | ```{r setupA} 31 | a <- 5 32 | ``` 33 | 34 | ### 35 | 36 | Use `exercise.setup` to chain setup chunks. 37 | Let's start with a simple, single, setup chunk called `setupB` that depends on `setupA`. 38 | 39 | ````{verbatim} 40 | ```{r setupB, exercise.setup = "setupA"} 41 | b <- a + d 42 | ``` 43 | ```` 44 | 45 | ```{r setupB, exercise.setup = "setupA"} 46 | b <- a + d 47 | ``` 48 | 49 | ### 50 | 51 | Then we define an exercise, `ex1`, 52 | that uses `setupB` as its setup chunk, 53 | thereby also using `setupA` for the exercise. 54 | 55 | ````{verbatim} 56 | ```{r ex1, exercise = TRUE, exercise.setup = "setupB"} 57 | x = b + 1 58 | x 59 | ``` 60 | ```` 61 | 62 | ```{r ex1, exercise = TRUE, exercise.setup = "setupB"} 63 | x = b + 1 64 | x 65 | ``` 66 | 67 | **Evaluate the code in the above exercise. The result of `x` should be `9`.** 68 | 69 | ### 70 | 71 | Now let's define another exercise, `ex2`, 72 | that uses the pre-filled code from `ex1` for its setup code, 73 | thereby also depending on `setupB` and `setupA`. 74 | 75 | ````{verbatim} 76 | ```{r ex2, exercise = TRUE, exercise.setup = "ex1"} 77 | y <- x + 1 78 | y 79 | ``` 80 | ```` 81 | 82 | ```{r ex2, exercise = TRUE, exercise.setup = "ex1"} 83 | y <- x + 1 84 | y 85 | ``` 86 | 87 | **Evaluate the code in the above exercise. The result of `y` should be `10`.** 88 | -------------------------------------------------------------------------------- /man/learnr-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/learnr-package.R 3 | \docType{package} 4 | \name{learnr-package} 5 | \alias{learnr} 6 | \alias{learnr-package} 7 | \title{learnr: Interactive Tutorials for R} 8 | \description{ 9 | \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} 10 | 11 | Create interactive tutorials using R Markdown. Use a combination of narrative, figures, videos, exercises, and quizzes to create self-paced tutorials for learning about R and R packages. 12 | } 13 | \seealso{ 14 | Useful links: 15 | \itemize{ 16 | \item \url{https://rstudio.github.io/learnr/} 17 | \item \url{https://github.com/rstudio/learnr} 18 | \item Report bugs at \url{https://github.com/rstudio/learnr/issues} 19 | } 20 | 21 | } 22 | \author{ 23 | \strong{Maintainer}: Garrick Aden-Buie \email{garrick@posit.co} (\href{https://orcid.org/0000-0002-7111-0077}{ORCID}) 24 | 25 | Authors: 26 | \itemize{ 27 | \item Barret Schloerke \email{barret@posit.co} (\href{https://orcid.org/0000-0001-9986-114X}{ORCID}) 28 | \item JJ Allaire \email{jj@posit.co} [conceptor] 29 | \item Alexander Rossell Hayes \email{alex.rossellhayes@posit.co} (\href{https://orcid.org/0000-0001-9412-0457}{ORCID}) 30 | } 31 | 32 | Other contributors: 33 | \itemize{ 34 | \item Nischal Shrestha \email{nischal@posit.co} (\href{https://orcid.org/0000-0003-3321-1712}{ORCID}) [contributor] 35 | \item Angela Li \email{angelali921@gmail.com} (vignette) [contributor] 36 | \item Posit, PBC [copyright holder, funder] 37 | \item Ajax.org B.V. (Ace library) [contributor, copyright holder] 38 | \item Zeno Rocha (clipboard.js library) [contributor, copyright holder] 39 | \item Nick Payne (Bootbox library) [contributor, copyright holder] 40 | \item Jake Archibald (idb-keyval library) [contributor, copyright holder] 41 | \item i18next authors (i18next library) [contributor, copyright holder] 42 | } 43 | 44 | } 45 | \keyword{internal} 46 | -------------------------------------------------------------------------------- /tools/deploy_tutorials.R: -------------------------------------------------------------------------------- 1 | 2 | if (!requireNamespace("remotes")) install.packages("remotes") 3 | 4 | # install rsconnect 5 | remotes::install_cran("rsconnect") 6 | 7 | # install the latest from github 8 | # must install for packrat to work as expected 9 | remotes::install_github("rstudio/learnr", upgrade = "always", force = TRUE) 10 | 11 | # install missing tutorial deps 12 | remotes::install_cran("renv") 13 | remotes::install_cran( 14 | setdiff( 15 | unique(renv::dependencies("inst/tutorials/")$Package), 16 | unname(installed.packages()[,"Package"]) 17 | ) 18 | ) 19 | 20 | server <- "shinyapps.io" 21 | account <- "learnr-examples" 22 | 23 | deploy_app <- function( 24 | app_dir, 25 | name = basename(app_dir), 26 | ... 27 | ) { 28 | cat("\n\n\n") 29 | message("Deploying: ", name) 30 | cat("\n") 31 | rsconnect::deployApp( 32 | appDir = app_dir, 33 | appName = name, 34 | server = server, 35 | account = account, 36 | forceUpdate = TRUE, 37 | ... 38 | ) 39 | } 40 | 41 | deploy_tutorial <- function( 42 | app_dir, 43 | doc = dir(app_dir, pattern = "\\.Rmd$")[1], 44 | name = basename(app_dir) 45 | ) { 46 | deploy_app( 47 | app_dir = app_dir, 48 | name = name, 49 | appPrimaryDoc = doc 50 | ) 51 | } 52 | 53 | 54 | deploy_folder <- function(path, fn) { 55 | lapply( 56 | dir(path, full.names = TRUE), 57 | function(path) { 58 | if (dir.exists(path)) { 59 | fn(path) 60 | } 61 | } 62 | ) 63 | } 64 | 65 | deploy_vignettes <- function() { 66 | lapply( 67 | dir("vignettes", pattern = ".Rmd", full.names = TRUE), 68 | function(rmd) { 69 | rsconnect::deployDoc( 70 | doc = rmd, 71 | appName = sub(".Rmd", "", basename(rmd)), 72 | server = server, 73 | account = account, 74 | forceUpdate = TRUE 75 | ) 76 | } 77 | ) 78 | } 79 | 80 | deploy_vignettes() 81 | deploy_folder("inst/tutorials", deploy_tutorial) 82 | 83 | message("done") 84 | -------------------------------------------------------------------------------- /tests/testthat/test-install-dependencies.R: -------------------------------------------------------------------------------- 1 | context("install tutorial dependencies") 2 | 3 | create_test_tutorial <- function(code) { 4 | tutorial_dir <- file.path(tempdir(), "tutorial-deps") 5 | dir.create(tutorial_dir) 6 | tutorial_path <- tempfile("tutorial-deps", 7 | tmpdir = tutorial_dir, 8 | fileext = ".R") 9 | writeLines(code, con = tutorial_path) 10 | invisible(tutorial_dir) 11 | } 12 | 13 | test_that("get_needed_pkgs returns appropriate packages", { 14 | tutorial_dir <- create_test_tutorial("library(pkg1)\npkg2::n()") 15 | on.exit(unlink(tutorial_dir, recursive = TRUE), add = TRUE) 16 | expect_equal(get_needed_pkgs(tutorial_dir), c("pkg1", "pkg2")) 17 | }) 18 | 19 | test_that("get_needed_pkgs returns length 0 if no new packages", { 20 | tutorial_dir <- create_test_tutorial("sum()") 21 | on.exit(unlink(tutorial_dir, recursive = TRUE), add = TRUE) 22 | expect_equal(length(get_needed_pkgs(tutorial_dir)), 0) 23 | }) 24 | 25 | test_that("tutorial dependency check returns NULL for no dependencies", { 26 | tutorial_dir <- create_test_tutorial("sum(1:3)") 27 | on.exit(unlink(tutorial_dir, recursive = TRUE), add = TRUE) 28 | 29 | expect_silent(install_tutorial_dependencies(tutorial_dir)) 30 | }) 31 | 32 | # test_that("tutorial dependency check works (interactive)", { 33 | # skip_if_not(interactive()) 34 | # 35 | # tutorial_dir <- create_test_tutorial("library(pkg1)\npkg2::n()") 36 | # on.exit(unlink(tutorial_dir, recursive = TRUE), add = TRUE) 37 | # 38 | # expect_error( 39 | # with_mock( 40 | # ask_pkgs_install = function(x) 2, 41 | # install_tutorial_dependencies(tutorial_dir) 42 | # ) 43 | # ) 44 | # }) 45 | 46 | test_that("tutorial dependency check works (not interactive)", { 47 | skip_if(interactive()) 48 | 49 | tutorial_dir <- create_test_tutorial("library(pkg1)\npkg2::n()") 50 | on.exit(unlink(tutorial_dir, recursive = TRUE), add = TRUE) 51 | 52 | expect_error(install_tutorial_dependencies(tutorial_dir)) 53 | }) 54 | -------------------------------------------------------------------------------- /man/question_radio.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/question_radio.R 3 | \name{question_radio} 4 | \alias{question_radio} 5 | \title{Radio question} 6 | \usage{ 7 | question_radio( 8 | text, 9 | ..., 10 | correct = "Correct!", 11 | incorrect = "Incorrect", 12 | try_again = incorrect, 13 | allow_retry = FALSE, 14 | random_answer_order = FALSE 15 | ) 16 | } 17 | \arguments{ 18 | \item{text}{Question or option text} 19 | 20 | \item{...}{Answers created with \code{\link[=answer]{answer()}} or extra parameters passed onto 21 | \code{\link[=question]{question()}}. Function answers are ignored for radio questions because the 22 | user is required to select a single answer.} 23 | 24 | \item{correct}{For \code{question}, text to print for a correct answer (defaults 25 | to "Correct!"). For \code{answer}, a boolean indicating whether this answer is 26 | correct.} 27 | 28 | \item{incorrect}{Text to print for an incorrect answer (defaults to 29 | "Incorrect") when \code{allow_retry} is \code{FALSE}.} 30 | 31 | \item{try_again}{Text to print for an incorrect answer (defaults to 32 | "Incorrect") when \code{allow_retry} is \code{TRUE}.} 33 | 34 | \item{allow_retry}{Allow retry for incorrect answers. Defaults to \code{FALSE}.} 35 | 36 | \item{random_answer_order}{Display answers in a random order.} 37 | } 38 | \value{ 39 | Returns a learnr question of type \code{"learnr_radio"}. 40 | } 41 | \description{ 42 | Creates a radio button tutorial quiz question. The student can select only 43 | one radio button before submitting their answer. Note: Multiple correct 44 | answers are allowed. 45 | } 46 | \examples{ 47 | question_radio( 48 | "Pick the letter B", 49 | answer("A"), 50 | answer("B", correct = TRUE), 51 | answer("C"), 52 | answer("D"), 53 | allow_retry = TRUE, 54 | random_answer_order = TRUE 55 | ) 56 | 57 | } 58 | \seealso{ 59 | Other Interactive Questions: 60 | \code{\link{question_checkbox}()}, 61 | \code{\link{question_numeric}()}, 62 | \code{\link{question_text}()}, 63 | \code{\link{quiz}()} 64 | } 65 | \concept{Interactive Questions} 66 | -------------------------------------------------------------------------------- /man/random_phrases_add.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/praise.R 3 | \name{random_phrases_add} 4 | \alias{random_phrases_add} 5 | \title{Add phrases to the bank of random phrases} 6 | \usage{ 7 | random_phrases_add(language = "en", praise = NULL, encouragement = NULL) 8 | } 9 | \arguments{ 10 | \item{language}{The language of the phrases to be added.} 11 | 12 | \item{praise, encouragement}{A vector of praising or encouraging phrases, 13 | including final punctuation.} 14 | } 15 | \value{ 16 | Returns the previous custom phrases invisibly when called in the 17 | global setup chunk or interactively. Otherwise, it returns a shiny pre- 18 | rendered chunk. 19 | } 20 | \description{ 21 | Augment the random phrases available in \code{\link[=random_praise]{random_praise()}} and 22 | \code{\link[=random_encouragement]{random_encouragement()}} with phrases of your own. Note that these phrases 23 | are added to the existing phrases, rather than overwriting them. 24 | } 25 | \section{Usage in learnr tutorials}{ 26 | 27 | 28 | To add random phrases in a learnr tutorial, you can either include one or 29 | more calls to \code{random_phrases_add()} in your global setup chunk: 30 | 31 | \if{html}{\out{
}}\preformatted{```\{r setup, include = FALSE\}`r ''` 32 | library(learnr) 33 | random_phrases_add( 34 | language = "en", 35 | praise = "Great work!", 36 | encouragement = "I believe in you." 37 | ) 38 | ``` 39 | }\if{html}{\out{
}} 40 | 41 | Alternatively, you can call \code{random_phrases_add()} in a separate, standard 42 | R chunk (with \code{echo = FALSE}): 43 | 44 | \if{html}{\out{
}}\preformatted{```\{r setup-phrases, echo = FALSE\}`r ''` 45 | random_phrases_add( 46 | language = "en", 47 | praise = c("Great work!", "You're awesome!"), 48 | encouragement = c("I believe in you.", "Yes we can!") 49 | ) 50 | ``` 51 | }\if{html}{\out{
}} 52 | } 53 | 54 | \examples{ 55 | random_phrases_add("demo", praise = "Great!", encouragement = "Try again.") 56 | random_praise(language = "demo") 57 | random_encouragement(language = "demo") 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /R/html_selector.R: -------------------------------------------------------------------------------- 1 | 2 | # only handles id and classes 3 | as_selector <- function(selector) { 4 | if (inherits(selector, "shiny_selector") || inherits(selector, "shiny_selector_list")) { 5 | return(selector) 6 | } 7 | 8 | # make sure it's a trimmed string 9 | selector <- str_trim(selector) 10 | 11 | # yell if there is a comma 12 | if (grepl(",", selector, fixed = TRUE)) { 13 | stop("Do not know how to handle comma separated selector values") 14 | } 15 | 16 | # if it contains multiple elements, recurse 17 | if (grepl(" ", selector)) { 18 | selector <- lapply(strsplit(selector, "\\s+"), as_selector) 19 | selector <- structure(class = "shiny_selector_list", selector) 20 | return(selector) 21 | } 22 | 23 | match_everything <- isTRUE(all.equal(selector, "*")) 24 | 25 | element <- str_match(selector, "^([^#.]+)") 26 | selector <- str_remove(selector, "^[^#.]+") 27 | 28 | id <- str_remove(str_match(selector, "^#([^.]+)"), "#") 29 | selector <- str_remove(selector, "^#[^.]+") 30 | 31 | classes <- str_remove(str_match_all(selector, "\\.([^.]+)"), "^\\.") 32 | 33 | structure(class = "shiny_selector", list( 34 | element = element, 35 | id = id, 36 | classes = classes, 37 | match_everything = match_everything 38 | )) 39 | } 40 | 41 | as_selector_list <- function(selector) { 42 | selector <- as_selector(selector) 43 | if (inherits(selector, "shiny_selector")) { 44 | selector <- structure(class = "shiny_selector_list", list(selector)) 45 | } 46 | selector 47 | } 48 | 49 | format.shiny_selector <- function(x, ...) { 50 | if (x$match_everything) { 51 | paste0("* // match everything") 52 | } else { 53 | paste0(x$element, if (!is.null(x$id)) paste0("#", x$id), paste0(".", x$classes, collapse = "")) 54 | } 55 | } 56 | format.shiny_selector_list <- function(x, ...) { 57 | paste0(unlist(lapply(x, format, ...)), collapse = " ") 58 | } 59 | 60 | print.shiny_selector <- function(x, ...) { 61 | cat("// css selector\n") 62 | cat(format(x, ...), "\n") 63 | } 64 | print.shiny_selector_list <- function(x, ...) { 65 | cat("// css selector list\n") 66 | cat(format(x, ...), "\n") 67 | } 68 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Pull Request 2 | 3 | Before you submit a pull request, please do the following: 4 | 5 | * Add an entry to NEWS concisely describing what you changed. 6 | 7 | * Add unit tests in the tests/testthat directory. 8 | 9 | * Run Build->Check Package in the RStudio IDE, or `devtools::check()`, to make sure your change did not add any messages, warnings, or errors. 10 | 11 | Doing these things will make it easier for the learnr development team to evaluate your pull request. Even so, we may still decide to modify your code or even not merge it at all. Factors that may prevent us from merging the pull request include: 12 | 13 | * breaking backward compatibility 14 | * adding a feature that we do not consider relevant for learnr 15 | * is hard to understand 16 | * is hard to maintain in the future 17 | * is computationally expensive 18 | * is not intuitive for people to use 19 | 20 | We will try to be responsive and provide feedback in case we decide not to merge your pull request. 21 | 22 | 23 | ## Minimal reproducible example 24 | 25 | Finally, please include a minimal reprex. The goal of a reprex is to make it as easy as possible for me to recreate your problem so that I can fix it. If you've never heard of a reprex before, start by reading , and follow the advice further down the page. Do NOT include session info unless it's explicitly asked for, or you've used `reprex::reprex(..., si = TRUE)` to hide it away. 26 | ```r 27 | reprex::reprex({ 28 | library(learnr) 29 | # insert minimal reprex here 30 | }) 31 | ``` 32 | 33 | Due to the nature of learnr being interactive, if a reprex is not possible, please include a minimal learnr tutorial RMarkdown file that can be called with `rmarkdown::run` to produce an error or display the error has been fixed. 34 | 35 | Delete these instructions once you have read them. 36 | 37 | --- 38 | 39 | Brief description of the original problem and the approach behind your solution. 40 | 41 | ```r 42 | # insert reprex here 43 | ``` 44 | 45 | PR task list: 46 | - [ ] Update NEWS 47 | - [ ] Add tests (if possible) 48 | - [ ] Update documentation with `devtools::document()` 49 | -------------------------------------------------------------------------------- /inst/rmarkdown/templates/tutorial/skeleton/skeleton.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Tutorial" 3 | output: learnr::tutorial 4 | runtime: shiny_prerendered 5 | --- 6 | 7 | ```{r setup, include=FALSE} 8 | library(learnr) 9 | knitr::opts_chunk$set(echo = FALSE) 10 | ``` 11 | 12 | 13 | ## Topic 1 14 | 15 | ### Exercise 16 | 17 | *Here's a simple exercise with an empty code chunk provided for entering the answer.* 18 | 19 | Write the R code required to add two plus two: 20 | 21 | ```{r two-plus-two, exercise=TRUE} 22 | 23 | ``` 24 | 25 | ### Exercise with Code 26 | 27 | *Here's an exercise with some prepopulated code as well as `exercise.lines = 5` to provide a bit more initial room to work.* 28 | 29 | Now write a function that adds any two numbers and then call it: 30 | 31 | ```{r add-function, exercise=TRUE, exercise.lines = 5} 32 | add <- function() { 33 | 34 | } 35 | ``` 36 | 37 | ## Topic 2 38 | 39 | ### Exercise with Hint 40 | 41 | *Here's an exercise where the chunk is pre-evaluated via the `exercise.eval` option (so the user can see the default output we'd like them to customize). We also add a "hint" to the correct solution via the chunk immediate below labeled `print-limit-hint`.* 42 | 43 | Modify the following code to limit the number of rows printed to 5: 44 | 45 | ```{r print-limit, exercise=TRUE, exercise.eval=TRUE} 46 | mtcars 47 | ``` 48 | 49 | ```{r print-limit-hint} 50 | head(mtcars) 51 | ``` 52 | 53 | ### Quiz 54 | 55 | *You can include any number of single or multiple choice questions as a quiz. Use the `question` function to define a question and the `quiz` function for grouping multiple questions together.* 56 | 57 | Some questions to verify that you understand the purposes of various base and recommended R packages: 58 | 59 | ```{r quiz} 60 | quiz( 61 | question("Which package contains functions for installing other R packages?", 62 | answer("base"), 63 | answer("tools"), 64 | answer("utils", correct = TRUE), 65 | answer("codetools") 66 | ), 67 | question("Which of the R packages listed below are used to create plots?", 68 | answer("lattice", correct = TRUE), 69 | answer("tools"), 70 | answer("stats"), 71 | answer("grid", correct = TRUE) 72 | ) 73 | ) 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /man/one_time.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/events.R 3 | \name{one_time} 4 | \alias{one_time} 5 | \title{Wrap an expression that will be executed one time in an event handler} 6 | \usage{ 7 | one_time(session, cond, expr, label = deparse(substitute(cond))) 8 | } 9 | \arguments{ 10 | \item{session}{A Shiny session object.} 11 | 12 | \item{cond}{A condition that is used as a filter. The first time the 13 | condition evaluates to true, \code{expr} will be evaluated; after that, \code{expr} 14 | will not be evaluated again.} 15 | 16 | \item{expr}{An expression that will be evaluated once, the first time that 17 | \code{cond} is true.} 18 | 19 | \item{label}{A unique identifier. This is used as an ID for the condition and 20 | expression; if two calls to \code{one_time()} uses the same label, there will be 21 | an ID collision and only one of them will execute. By default, \code{cond} is 22 | deparsed and used as the label.} 23 | } 24 | \value{ 25 | The result of evaluating \code{expr} (\code{one_time()} is intended to be 26 | called within an event handler). 27 | } 28 | \description{ 29 | This wraps an expression so that it will be executed one time for a tutorial, 30 | based on some condition. The first time the condition is true, the expression 31 | will be executed; after that, the expression will not be evaluated again. 32 | 33 | The execution state is stored so that if the expression is executed, then the 34 | user quits the tutorial and then returns to it, the expression will not be 35 | executed a second time. 36 | 37 | A common use for \code{one_time} is to execute an expression when a section is 38 | viewed for the first time. 39 | } 40 | \examples{ 41 | \dontrun{ 42 | # This goes in a {r context="server-start"} chunk 43 | 44 | # The expression with message() will be executed the first time the user 45 | # sees the section with ID "section-exercise-with-hint". 46 | event_register_handler("section_viewed", 47 | function(session, event, data) { 48 | one_time( 49 | session, 50 | data$sectionId == "section-exercise-with-hint", 51 | { 52 | message("Seeing ", data$sectionId, " for the first time.") 53 | } 54 | ) 55 | } 56 | ) 57 | 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /tests/testthat/test-events.R: -------------------------------------------------------------------------------- 1 | test_that("Event handlers", { 2 | # Check that session, event, data are passed to callback. 3 | result <- NULL 4 | cancel <- event_register_handler("foo", 5 | function(session, event, data) { result <<- list(session, event, data) } 6 | ) 7 | on.exit(cancel(), add = TRUE) 8 | event_trigger("session_obj", "foo", "data") 9 | expect_identical(result, list("session_obj", "foo", "data")) 10 | cancel() 11 | 12 | 13 | # Testing multiple event handlers for same event, checking for order 14 | x <- numeric() 15 | cancel1 <- event_register_handler( 16 | "foo", 17 | function(session, event, data) { x <<- c(x, 1) } 18 | ) 19 | on.exit(cancel1(), add = TRUE) 20 | 21 | cancel2 <- event_register_handler( 22 | "foo", 23 | function(session, event, data) { x <<- c(x, 2) } 24 | ) 25 | on.exit(cancel2(), add = TRUE) 26 | 27 | event_trigger(NULL, "foo", NA) 28 | expect_identical(x, c(1, 2)) 29 | 30 | event_trigger(NULL, "foo", NA) 31 | expect_identical(x, c(1, 2, 1, 2)) 32 | 33 | # Cancel first handler 34 | expect_true(cancel1()) 35 | expect_false(cancel1()) 36 | 37 | event_trigger(NULL, "foo", NA) 38 | expect_identical(x, c(1, 2, 1, 2, 2)) 39 | }) 40 | 41 | 42 | test_that("Event handler input checking", { 43 | # Should error if callback has incorrect args (session, event, data) 44 | expect_error( 45 | event_register_handler("foo", function(session, data) NULL) 46 | ) 47 | expect_error( 48 | event_register_handler("foo", function(session, data, event) NULL) 49 | ) 50 | 51 | # Error for empty event name 52 | expect_error( 53 | event_register_handler("", function(session, event, data) NULL) 54 | ) 55 | }) 56 | 57 | 58 | test_that("Errors are converted to warnings", { 59 | n <- 0 60 | f <- function() g() 61 | g <- function() stop("error in g") 62 | cancel1 <- event_register_handler("foo", function(session, event, data) f()) 63 | on.exit(cancel1(), add = TRUE) 64 | cancel2 <- event_register_handler("foo", function(session, event, data) n <<- n + 1) 65 | on.exit(cancel2(), add = TRUE) 66 | 67 | expect_warning(event_trigger(NULL, "foo", NA), "error in g") 68 | # Other callbacks should still have executed. 69 | expect_identical(n, 1) 70 | }) 71 | -------------------------------------------------------------------------------- /man/run_tutorial.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/run.R 3 | \name{run_tutorial} 4 | \alias{run_tutorial} 5 | \title{Run a tutorial} 6 | \usage{ 7 | run_tutorial( 8 | name = NULL, 9 | package = NULL, 10 | ..., 11 | shiny_args = NULL, 12 | clean = FALSE, 13 | as_rstudio_job = NULL 14 | ) 15 | } 16 | \arguments{ 17 | \item{name}{Tutorial name (subdirectory within \code{tutorials/} directory of 18 | installed \code{package}). Alternatively, if \code{package} is not provided, \code{name} 19 | may be a path to a local tutorial R Markdown file or a local directory 20 | containing a learnr tutorial. If \code{package} is provided, \code{name} must be the 21 | tutorial name.} 22 | 23 | \item{package}{Name of package. If \code{name} is a path to the local directory 24 | containing a learnr tutorial, then \code{package} should not be provided.} 25 | 26 | \item{...}{Unused. Included for future expansion and to ensure named 27 | arguments are used.} 28 | 29 | \item{shiny_args}{Additional arguments to forward to 30 | \code{\link[shiny:runApp]{shiny::runApp}}.} 31 | 32 | \item{clean}{When \code{TRUE}, the shiny prerendered HTML files are removed and 33 | the tutorial is re-rendered prior to starting the tutorial.} 34 | 35 | \item{as_rstudio_job}{Runs the tutorial in the background as an RStudio job. 36 | This is the default behavior when \code{run_tutorial()} detects that RStudio is 37 | available and can run jobs. Set to \code{FALSE} to disable and to run the 38 | tutorial in the current R session. 39 | 40 | When running as an RStudio job, \code{run_tutorial()} sets or overrides the 41 | \code{launch.browser} option for \code{shiny_args}. You can instead use the 42 | \code{shiny.launch.browser} global option in your current R session to set 43 | the default behavior when the tutorial is run. See \link[shiny:shinyOptions]{the shiny options documentation} for more information.} 44 | } 45 | \value{ 46 | Starts a Shiny server running the learnr tutorial. 47 | } 48 | \description{ 49 | Run a tutorial provided by an installed R package. 50 | } 51 | \examples{ 52 | # display all "learnr" tutorials 53 | available_tutorials("learnr") 54 | 55 | # run basic example within learnr 56 | \dontrun{ 57 | run_tutorial("hello", "learnr") 58 | } 59 | 60 | } 61 | \seealso{ 62 | \code{\link{safe}} and \code{\link{available_tutorials}} 63 | } 64 | -------------------------------------------------------------------------------- /tests/testthat/tutorials/basic.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Tutorial" 3 | output: learnr::tutorial 4 | tutorial: 5 | id: test-basic 6 | version: 9.9.9 7 | runtime: shiny_prerendered 8 | --- 9 | 10 | ```{r setup, include=FALSE} 11 | library(learnr) 12 | knitr::opts_chunk$set(echo = FALSE) 13 | ``` 14 | 15 | 16 | ## Topic 1 17 | 18 | ### Exercise 19 | 20 | *Here's a simple exercise with an empty code chunk provided for entering the answer.* 21 | 22 | Write the R code required to add two plus two: 23 | 24 | ```{r two-plus-two, exercise=TRUE} 25 | 26 | ``` 27 | 28 | ### Exercise with Code 29 | 30 | *Here's an exercise with some prepopulated code as well as `exercise.lines = 5` to provide a bit more initial room to work.* 31 | 32 | Now write a function that adds any two numbers and then call it: 33 | 34 | ```{r add-function, exercise=TRUE, exercise.lines = 5} 35 | add <- function() { 36 | 37 | } 38 | ``` 39 | 40 | ## Topic 2 41 | 42 | ### Exercise with Hint 43 | 44 | *Here's an exercise where the chunk is pre-evaluated via the `exercise.eval` option (so the user can see the default output we'd like them to customize). We also add a "hint" to the correct solution via the chunk immediate below labeled `print-limit-hint`.* 45 | 46 | Modify the following code to limit the number of rows printed to 5: 47 | 48 | ```{r print-limit, exercise=TRUE, exercise.eval=TRUE} 49 | mtcars 50 | ``` 51 | 52 | ```{r print-limit-hint} 53 | head(mtcars) 54 | ``` 55 | 56 | ### Quiz 57 | 58 | *You can include any number of single or multiple choice questions as a quiz. Use the `question` function to define a question and the `quiz` function for grouping multiple questions together.* 59 | 60 | Some questions to verify that you understand the purposes of various base and recommended R packages: 61 | 62 | ```{r quiz} 63 | # Quiz answers create a stochastic ID that changes when rendered from a clean state 64 | # so this chunk has been disabled. 65 | quiz( 66 | question("Which package contains functions for installing other R packages?", 67 | answer("base"), 68 | answer("tools"), 69 | answer("utils", correct = TRUE), 70 | answer("codetools") 71 | ), 72 | question("Which of the R packages listed below are used to create plots?", 73 | answer("lattice", correct = TRUE), 74 | answer("tools"), 75 | answer("stats"), 76 | answer("grid", correct = TRUE) 77 | ) 78 | ) 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /man/tutorial_options.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/options.R 3 | \name{tutorial_options} 4 | \alias{tutorial_options} 5 | \title{Set tutorial options} 6 | \usage{ 7 | tutorial_options( 8 | exercise.cap = NULL, 9 | exercise.eval = FALSE, 10 | exercise.timelimit = 30, 11 | exercise.lines = NULL, 12 | exercise.blanks = NULL, 13 | exercise.checker = NULL, 14 | exercise.error.check.code = NULL, 15 | exercise.completion = TRUE, 16 | exercise.diagnostics = TRUE, 17 | exercise.startover = TRUE, 18 | exercise.reveal_solution = TRUE 19 | ) 20 | } 21 | \arguments{ 22 | \item{exercise.cap}{Caption for exercise chunk (defaults to the engine's icon or the combination of the engine and \code{" code"}).} 23 | 24 | \item{exercise.eval}{Whether to pre-evaluate the exercise so the reader can 25 | see some default output (defaults to \code{FALSE}).} 26 | 27 | \item{exercise.timelimit}{Number of seconds to limit execution time to 28 | (defaults to \code{30}).} 29 | 30 | \item{exercise.lines}{Lines of code for exercise editor (defaults to the 31 | number of lines in the code chunk).} 32 | 33 | \item{exercise.blanks}{A regular expression to be used to identify blanks in 34 | submitted code that the user should fill in. If \code{TRUE} (default), blanks 35 | are three or more underscores in a row. If \code{FALSE}, blank checking is not 36 | performed.} 37 | 38 | \item{exercise.checker}{Function used to check exercise answers 39 | (e.g., \code{gradethis::grade_learnr()}).} 40 | 41 | \item{exercise.error.check.code}{A string containing R code to use for checking 42 | code when an exercise evaluation error occurs (e.g., \code{"gradethis::grade_code()"}).} 43 | 44 | \item{exercise.completion}{Use code completion in exercise editors.} 45 | 46 | \item{exercise.diagnostics}{Show diagnostics in exercise editors.} 47 | 48 | \item{exercise.startover}{Show "Start Over" button on exercise.} 49 | 50 | \item{exercise.reveal_solution}{Whether to reveal the exercise solution if 51 | a solution chunk is provided.} 52 | } 53 | \value{ 54 | Nothing. Invisibly sets \link[knitr:opts_chunk]{knitr::opts_chunk} settings. 55 | } 56 | \description{ 57 | Set various tutorial options that control the display and evaluation of 58 | exercises. 59 | } 60 | \examples{ 61 | if (interactive()) { 62 | tutorial_options(exercise.eval = TRUE, exercise.timelimt = 10) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /tests/testthat/tutorials/optional-show-solution.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Optionally Reveal Exercise Solution" 3 | output: learnr::tutorial 4 | runtime: shiny_prerendered 5 | --- 6 | 7 | ```{r setup, include=FALSE} 8 | library(learnr) 9 | 10 | HIDE_SOLUTION <- FALSE 11 | SHOW_SOLUTION <- TRUE 12 | 13 | # Set global option reveal solution option 14 | # options(tutorial.exercise.reveal_solution = FALSE) 15 | # tutorial_options(exercise.reveal_solution = TRUE) 16 | #<> 17 | ``` 18 | 19 | ## Intro 20 | 21 | ### Default 22 | 23 | ```{r default, exercise = TRUE} 24 | 1 + 1 25 | ``` 26 | 27 | ```{r default-hint-1} 28 | # DEFAULT HINT 1 29 | ``` 30 | 31 | ```{r default-hint-2} 32 | # DEFAULT HINT 2 33 | ``` 34 | 35 | ```{r default-solution} 36 | # DEFAULT SOLUTION 4631b0 37 | ``` 38 | 39 | ### Reveal Set on Exercise 40 | 41 | ```{r set-on-ex, exercise = TRUE, exercise.reveal_solution = TRUE} 42 | 1 + 1 43 | ``` 44 | 45 | ```{r set-on-ex-hint-1} 46 | # EXERCISE OPT HINT 1 47 | ``` 48 | 49 | ```{r set-on-ex-hint-2} 50 | # EXERCISE OPT HINT 2 51 | ``` 52 | 53 | ```{r set-on-ex-solution} 54 | # EXERCISE OPT SOLUTION 15c861 55 | ``` 56 | 57 | ### Always Hides Solution 58 | 59 | ```{r hide, exercise = TRUE, exercise.reveal_solution = TRUE} 60 | 1 + 1 61 | ``` 62 | 63 | ```{r hide-hint-1} 64 | # HIDDEN HINT 1 65 | ``` 66 | 67 | ```{r hide-hint-2} 68 | # HIDDEN HINT 2 69 | ``` 70 | 71 | ```{r hide-solution, exercise.reveal_solution = FALSE} 72 | # HIDDEN SOLUTION 48da3c 73 | ``` 74 | 75 | ### Always Shows Solution 76 | 77 | ```{r show, exercise = TRUE, exercise.reveal_solution = FALSE} 78 | 1 + 1 79 | ``` 80 | 81 | ```{r show-hint-1} 82 | # SHOWN HINT 1 83 | ``` 84 | 85 | ```{r show-hint-2} 86 | # SHOWN HINT 2 87 | ``` 88 | 89 | ```{r show-solution, exercise.reveal_solution = TRUE} 90 | # SHOWN SOLUTION 781cbb 91 | ``` 92 | 93 | ### Hidden Using Variable in Chunk Opt 94 | 95 | ```{r var-hide, exercise = TRUE, exercise.reveal_solution = HIDE_SOLUTION} 96 | 1 + 1 97 | ``` 98 | 99 | ```{r var-hide-solution} 100 | # HIDDEN VAR SOLUTION 0b219b 101 | ``` 102 | 103 | ### Shown Using Variable in Chunk Opt 104 | 105 | ```{r var-shown, exercise = TRUE, exercise.reveal_solution = SHOW_SOLUTION} 106 | 1 + 1 107 | ``` 108 | 109 | ```{r var-shown-solution} 110 | # SHOWN VAR SOLUTION aba888 111 | ``` 112 | -------------------------------------------------------------------------------- /R/initialize.R: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #' Initialize tutorial R Markdown extensions 5 | #' 6 | #' One time initialization of R Markdown extensions required by the 7 | #' \pkg{learnr} package. This function is typically called automatically 8 | #' as a result of using exercises or questions. 9 | #' 10 | #' @return If not previously run, initializes knitr hooks and provides the 11 | #' required [rmarkdown::shiny_prerendered_chunk()]s to initialize \pkg{learnr}. 12 | #' 13 | #' @export 14 | initialize_tutorial <- function() { 15 | 16 | # helper function for one time initialization 17 | if (isTRUE(getOption("knitr.in.progress")) && 18 | !isTRUE(knitr::opts_knit$get("tutorial.initialized"))) { 19 | 20 | # html dependencies 21 | knitr::knit_meta_add(list( 22 | rmarkdown::html_dependency_jquery(), 23 | rmarkdown::html_dependency_font_awesome(), 24 | bootbox_html_dependency(), 25 | idb_html_dependency(), 26 | tutorial_html_dependency() 27 | )) 28 | 29 | # session initialization (forward tutorial metadata) 30 | rmarkdown::shiny_prerendered_chunk( 31 | 'server', 32 | sprintf('learnr:::register_http_handlers(session, metadata = %s)', 33 | dput_to_string(rmarkdown::metadata$tutorial)), 34 | singleton = TRUE 35 | ) 36 | 37 | # clear exercise/question and initialize user state reactive 38 | rmarkdown::shiny_prerendered_chunk( 39 | 'server', 40 | 'learnr:::prepare_tutorial_state(session)', 41 | singleton = TRUE 42 | ) 43 | 44 | # record tutorial language in session object 45 | rmarkdown::shiny_prerendered_chunk( 46 | "server", 47 | "learnr:::i18n_observe_tutorial_language(input, session)" 48 | ) 49 | 50 | # Register session stop handler 51 | rmarkdown::shiny_prerendered_chunk( 52 | 'server', 53 | sprintf('session$onSessionEnded(function() { 54 | learnr:::event_trigger(session, "session_stop") 55 | })'), 56 | singleton = TRUE 57 | ) 58 | 59 | # set initialized flag to ensure single initialization 60 | knitr::opts_knit$set(tutorial.initialized = TRUE) 61 | } 62 | } 63 | 64 | 65 | dput_to_string <- function(x) { 66 | conn <- textConnection("dput_to_string", "w") 67 | on.exit({close(conn)}) 68 | dput(x, file = conn) 69 | # Must use a `"\n"` if `dput()`ing a function 70 | paste0(textConnectionValue(conn), collapse = "\n") 71 | } 72 | -------------------------------------------------------------------------------- /inst/lib/idb-keyval/idb-keyval-iife-compat.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | idb-keyval -- super-simple-small promise-based keyval store implemented with IndexedDB 3 | Version 3.2.0 4 | https: //github.com/jakearchibald/idb-keyval 5 | (c) 2016 Jake Archibald, Apache License 2.0 6 | */ 7 | "use strict";var _createClass=function(){function e(e,n){for(var t=0;t0&&void 0!==arguments[0]?arguments[0]:"keyval-store",r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"keyval";_classCallCheck(this,e),this.storeName=r,this._dbp=n(indexedDB.open(t),r).then(function(e){return e.objectStoreNames.contains(r)?e:(e.close(),n(indexedDB.open(t,e.version+1),r))})}return _createClass(e,[{key:"_withIDBStore",value:function(e,n){var t=this;return this._dbp.then(function(r){return new Promise(function(o,i){var u=r.transaction(t.storeName,e);u.oncomplete=function(){return o()},u.onabort=u.onerror=function(){return i(u.error)},n(u.objectStore(t.storeName))})})}}]),e}(),r=void 0;function o(){return r||(r=new t),r}return e.Store=t,e.get=function(e){var n=void 0;return(arguments.length>1&&void 0!==arguments[1]?arguments[1]:o())._withIDBStore("readonly",function(t){n=t.get(e)}).then(function(){return n.result})},e.set=function(e,n){return(arguments.length>2&&void 0!==arguments[2]?arguments[2]:o())._withIDBStore("readwrite",function(t){t.put(n,e)})},e.del=function(e){return(arguments.length>1&&void 0!==arguments[1]?arguments[1]:o())._withIDBStore("readwrite",function(n){n.delete(e)})},e.clear=function(){return(arguments.length>0&&void 0!==arguments[0]?arguments[0]:o())._withIDBStore("readwrite",function(e){e.clear()})},e.keys=function(){var e=[];return(arguments.length>0&&void 0!==arguments[0]?arguments[0]:o())._withIDBStore("readonly",function(n){(n.openKeyCursor||n.openCursor).call(n).onsuccess=function(){this.result&&(e.push(this.result.key),this.result.continue())}}).then(function(){return e})},e}({}); 8 | -------------------------------------------------------------------------------- /R/tutorial_package_dependencies.R: -------------------------------------------------------------------------------- 1 | get_needed_pkgs <- function(dir) { 2 | 3 | pkgs <- tutorial_dir_package_dependencies(dir) 4 | 5 | pkgs[!pkgs %in% utils::installed.packages()] 6 | } 7 | 8 | format_needed_pkgs <- function(needed_pkgs) { 9 | paste(" -", needed_pkgs, collapse = "\n") 10 | } 11 | 12 | ask_pkgs_install <- function(needed_pkgs) { 13 | question <- sprintf("Would you like to install the following packages?\n%s", 14 | format_needed_pkgs(needed_pkgs)) 15 | 16 | utils::menu(choices = c("yes", "no"), 17 | title = question) 18 | } 19 | 20 | install_tutorial_dependencies <- function(dir) { 21 | needed_pkgs <- get_needed_pkgs(dir) 22 | 23 | if(length(needed_pkgs) == 0) { 24 | return(invisible(NULL)) 25 | } 26 | 27 | if(!interactive()) { 28 | stop("The following packages need to be installed:\n", 29 | format_needed_pkgs(needed_pkgs)) 30 | } 31 | 32 | answer <- ask_pkgs_install(needed_pkgs) 33 | 34 | if(answer == 2) { 35 | stop("The tutorial is missing required packages and cannot be rendered.") 36 | } 37 | 38 | utils::install.packages(needed_pkgs) 39 | } 40 | 41 | 42 | 43 | 44 | #' List tutorial dependencies 45 | #' 46 | #' List the \R packages required to run a particular tutorial. 47 | #' 48 | #' @param name The tutorial name. If \code{name} is \code{NULL}, then all 49 | #' tutorials within \code{package} will be searched. 50 | #' @param package The \R package providing the tutorial. If \code{package} is 51 | #' \code{NULL}, then all tutorials will be searched. 52 | #' 53 | #' @return A character vector of package names that are required for execution. 54 | #' 55 | #' @examples 56 | #' tutorial_package_dependencies(package = "learnr") 57 | #' 58 | #' @export 59 | tutorial_package_dependencies <- function(name = NULL, package = NULL) { 60 | if (identical(name, NULL)) { 61 | info <- available_tutorials(package = package) 62 | return( 63 | sort(unique(unlist(info$package_dependencies))) 64 | ) 65 | } 66 | 67 | tutorial_dir_package_dependencies( 68 | get_tutorial_path(name = name, package = package) 69 | ) 70 | } 71 | 72 | tutorial_dir_package_dependencies <- function(dir) { 73 | # enumerate tutorial package dependencies 74 | deps <- renv::dependencies(dir, quiet = TRUE) 75 | 76 | # R <= 3.4 can not sort(NULL) 77 | # if no deps are found, renv::dependencies returns NULL 78 | if (is.null(deps)) { 79 | return(NULL) 80 | } 81 | 82 | sort(unique(deps$Package)) 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/pr-commands.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | issue_comment: 3 | types: [created] 4 | name: Commands 5 | jobs: 6 | document: 7 | if: startsWith(github.event.comment.body, '/document') 8 | name: document 9 | runs-on: ubuntu-18.04 10 | env: 11 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: r-lib/actions/pr-fetch@v2 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | - uses: r-lib/actions/setup-r@v2 18 | - name: Install dependencies 19 | run: | 20 | options(repos = c(CRAN = "https://demo.rstudiopm.com/all/__linux__/bionic/latest")) 21 | install.packages(c("remotes", "roxygen2")) 22 | remotes::install_deps(dependencies = TRUE) 23 | shell: Rscript {0} 24 | - name: Document 25 | run: Rscript -e 'roxygen2::roxygenise()' 26 | - name: commit 27 | run: | 28 | git config --local user.name "$GITHUB_ACTOR" 29 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 30 | git add man/\* NAMESPACE 31 | git commit -m 'Document' 32 | - uses: r-lib/actions/pr-push@v2 33 | with: 34 | repo-token: ${{ secrets.GITHUB_TOKEN }} 35 | i18n: 36 | if: startsWith(github.event.comment.body, '/i18nize') 37 | name: i18nize 38 | runs-on: ubuntu-18.04 39 | env: 40 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 41 | steps: 42 | - uses: actions/checkout@v2 43 | - uses: r-lib/actions/pr-fetch@v2 44 | with: 45 | repo-token: ${{ secrets.GITHUB_TOKEN }} 46 | - uses: r-lib/actions/setup-r@v2 47 | - name: Install dependencies 48 | run: | 49 | options(repos = c(CRAN = "https://demo.rstudiopm.com/all/__linux__/bionic/latest")) 50 | install.packages(c("cli", "here", "purrr", "stringi", "yaml")) 51 | shell: Rscript {0} 52 | - name: Internationalize 53 | run: Rscript data-raw/i18n_translations.R 54 | - name: commit 55 | run: | 56 | git config --local user.name "$GITHUB_ACTOR" 57 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 58 | git commit inst/internals/i18n* -m 'Re-build internal internationalization data' || echo "No changes to commit" 59 | git push origin || echo "No changes to commit" 60 | - uses: r-lib/actions/pr-push@v2 61 | with: 62 | repo-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /pkgdown/assets/snippets/ace/theme-textmate.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/textmate",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";t.isDark=!1,t.cssClass="ace-tm",t.cssText='.ace-tm .ace_gutter {background: #f0f0f0;color: #333;}.ace-tm .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-tm .ace_fold {background-color: #6B72E6;}.ace-tm {background-color: #FFFFFF;color: black;}.ace-tm .ace_cursor {color: black;}.ace-tm .ace_invisible {color: rgb(191, 191, 191);}.ace-tm .ace_storage,.ace-tm .ace_keyword {color: blue;}.ace-tm .ace_constant {color: rgb(197, 6, 11);}.ace-tm .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-tm .ace_constant.ace_language {color: rgb(88, 92, 246);}.ace-tm .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-tm .ace_invalid {background-color: rgba(255, 0, 0, 0.1);color: red;}.ace-tm .ace_support.ace_function {color: rgb(60, 76, 114);}.ace-tm .ace_support.ace_constant {color: rgb(6, 150, 14);}.ace-tm .ace_support.ace_type,.ace-tm .ace_support.ace_class {color: rgb(109, 121, 222);}.ace-tm .ace_keyword.ace_operator {color: rgb(104, 118, 135);}.ace-tm .ace_string {color: rgb(3, 106, 7);}.ace-tm .ace_comment {color: rgb(76, 136, 107);}.ace-tm .ace_comment.ace_doc {color: rgb(0, 102, 255);}.ace-tm .ace_comment.ace_doc.ace_tag {color: rgb(128, 159, 191);}.ace-tm .ace_constant.ace_numeric {color: rgb(0, 0, 205);}.ace-tm .ace_variable {color: rgb(49, 132, 149);}.ace-tm .ace_xml-pe {color: rgb(104, 104, 91);}.ace-tm .ace_entity.ace_name.ace_function {color: #0000A2;}.ace-tm .ace_heading {color: rgb(12, 7, 255);}.ace-tm .ace_list {color:rgb(185, 6, 144);}.ace-tm .ace_meta.ace_tag {color:rgb(0, 22, 142);}.ace-tm .ace_string.ace_regex {color: rgb(255, 0, 0)}.ace-tm .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-tm.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px white;}.ace-tm .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-tm .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-tm .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-tm .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.07);}.ace-tm .ace_gutter-active-line {background-color : #dcdcdc;}.ace-tm .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-tm .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}';var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /man/get_tutorial_state.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tutorial-state.R 3 | \name{get_tutorial_state} 4 | \alias{get_tutorial_state} 5 | \title{Observe the user's progress in the tutorial} 6 | \usage{ 7 | get_tutorial_state(label = NULL, session = getDefaultReactiveDomain()) 8 | } 9 | \arguments{ 10 | \item{label}{A length-1 character label of the exercise or question.} 11 | 12 | \item{session}{The \code{session} object passed to function given to 13 | \code{shinyServer.} Default is \code{\link[shiny:domains]{shiny::getDefaultReactiveDomain()}}.} 14 | } 15 | \value{ 16 | A reactiveValues object or a single reactive value (if \code{label} is 17 | provided). The names of the full reactiveValues object correspond to the 18 | label of the question or exercise. Each item contains the following 19 | entries: 20 | \itemize{ 21 | \item \code{type}: One of \code{"question"} or \code{"exercise"}. 22 | \item \code{answer}: A character vector containing the user's submitted answer(s). 23 | \item \code{correct}: A logical indicating whether the user's answer was correct, 24 | or a logical \code{NA} if the submission was not checked for correctness. 25 | \item \code{timestamp}: The time at which the user's submission was completed, as 26 | a character string in UTC, formatted as \code{"\%F \%H:\%M:\%OS3 \%Z"}. 27 | } 28 | } 29 | \description{ 30 | As a student progresses through a \pkg{learnr} tutorial, their progress is 31 | stored in a Shiny reactive values list for their session (see 32 | \code{\link[shiny:reactiveValues]{shiny::reactiveValues()}}). Without arguments, \code{get_tutorial_state()} returns 33 | the full reactiveValues object that can be converted to a conventional list 34 | with \code{\link[shiny:reactiveValuesToList]{shiny::reactiveValuesToList()}}. If the \code{label} argument is provided, 35 | the state of an individual question or exercise with that label is returned. 36 | 37 | Calling \code{get_tutorial_state()} introduces a reactive dependency on the state 38 | of returned questions or exercises unless called within \code{isolate()}. Note 39 | that \code{get_tutorial_state()} will only work for the tutorial author and must 40 | be used in a reactive context, i.e. within \code{\link[shiny:observe]{shiny::observe()}}, 41 | \code{\link[shiny:observeEvent]{shiny::observeEvent()}}, or \code{\link[shiny:reactive]{shiny::reactive()}}. Any logic observing the 42 | user's tutorial state must be written inside a \code{context="server"} chunk in 43 | the tutorial's R Markdown source. 44 | } 45 | \seealso{ 46 | \code{\link[=get_tutorial_info]{get_tutorial_info()}} 47 | } 48 | -------------------------------------------------------------------------------- /R/learnr_messages.R: -------------------------------------------------------------------------------- 1 | # {learnr} functions are intended to be written into R Markdown documents, 2 | # but there are certain times when we'd like to warn tutorial authors of 3 | # potential issues without the warning text appearing in the actual tutorial. 4 | # Since we can't ask users to set message = FALSE globally, we have to do our 5 | # own thing. Instead, we have a way to create messages that are automatically 6 | # added to a queue of items when knitting is in progress -- if we're not knitting 7 | # then we just emit the message immediately. Then we take advantage of the 8 | # `tutorial` knit hook that runs before and after each chunk in the tutorial. 9 | # In the after run, we flush the queue and re-signal the condition so that it 10 | # appears in the render console, thus avoiding writing to the tutorial HTML. 11 | 12 | .learnr_messages <- local({ 13 | queue <- list() 14 | list( 15 | peek = function() { 16 | if (length(queue)) queue 17 | }, 18 | flush = function() { 19 | while(length(queue)) { 20 | cnd <- queue[[1]] 21 | if (inherits(cnd, "error")) { 22 | # throw errors, they're immediate 23 | rlang::cnd_signal(cnd) 24 | } else { 25 | # otherwise report condition as a message, but re-signal warnings 26 | msg <- rlang::cnd_message(cnd) 27 | 28 | if (inherits(cnd, "warning")) { 29 | mgs <- paste0("Warning: ", msg) 30 | rlang::cnd_signal(cnd) 31 | } 32 | 33 | rlang::inform(msg, class = "learnr_render_message") 34 | } 35 | queue[[1]] <<- NULL 36 | } 37 | }, 38 | add = function(cnd) { 39 | queue <<- c(queue, list(cnd)) 40 | invisible(cnd) 41 | } 42 | ) 43 | }) 44 | 45 | learnr_render_message <- function(..., level = c("inform", "warn", "abort")) { 46 | create_cnd <- switch( 47 | tolower(level), 48 | inform = rlang::inform, 49 | warn = rlang::warn, 50 | abort = rlang::abort 51 | ) 52 | cnd <- rlang::catch_cnd(create_cnd(paste0(..., "\n"), "learnr_render_message")) 53 | 54 | if (isTRUE(getOption('knitr.in.progress'))) { 55 | .learnr_messages$add(cnd) 56 | } else { 57 | rlang::cnd_signal(cnd) 58 | } 59 | } 60 | 61 | learnr_render_catch <- function(expr, env = rlang::caller_env()) { 62 | cnd <- tryCatch( 63 | rlang::eval_bare(expr, env), 64 | error = identity, 65 | warning = identity, 66 | message = identity 67 | ) 68 | 69 | if (!inherits(cnd, "condition")) { 70 | return(invisible()) 71 | } 72 | 73 | if (isTRUE(getOption('knitr.in.progress'))) { 74 | .learnr_messages$add(cnd) 75 | } else { 76 | rlang::cnd_signal(cnd) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /R/html-dependencies.R: -------------------------------------------------------------------------------- 1 | 2 | #' Tutorial HTML dependency 3 | #' 4 | #' HTML dependency for core tutorial JS and CSS. This should be included as a 5 | #' dependency for custom tutorial formats that wish to ensure that that 6 | #' `tutorial.js` and `tutorial.css` are loaded prior their own scripts and 7 | #' stylesheets. 8 | #' 9 | #' @return \pkg{learnr}'s HTML dependencies 10 | #' 11 | #' @export 12 | tutorial_html_dependency <- function() { 13 | htmltools::htmlDependency( 14 | name = "tutorial", 15 | version = utils::packageVersion("learnr"), 16 | src = html_dependency_src("lib", "tutorial"), 17 | script = "tutorial.js", 18 | stylesheet = "tutorial.css", 19 | all_files = TRUE 20 | ) 21 | } 22 | 23 | html_dependency_src <- function(...) { 24 | if (nzchar(Sys.getenv("RMARKDOWN_SHINY_PRERENDERED_DEVMODE"))) { 25 | r_dir <- utils::getSrcDirectory(html_dependency_src, unique = TRUE) 26 | pkg_dir <- dirname(r_dir) 27 | file.path(pkg_dir, "inst", ...) 28 | } 29 | else { 30 | system.file(..., package = "learnr") 31 | } 32 | } 33 | 34 | idb_html_dependency <- function() { 35 | htmltools::htmlDependency( 36 | name = "idb-keyvalue", 37 | version = "3.2.0", 38 | src = system.file("lib/idb-keyval", package = "learnr"), 39 | script = "idb-keyval-iife-compat.min.js", 40 | all_files = FALSE 41 | ) 42 | } 43 | 44 | bootbox_html_dependency <- function() { 45 | htmltools::htmlDependency( 46 | name = "bootbox", 47 | version = "5.5.2", 48 | src = system.file("lib/bootbox", package = "learnr"), 49 | script = "bootbox.min.js" 50 | ) 51 | } 52 | 53 | clipboardjs_html_dependency <- function() { 54 | htmltools::htmlDependency( 55 | name = "clipboardjs", 56 | version = "2.0.10", 57 | src = system.file("lib/clipboardjs", package = "learnr"), 58 | script = "clipboard.min.js" 59 | ) 60 | } 61 | 62 | 63 | ace_html_dependency <- function() { 64 | htmltools::htmlDependency( 65 | name = "ace", 66 | version = ACE_VERSION, 67 | src = system.file("lib/ace", package = "learnr"), 68 | script = "ace.js" 69 | ) 70 | } 71 | 72 | tutorial_i18n_html_dependency <- function(language = NULL) { 73 | htmltools::htmlDependency( 74 | name = "i18n", 75 | version = "21.6.10", 76 | src = system.file("lib/i18n", package = "learnr"), 77 | script = c("i18next.min.js", "tutorial-i18n-init.js"), 78 | head = format(htmltools::tags$script( 79 | id = "i18n-cstm-trns", 80 | type = "application/json", 81 | htmltools::HTML( 82 | jsonlite::toJSON( 83 | i18n_process_language_options(language), 84 | auto_unbox = TRUE 85 | ) 86 | ) 87 | )) 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # learnr 2 | 3 | 4 | 5 | [![R build 6 | status](https://github.com/rstudio/learnr/workflows/R-CMD-check/badge.svg)](https://github.com/rstudio/learnr) 7 | [![CRAN 8 | status](https://www.r-pkg.org/badges/version/learnr)](https://CRAN.R-project.org/package=learnr) 9 | [![learnr downloads per 10 | month](http://cranlogs.r-pkg.org/badges/learnr)](http://www.rpackages.io/package/learnr) 11 | [![DOI](https://zenodo.org/badge/71377580.svg)](https://zenodo.org/badge/latestdoi/71377580) 12 |
[![GitHub 13 | Discussions](https://img.shields.io/github/discussions/rstudio/learnr?logo=github&style=social)](https://github.com/rstudio/learnr/discussions) 14 | [![RStudio 15 | community](https://img.shields.io/badge/community-teaching-blue?style=social&logo=rstudio&logoColor=75AADB)](https://community.rstudio.com/c/teaching/13) 16 | [![RStudio 17 | community](https://img.shields.io/badge/community-learnr-blue?style=social&logo=rstudio&logoColor=75AADB)](https://community.rstudio.com/new-topic?title=&category_id=13&tags=learnr&body=%0A%0A%0A%20%20--------%0A%20%20%0A%20%20%3Csup%3EReferred%20here%20by%20%60learnr%60%27s%20GitHub%3C/sup%3E%0A&u=barret) 18 | 19 | 20 | The **learnr** package makes it easy to turn any [R 21 | Markdown](https://rmarkdown.rstudio.com/) document into an interactive 22 | tutorial. Tutorials consist of content along with interactive components 23 | for checking and reinforcing understanding. Tutorials can include any or 24 | all of the following: 25 | 26 | 1. Narrative, figures, illustrations, and equations. 27 | 28 | 2. Code exercises (R code chunks that users can edit and execute 29 | directly). 30 | 31 | 3. Quiz questions. 32 | 33 | 4. Videos (supported services include YouTube and Vimeo). 34 | 35 | 5. Interactive Shiny components. 36 | 37 | Tutorials automatically preserve work done within them, so if a user 38 | works on a few exercises or questions and returns to the tutorial later 39 | they can pick up right where they left off. 40 | 41 | Learn more about the **learnr** package and try example tutorials online 42 | at . 43 | 44 | ## Installation 45 | 46 | Install the latest official learnr release from CRAN: 47 | 48 | install.packages("learnr") 49 | 50 | Or you can install the most recent version in-development from GitHub 51 | with the [remotes package](https://remotes.r-lib.org): 52 | 53 | # install.packages("remotes") 54 | remotes::install_github("rstudio/learnr") 55 | 56 | learnr works best with a recent [version of 57 | RStudio](https://posit.co/download/rstudio-desktop/) (v1.0.136 or later) 58 | which includes tools for easily running and previewing tutorials. 59 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Type: Package 2 | Package: learnr 3 | Title: Interactive Tutorials for R 4 | Version: 0.11.3.9000 5 | Authors@R: c( 6 | person("Garrick", "Aden-Buie", , "garrick@posit.co", role = c("aut", "cre"), 7 | comment = c(ORCID = "0000-0002-7111-0077")), 8 | person("Barret", "Schloerke", , "barret@posit.co", role = "aut", 9 | comment = c(ORCID = "0000-0001-9986-114X")), 10 | person("JJ", "Allaire", , "jj@posit.co", role = c("aut", "ccp")), 11 | person("Alexander", "Rossell Hayes", , "alex.rossellhayes@posit.co", role = "aut", 12 | comment = c(ORCID = "0000-0001-9412-0457")), 13 | person("Nischal", "Shrestha", , "nischal@posit.co", role = "ctb", 14 | comment = c(ORCID = "0000-0003-3321-1712")), 15 | person("Angela", "Li", , "angelali921@gmail.com", role = "ctb", 16 | comment = "vignette"), 17 | person("Posit, PBC", role = c("cph", "fnd")), 18 | person(, "Ajax.org B.V.", role = c("ctb", "cph"), 19 | comment = "Ace library"), 20 | person("Zeno", "Rocha", role = c("ctb", "cph"), 21 | comment = "clipboard.js library"), 22 | person("Nick", "Payne", role = c("ctb", "cph"), 23 | comment = "Bootbox library"), 24 | person("Jake", "Archibald", role = c("ctb", "cph"), 25 | comment = "idb-keyval library"), 26 | person("i18next authors", role = c("ctb", "cph"), 27 | comment = "i18next library") 28 | ) 29 | Description: Create interactive tutorials using R Markdown. Use a 30 | combination of narrative, figures, videos, exercises, and quizzes to 31 | create self-paced tutorials for learning about R and R packages. 32 | License: Apache License (>= 2.0) 33 | URL: https://rstudio.github.io/learnr/, https://github.com/rstudio/learnr 34 | BugReports: https://github.com/rstudio/learnr/issues 35 | Imports: 36 | checkmate, 37 | digest, 38 | ellipsis (>= 0.2.0.1), 39 | evaluate, 40 | htmltools (>= 0.3.5), 41 | htmlwidgets, 42 | jsonlite, 43 | knitr (>= 1.31), 44 | lifecycle, 45 | markdown (>= 1.3), 46 | parallel, 47 | promises, 48 | rappdirs, 49 | renv (>= 0.8.0), 50 | rlang, 51 | rmarkdown (>= 1.12.0), 52 | rprojroot, 53 | shiny (>= 1.0), 54 | stats, 55 | utils, 56 | withr 57 | Suggests: 58 | bslib, 59 | callr, 60 | curl, 61 | DBI (>= 0.4-1), 62 | httpuv, 63 | later, 64 | reticulate, 65 | RSQLite, 66 | rstudioapi (>= 0.11), 67 | shinytest2, 68 | sortable, 69 | testthat (>= 3.0.3) 70 | VignetteBuilder: 71 | knitr 72 | Config/Needs/connect: rsconnect 73 | Config/Needs/coverage: covr 74 | Config/Needs/website: pkgdown, tidyverse/tidytemplate 75 | Encoding: UTF-8 76 | Roxygen: list(markdown = TRUE) 77 | RoxygenNote: 7.2.3 78 | SystemRequirements: pandoc (>= 1.14) - http://pandoc.org 79 | -------------------------------------------------------------------------------- /R/feedback.R: -------------------------------------------------------------------------------- 1 | # Provide exercise feedback 2 | feedback <- function(message, correct, type, location) { 3 | feedback_validated(list( 4 | message = message, 5 | correct = correct, 6 | type = type, 7 | location = location 8 | )) 9 | } 10 | 11 | # return feedback if it's valid (with defaults), otherwise throw an error 12 | feedback_validated <- function(feedback) { 13 | if (!length(feedback)) { 14 | return(feedback) 15 | } 16 | if (!(is.list(feedback) && all(c("message", "correct") %in% names(feedback)))) { 17 | stop("Feedback must be a list with 'message' and 'correct' fields", call. = FALSE) 18 | } 19 | if (!(is.character(feedback$message) || inherits(feedback$message, c("shiny.tag", "shiny.tag.list")))) { 20 | stop("The 'message' field of feedback must be a character vector or an htmltools tag or tagList", call. = FALSE) 21 | } 22 | if (!is.logical(feedback$correct)) { 23 | stop("The 'correct' field of feedback must be a logical (i.e., boolean) value", call. = FALSE) 24 | } 25 | # Fill in type/location defaults and check their value 26 | feedback$type <- feedback$type[1] %||% "auto" 27 | feedback$location <- feedback$location[1] %||% "append" 28 | feedback_types <- c("auto", "success", "info", "warning", "error", "custom") 29 | if (!feedback$type %in% feedback_types) { 30 | stop("Feedback 'type' field must be one of these values: ", 31 | paste(feedback_types, collapse = ", "), call. = FALSE) 32 | } 33 | feedback_locations <- c("append", "prepend", "replace") 34 | if (!feedback$location %in% feedback_locations) { 35 | stop("Feedback 'location' field must be one of these values: ", 36 | paste(feedback_locations, collapse = ", "), call. = FALSE) 37 | } 38 | if (feedback$type %in% "auto") { 39 | feedback$type <- if (feedback$correct) "success" else "error" 40 | } 41 | feedback 42 | } 43 | 44 | feedback_as_html <- function(feedback) { 45 | if (!length(feedback)) { 46 | return(feedback) 47 | } 48 | feedback <- feedback_validated(feedback) 49 | if (feedback$type %in% "custom") { 50 | return(div(feedback$message)) 51 | } 52 | if (feedback$type %in% "error") { 53 | feedback$type <- "danger" 54 | } 55 | if (feedback$type %in% c("success", "info", "warning", "danger")) { 56 | return(div( 57 | role = "alert", 58 | class = paste0("alert alert-", feedback$type), 59 | feedback$message 60 | )) 61 | } 62 | stop("Invalid message type specified.", call. = FALSE) 63 | } 64 | 65 | # helper function to create tags for error message 66 | error_message_html <- function(message, style = "code") { 67 | switch( 68 | style, 69 | alert = div(class = "alert alert-danger", role = "alert", message), 70 | code = , 71 | pre( 72 | code(class = "text-danger", message, .noWS = c("before", "after")), 73 | .noWS = c("before", "after") 74 | ) 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /revdep/README.md: -------------------------------------------------------------------------------- 1 | # Platform 2 | 3 | |field |value | 4 | |:--------|:------------------------------| 5 | |version |R version 4.2.2 (2022-10-31) | 6 | |os |macOS Ventura 13.2 | 7 | |system |aarch64, darwin20 | 8 | |ui |X11 | 9 | |language |(EN) | 10 | |collate |en_US.UTF-8 | 11 | |ctype |en_US.UTF-8 | 12 | |tz |America/New_York | 13 | |date |2023-03-14 | 14 | |pandoc |3.1 @ /opt/homebrew/bin/pandoc | 15 | 16 | # Dependencies 17 | 18 | |package |old |new |Δ | 19 | |:-----------|:---------|:---------|:--| 20 | |learnr |0.11.2 |0.11.3 |* | 21 | |backports |1.4.1 |1.4.1 | | 22 | |base64enc |0.1-3 |0.1-3 | | 23 | |bslib |0.4.2 |0.4.2 | | 24 | |cachem |1.0.7 |1.0.7 | | 25 | |checkmate |2.1.0 |2.1.0 | | 26 | |cli |3.6.0 |3.6.0 | | 27 | |commonmark |1.8.1 |1.8.1 | | 28 | |crayon |1.5.2 |1.5.2 | | 29 | |curl |5.0.0 |5.0.0 | | 30 | |digest |0.6.31 |0.6.31 | | 31 | |ellipsis |0.3.2 |0.3.2 | | 32 | |evaluate |0.20 |0.20 | | 33 | |fastmap |1.1.1 |1.1.1 | | 34 | |fontawesome |0.5.0 |0.5.0 | | 35 | |fs |1.6.1 |1.6.1 | | 36 | |glue |1.6.2 |1.6.2 | | 37 | |highr |0.10 |0.10 | | 38 | |htmltools |0.5.4 |0.5.4 | | 39 | |htmlwidgets |1.6.1 |1.6.1 | | 40 | |httpuv |1.6.9 |1.6.9 | | 41 | |jquerylib |0.1.4 |0.1.4 | | 42 | |jsonlite |1.8.4 |1.8.4 | | 43 | |knitr |1.42 |1.42 | | 44 | |later |1.3.0 |1.3.0 | | 45 | |lifecycle |1.0.3 |1.0.3 | | 46 | |magrittr |2.0.3 |2.0.3 | | 47 | |markdown |1.5 |1.5 | | 48 | |memoise |2.0.1 |2.0.1 | | 49 | |mime |0.12 |0.12 | | 50 | |promises |1.2.0.1 |1.2.0.1 | | 51 | |R6 |2.5.1 |2.5.1 | | 52 | |rappdirs |0.3.3 |0.3.3 | | 53 | |Rcpp |1.0.10 |1.0.10 | | 54 | |renv |0.17.0-40 |0.17.0-49 |* | 55 | |rlang |1.0.6 |1.0.6 | | 56 | |rmarkdown |2.20 |2.20 | | 57 | |rprojroot |2.0.3 |2.0.3 | | 58 | |sass |0.4.5 |0.4.5 | | 59 | |shiny |1.7.4 |1.7.4 | | 60 | |sourcetools |0.1.7-1 |0.1.7-1 | | 61 | |stringi |1.7.12 |1.7.12 | | 62 | |stringr |1.5.0 |1.5.0 | | 63 | |tinytex |0.44 |0.44 | | 64 | |vctrs |0.5.2 |0.5.2 | | 65 | |withr |2.5.0 |2.5.0 | | 66 | |xfun |0.37 |0.37 | | 67 | |xtable |1.8-4 |1.8-4 | | 68 | |yaml |2.3.7 |2.3.7 | | 69 | 70 | # Revdeps 71 | 72 | -------------------------------------------------------------------------------- /tests/testthat/test-mutate_tags.R: -------------------------------------------------------------------------------- 1 | has_class <- function(el, .class, ...) { 2 | class_idx <- which(names(el$attribs) == "class") 3 | if (!length(class_idx)) return(FALSE) 4 | el_class <- vapply(class_idx, function(i) el$attribs[[i]], FUN.VALUE = character(1)) 5 | grepl(.class, paste(el_class, collapse = " "), ...) 6 | } 7 | 8 | test_that("finalize_tags() finalizes the question UI", { 9 | q_ui_final <- finalize_question( 10 | htmltools::div( 11 | class = "custom-question", 12 | htmltools::div("answer 1"), 13 | htmltools::div("answer 2") 14 | ) 15 | ) 16 | 17 | expect_true(has_class(q_ui_final, "question-final")) 18 | expect_true(has_class(q_ui_final, "disabled")) 19 | expect_true(has_class(q_ui_final$children[[1]], "disabled")) 20 | expect_true(has_class(q_ui_final$children[[2]], "disabled")) 21 | expect_true("disabled" %in% q_ui_final$attribs) 22 | expect_true("disabled" %in% names(q_ui_final$children[[1]]$attribs)) 23 | expect_true("disabled" %in% names(q_ui_final$children[[2]]$attribs)) 24 | 25 | q_ui_checkbox <- 26 | checkboxGroupInput( 27 | "q-checkbox", 28 | label = "check box question", 29 | choiceValues = letters, 30 | choiceNames = LETTERS, 31 | selected = "a" 32 | ) 33 | 34 | q_ui_checkbox_final <- finalize_question(q_ui_checkbox) 35 | # before 36 | expect_false(has_class(q_ui_checkbox, "question-final")) 37 | expect_false(has_class(q_ui_checkbox, "disabled")) 38 | expect_false("disabled" %in% names(q_ui_checkbox$attribs)) 39 | # after 40 | expect_true(has_class(q_ui_checkbox_final, "question-final")) 41 | expect_true(has_class(q_ui_checkbox_final, "disabled")) 42 | expect_true("disabled" %in% names(q_ui_checkbox_final$attribs)) 43 | 44 | q_ui_radio <- 45 | radioButtons( 46 | "q-radio", 47 | label = "radio question", 48 | choiceValues = letters, 49 | choiceNames = LETTERS, 50 | selected = "b" 51 | ) 52 | 53 | q_ui_radio_final <- finalize_question(q_ui_radio) 54 | # before 55 | expect_false(has_class(q_ui_radio, "question-final")) 56 | expect_false(has_class(q_ui_radio, "disabled")) 57 | expect_false("disabled" %in% names(q_ui_radio$attribs)) 58 | # after 59 | expect_true(has_class(q_ui_radio_final, "question-final")) 60 | expect_true(has_class(q_ui_radio_final, "disabled")) 61 | expect_true("disabled" %in% names(q_ui_radio_final$attribs)) 62 | }) 63 | 64 | test_that("finalize_question() works with a shiny.tag.list, too", { 65 | q_ui_final <- finalize_question( 66 | htmltools::tagList( 67 | htmltools::div("thing 1"), 68 | htmltools::div("thing 2") 69 | ) 70 | ) 71 | 72 | expect_s3_class(q_ui_final, "shiny.tag.list") 73 | expect_true(has_class(q_ui_final[[1]], "question-final")) 74 | expect_true(has_class(q_ui_final[[1]], "disabled")) 75 | expect_true(has_class(q_ui_final[[2]], "question-final")) 76 | expect_true(has_class(q_ui_final[[2]], "disabled")) 77 | }) --------------------------------------------------------------------------------