From efa977722d80cc02cacd9096bef753ab80a2bc64 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Fri, 25 Jun 2021 13:58:42 -0700 Subject: [PATCH 01/58] Add handler for underscores to render_exercise() --- R/exercise.R | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/R/exercise.R b/R/exercise.R index 626c32096..1ecb67ba9 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -562,6 +562,18 @@ render_exercise <- function(exercise, envir) { if (grepl(pattern, msg, fixed = TRUE)) { return(exercise_result_timeout()) } + + pattern <- paste0( + gettext("unexpected input", domain = "R"), + "\n1: _\n ^" + ) + if (grepl(pattern, msg, fixed = TRUE)) { + return(exercise_result_error(paste( + "The exercise contains underscores.", + "Please replace the _ with valid R code." + ))) + } + if (length(exercise$error_check)) { # Run the condition through an error checker (the exercise could be to throw an error!) checker_feedback <- try_checker( From 8f1323c1e748d944637223114f083e3aefb4d94e Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Fri, 25 Jun 2021 23:47:21 -0700 Subject: [PATCH 02/58] Import and modify code from `gradethis` that serves a similar function --- R/exercise.R | 49 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 1ecb67ba9..f41b95d10 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -307,6 +307,13 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { eval(parse(text = exercise$global_setup), envir = envir) } + parse_results <- tryCatch( + parse(text = exercise$code), + error = function(e) exercise_result_parse_error(e, exercise) + ) + + if (is_exercise_result(parse_results)) {return(parse_results)} + # Setup a temporary directory for rendering the exercise exercise_dir <- tempfile(pattern = "lnr-ex") dir.create(exercise_dir) @@ -562,18 +569,6 @@ render_exercise <- function(exercise, envir) { if (grepl(pattern, msg, fixed = TRUE)) { return(exercise_result_timeout()) } - - pattern <- paste0( - gettext("unexpected input", domain = "R"), - "\n1: _\n ^" - ) - if (grepl(pattern, msg, fixed = TRUE)) { - return(exercise_result_error(paste( - "The exercise contains underscores.", - "Please replace the _ with valid R code." - ))) - } - if (length(exercise$error_check)) { # Run the condition through an error checker (the exercise could be to throw an error!) checker_feedback <- try_checker( @@ -670,6 +665,36 @@ exercise_code_chunks <- function(chunks) { }, character(1)) } +exercise_result_parse_error <- function(error, exercise) { + # Code scaffolding in exercise code will cause parse errors, so first check + # for blanks. We consider a blank to be 3+ "_" characters. + n_blanks <- sum(vapply( + gregexpr("_{3,}", exercise$code), + function(x) sum(x > 0), + integer(1) + )) + + msg <- if (n_blanks > 0) { + paste0( + "The exercise contains ", n_blanks, " blank", + if (n_blanks != 1L) {"s"}, + ". Please replace the `____` with valid R code." + ) + } else { + paste0( + "It looks like this might not be valid R code:\n\n```r\n", + conditionMessage(error), + "\n```\n\nR cannot determine how to turn your text into ", + "a complete command. You may have forgotten to fill in a blank, ", + "to remove an underscore, to include a comma between arguments, ", + "or to close an opening `\"`, `'`, `(`, or `{{` ", + "with a matching `\"`, `'`, `)`, or `}}`. " + ) + } + + exercise_result_error(msg) +} + exercise_result_timeout <- function() { exercise_result_error( "Error: Your code ran longer than the permitted timelimit for this exercise.", From 8ed3ae4f1a4e48dcfe3eae34fed6a0144a5fb613 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Sat, 26 Jun 2021 20:31:25 -0700 Subject: [PATCH 03/58] Add tests with unparsable code --- R/exercise.R | 1 + tests/testthat/test-exercise.R | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/R/exercise.R b/R/exercise.R index f41b95d10..e86887cf2 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -307,6 +307,7 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { eval(parse(text = exercise$global_setup), envir = envir) } + # Check if user code is parsable parse_results <- tryCatch( parse(text = exercise$code), error = function(e) exercise_result_parse_error(e, exercise) diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index ef6b06922..3c608dba4 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -626,3 +626,28 @@ test_that("env vars are protected from both user and author modification", { expect_equal(res$after_eval, "APP") }) +# unparsable input ----------------------------------------------------------- + +test_that("evaluate_exercise() returns a message if code contains ___", { + ex <- mock_exercise(user_code = '____("test")') + result <- evaluate_exercise(ex, new.env()) + expect_match(result$error_message, "contains 1 blank") + + ex <- mock_exercise(user_code = '____("____")') + result <- evaluate_exercise(ex, new.env()) + expect_match(result$error_message, "contains 2 blanks") +}) + +est_that("evaluate_exercise() returns a message if code is unparsable", { + ex <- mock_exercise(user_code = 'print("test"') + result <- evaluate_exercise(ex, new.env()) + expect_match(result$error_message, "this might not be valid R code") + + ex <- mock_exercise(user_code = 'print("test)') + result <- evaluate_exercise(ex, new.env()) + expect_match(result$error_message, "this might not be valid R code") + + ex <- mock_exercise(user_code = 'mean(1:10 na.rm = TRUE)') + result <- evaluate_exercise(ex, new.env()) + expect_match(result$error_message, "this might not be valid R code") +}) From 0c8a229819dcdf78990ded676389f0ff2ab22274 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Tue, 29 Jun 2021 11:27:27 -0700 Subject: [PATCH 04/58] Add test for *parsable* code with ___ --- tests/testthat/test-exercise.R | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index 3c608dba4..91cb1630a 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -633,11 +633,21 @@ test_that("evaluate_exercise() returns a message if code contains ___", { result <- evaluate_exercise(ex, new.env()) expect_match(result$error_message, "contains 1 blank") + ex <- mock_exercise(user_code = '____(____)') + result <- evaluate_exercise(ex, new.env()) + expect_match(result$error_message, "contains 2 blanks") + ex <- mock_exercise(user_code = '____("____")') result <- evaluate_exercise(ex, new.env()) expect_match(result$error_message, "contains 2 blanks") }) +test_that("no error if code containing ___ is parsable", { + ex <- mock_exercise(user_code = 'print("____")') + result <- evaluate_exercise(ex, new.env()) + expect_null(result$error_message) +}) + est_that("evaluate_exercise() returns a message if code is unparsable", { ex <- mock_exercise(user_code = 'print("test"') result <- evaluate_exercise(ex, new.env()) From b09a6d45a276a95d1cff8078e93395befd8ff8fa Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes <44556601+rossellhayes@users.noreply.github.com> Date: Tue, 29 Jun 2021 12:24:06 -0700 Subject: [PATCH 05/58] Fix typo Co-authored-by: Garrick Aden-Buie --- tests/testthat/test-exercise.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index 91cb1630a..b0f4ed37e 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -648,7 +648,7 @@ test_that("no error if code containing ___ is parsable", { expect_null(result$error_message) }) -est_that("evaluate_exercise() returns a message if code is unparsable", { +test_that("evaluate_exercise() returns a message if code is unparsable", { ex <- mock_exercise(user_code = 'print("test"') result <- evaluate_exercise(ex, new.env()) expect_match(result$error_message, "this might not be valid R code") From 05d5e57b94c55b4bd5c33f1a96a2375405ea35b1 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes <44556601+rossellhayes@users.noreply.github.com> Date: Tue, 29 Jun 2021 12:24:48 -0700 Subject: [PATCH 06/58] Fix formatting Co-authored-by: Garrick Aden-Buie --- R/exercise.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/R/exercise.R b/R/exercise.R index e86887cf2..0075a1bc0 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -313,7 +313,9 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { error = function(e) exercise_result_parse_error(e, exercise) ) - if (is_exercise_result(parse_results)) {return(parse_results)} + if (is_exercise_result(parse_results)) { + return(parse_results) + } # Setup a temporary directory for rendering the exercise exercise_dir <- tempfile(pattern = "lnr-ex") From 36c838ae24d0d4831a909c33cf8f47270eeff7ba Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Tue, 29 Jun 2021 12:26:46 -0700 Subject: [PATCH 07/58] Run parse check before global setup --- R/exercise.R | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 0075a1bc0..017919ab0 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -303,20 +303,19 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { return(exercise_result(html_output = " ")) } - if (evaluate_global_setup) { - eval(parse(text = exercise$global_setup), envir = envir) - } - # Check if user code is parsable parse_results <- tryCatch( parse(text = exercise$code), error = function(e) exercise_result_parse_error(e, exercise) ) - if (is_exercise_result(parse_results)) { return(parse_results) } + if (evaluate_global_setup) { + eval(parse(text = exercise$global_setup), envir = envir) + } + # Setup a temporary directory for rendering the exercise exercise_dir <- tempfile(pattern = "lnr-ex") dir.create(exercise_dir) From 0ebebfe4883c4c14b5485976f5776a4cdcf1be03 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Tue, 29 Jun 2021 12:34:54 -0700 Subject: [PATCH 08/58] Add chunk option `exercise.parse.error.check` which disables parsability checking if `FALSE` --- R/exercise.R | 16 +++++++++------- tests/testthat/test-exercise.R | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 017919ab0..9839eb7c1 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -303,13 +303,15 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { return(exercise_result(html_output = " ")) } - # Check if user code is parsable - parse_results <- tryCatch( - parse(text = exercise$code), - error = function(e) exercise_result_parse_error(e, exercise) - ) - if (is_exercise_result(parse_results)) { - return(parse_results) + if (!isFALSE(exercise$options$exercise.parse.error.check)) { + # Check if user code is parsable + parse_results <- tryCatch( + parse(text = exercise$code), + error = function(e) exercise_result_parse_error(e, exercise) + ) + if (is_exercise_result(parse_results)) { + return(parse_results) + } } if (evaluate_global_setup) { diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index b0f4ed37e..9302ea405 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -661,3 +661,23 @@ test_that("evaluate_exercise() returns a message if code is unparsable", { result <- evaluate_exercise(ex, new.env()) expect_match(result$error_message, "this might not be valid R code") }) + +test_that("default error message if exercise.parse.error.check is FALSE", { + ex <- mock_exercise( + user_code = '____("test")', exercise.parse.error.check = FALSE + ) + result <- evaluate_exercise(ex, new.env()) + expect_match(result$error_message, "unexpected input") + + ex <- mock_exercise( + user_code = 'print("test"', exercise.parse.error.check = FALSE + ) + result <- evaluate_exercise(ex, new.env()) + expect_match(result$error_message, "unexpected end of input") + + ex <- mock_exercise( + user_code = 'mean(1:10 na.rm = TRUE)', exercise.parse.error.check = FALSE + ) + result <- evaluate_exercise(ex, new.env()) + expect_match(result$error_message, "unexpected symbol") +}) From e0c3fe1c75491f5ed7706778aad4e20cec7f4759 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Fri, 2 Jul 2021 10:32:20 -0700 Subject: [PATCH 09/58] Implement parse checking and blank checking as exercise.checker functions --- R/exercise.R | 91 +++++++++++++++++++++++----------- R/knitr-hooks.R | 5 +- tests/testthat/test-exercise.R | 36 +++++++------- 3 files changed, 84 insertions(+), 48 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 9839eb7c1..45b366efd 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -303,14 +303,41 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { return(exercise_result(html_output = " ")) } - if (!isFALSE(exercise$options$exercise.parse.error.check)) { + checker_feedback <- NULL + + if (!isFALSE(exercise$options$exercise.blank.check.code)) { + # Check if user code contains blanks + exercise$options$exercise.blank.check.code <- + exercise$options$exercise.blank.check.code %||% + dput_to_string(exercise_blank_checker) + + checker_feedback <- try_checker( + exercise, "exercise.blank.check.code", + check_code = exercise$options$exercise.blanks %||% "_{3,}", + envir_prep = duplicate_env(envir), + engine = exercise$engine + ) + if (is_exercise_result(checker_feedback)) { + return(checker_feedback) + } + } + + if ( + tolower(exercise$engine) == "r" && + !isFALSE(exercise$options$exercise.parse.check.code) + ) { # Check if user code is parsable - parse_results <- tryCatch( - parse(text = exercise$code), - error = function(e) exercise_result_parse_error(e, exercise) + exercise$options$exercise.parse.check.code <- exercise$parse_check %||% + exercise$options$exercise.parse.check.code %||% + dput_to_string(exercise_parse_checker) + + checker_feedback <- try_checker( + exercise, "exercise.parse.check.code", + envir_prep = duplicate_env(envir), + engine = exercise$engine ) - if (is_exercise_result(parse_results)) { - return(parse_results) + if (is_exercise_result(checker_feedback)) { + return(checker_feedback) } } @@ -323,7 +350,6 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { dir.create(exercise_dir) on.exit(unlink(exercise_dir), add = TRUE) - checker_feedback <- NULL # Run the checker pre-evaluation _if_ there is code checking to do if (length(exercise$code_check)) { checker_feedback <- try_checker( @@ -371,8 +397,8 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { } -try_checker <- function(exercise, name, check_code, envir_result, - evaluate_result, envir_prep, last_value, +try_checker <- function(exercise, name, check_code = NULL, envir_result = NULL, + evaluate_result = NULL, envir_prep, last_value = NULL, engine) { checker_func <- tryCatch( get_checker_func(exercise, name, envir_prep), @@ -669,34 +695,39 @@ exercise_code_chunks <- function(chunks) { }, character(1)) } -exercise_result_parse_error <- function(error, exercise) { - # Code scaffolding in exercise code will cause parse errors, so first check - # for blanks. We consider a blank to be 3+ "_" characters. +exercise_blank_checker <- function(label, user_code, check_code, ...) { n_blanks <- sum(vapply( - gregexpr("_{3,}", exercise$code), + gregexpr(check_code, user_code), function(x) sum(x > 0), integer(1) )) - msg <- if (n_blanks > 0) { - paste0( - "The exercise contains ", n_blanks, " blank", - if (n_blanks != 1L) {"s"}, - ". Please replace the `____` with valid R code." - ) - } else { - paste0( - "It looks like this might not be valid R code:\n\n```r\n", - conditionMessage(error), - "\n```\n\nR cannot determine how to turn your text into ", - "a complete command. You may have forgotten to fill in a blank, ", - "to remove an underscore, to include a comma between arguments, ", - "or to close an opening `\"`, `'`, `(`, or `{{` ", - "with a matching `\"`, `'`, `)`, or `}}`. " - ) + if (n_blanks == 0) { + return(NULL) } - exercise_result_error(msg) + msg <- paste0( + "The exercise contains ", n_blanks, " blank", if (n_blanks != 1L) {"s"}, + ". Please replace the `___` with valid code." + ) + + list(message = msg, correct = FALSE, location = "append") +} + +exercise_parse_checker <- function(label, user_code, ...) { + error <- rlang::catch_cnd(parse(text = user_code), "error") + if (is.null(error)) {return(NULL)} + + msg <- paste( + "It looks like this might not be valid R code.", + "R cannot determine how to turn your text into a complete command.", + "You may have forgotten to fill in a blank,", + "to remove an underscore, to include a comma between arguments,", + "or to close an opening `\"`, `'`, `(`, or `{{`", + "with a matching `\"`, `'`, `)`, or `}}`." + ) + + list(message = msg, correct = FALSE, type = "error", location = "append") } exercise_result_timeout <- function() { diff --git a/R/knitr-hooks.R b/R/knitr-hooks.R index b647df7f3..8c72c9d0f 100644 --- a/R/knitr-hooks.R +++ b/R/knitr-hooks.R @@ -29,6 +29,7 @@ install_knitr_hooks <- function() { "solution", "error-check", "code-check", + "parse-check", "check")) { support_regex <- paste0("-(", paste(type, collapse = "|"), ")$") if (grepl(support_regex, options$label)) { @@ -319,6 +320,7 @@ install_knitr_hooks <- function() { code_check_chunk <- get_knitr_chunk(paste0(options$label, "-code-check")) error_check_chunk <- get_knitr_chunk(paste0(options$label, "-error-check")) + parse_check_chunk <- get_knitr_chunk(paste0(options$label, "-parse-check")) check_chunk <- get_knitr_chunk(paste0(options$label, "-check")) solution <- get_knitr_chunk(paste0(options$label, "-solution")) @@ -339,8 +341,9 @@ install_knitr_hooks <- function() { chunks = all_chunks, code_check = code_check_chunk, error_check = error_check_chunk, + parse_check = parse_check_chunk, check = check_chunk, - solution = solution, + solution = solution, options = options, engine = options$engine) diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index 9302ea405..a5727edf4 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -631,52 +631,54 @@ test_that("env vars are protected from both user and author modification", { test_that("evaluate_exercise() returns a message if code contains ___", { ex <- mock_exercise(user_code = '____("test")') result <- evaluate_exercise(ex, new.env()) - expect_match(result$error_message, "contains 1 blank") + expect_match(result$feedback$message, "contains 1 blank") ex <- mock_exercise(user_code = '____(____)') result <- evaluate_exercise(ex, new.env()) - expect_match(result$error_message, "contains 2 blanks") + expect_match(result$feedback$message, "contains 2 blanks") ex <- mock_exercise(user_code = '____("____")') result <- evaluate_exercise(ex, new.env()) - expect_match(result$error_message, "contains 2 blanks") + expect_match(result$feedback$message, "contains 2 blanks") }) -test_that("no error if code containing ___ is parsable", { - ex <- mock_exercise(user_code = 'print("____")') +test_that("default message if exercise.blank.error.check is FALSE", { + ex <- mock_exercise( + user_code = 'print("____")', exercise.blank.check.code = FALSE + ) + result <- evaluate_exercise(ex, new.env()) + expect_null(result$feedback$message) + + ex <- mock_exercise( + user_code = '____("test")', exercise.blank.check.code = FALSE + ) result <- evaluate_exercise(ex, new.env()) - expect_null(result$error_message) + expect_match(result$feedback$message, "this might not be valid R code") }) test_that("evaluate_exercise() returns a message if code is unparsable", { ex <- mock_exercise(user_code = 'print("test"') result <- evaluate_exercise(ex, new.env()) - expect_match(result$error_message, "this might not be valid R code") + expect_match(result$feedback$message, "this might not be valid R code") ex <- mock_exercise(user_code = 'print("test)') result <- evaluate_exercise(ex, new.env()) - expect_match(result$error_message, "this might not be valid R code") + expect_match(result$feedback$message, "this might not be valid R code") ex <- mock_exercise(user_code = 'mean(1:10 na.rm = TRUE)') result <- evaluate_exercise(ex, new.env()) - expect_match(result$error_message, "this might not be valid R code") + expect_match(result$feedback$message, "this might not be valid R code") }) test_that("default error message if exercise.parse.error.check is FALSE", { ex <- mock_exercise( - user_code = '____("test")', exercise.parse.error.check = FALSE - ) - result <- evaluate_exercise(ex, new.env()) - expect_match(result$error_message, "unexpected input") - - ex <- mock_exercise( - user_code = 'print("test"', exercise.parse.error.check = FALSE + user_code = 'print("test"', exercise.parse.check.code = FALSE ) result <- evaluate_exercise(ex, new.env()) expect_match(result$error_message, "unexpected end of input") ex <- mock_exercise( - user_code = 'mean(1:10 na.rm = TRUE)', exercise.parse.error.check = FALSE + user_code = 'mean(1:10 na.rm = TRUE)', exercise.parse.check.code = FALSE ) result <- evaluate_exercise(ex, new.env()) expect_match(result$error_message, "unexpected symbol") From 3931555605a26a8f5918897b62bcacdf991ef8ea Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Fri, 2 Jul 2021 12:45:01 -0700 Subject: [PATCH 10/58] Remove support for `-parse-check` chunk --- R/exercise.R | 2 +- R/knitr-hooks.R | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 45b366efd..047383001 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -327,7 +327,7 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { !isFALSE(exercise$options$exercise.parse.check.code) ) { # Check if user code is parsable - exercise$options$exercise.parse.check.code <- exercise$parse_check %||% + exercise$options$exercise.parse.check.code <- exercise$options$exercise.parse.check.code %||% dput_to_string(exercise_parse_checker) diff --git a/R/knitr-hooks.R b/R/knitr-hooks.R index 8c72c9d0f..5014d787b 100644 --- a/R/knitr-hooks.R +++ b/R/knitr-hooks.R @@ -29,7 +29,6 @@ install_knitr_hooks <- function() { "solution", "error-check", "code-check", - "parse-check", "check")) { support_regex <- paste0("-(", paste(type, collapse = "|"), ")$") if (grepl(support_regex, options$label)) { @@ -320,7 +319,6 @@ install_knitr_hooks <- function() { code_check_chunk <- get_knitr_chunk(paste0(options$label, "-code-check")) error_check_chunk <- get_knitr_chunk(paste0(options$label, "-error-check")) - parse_check_chunk <- get_knitr_chunk(paste0(options$label, "-parse-check")) check_chunk <- get_knitr_chunk(paste0(options$label, "-check")) solution <- get_knitr_chunk(paste0(options$label, "-solution")) @@ -341,7 +339,6 @@ install_knitr_hooks <- function() { chunks = all_chunks, code_check = code_check_chunk, error_check = error_check_chunk, - parse_check = parse_check_chunk, check = check_chunk, solution = solution, options = options, From e6ccad9b3eb889ee799ed13b8338fd8681750565 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Tue, 6 Jul 2021 09:36:41 -0700 Subject: [PATCH 11/58] Add isFALSE() as an internal function for R < 3.5 --- R/utils.R | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/R/utils.R b/R/utils.R index 034e27cc6..88ff915c3 100644 --- a/R/utils.R +++ b/R/utils.R @@ -107,3 +107,7 @@ is_installed <- function(package, version = NULL) { } TRUE } + +isFALSE <- function(x) { + is.logical(x) && length(x) == 1L && !is.na(x) && !x +} \ No newline at end of file From b40c9614b9245d6021ad150ad301c8dd0ce7ae09 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Tue, 6 Jul 2021 14:06:06 -0700 Subject: [PATCH 12/58] Ensure blank check and parse check options can be set in global setup --- R/exercise.R | 14 +++++++++----- tests/testthat/test-exercise.R | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 047383001..e085ee724 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -303,17 +303,24 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { return(exercise_result(html_output = " ")) } + if (evaluate_global_setup) { + eval(parse(text = exercise$global_setup), envir = envir) + } + checker_feedback <- NULL if (!isFALSE(exercise$options$exercise.blank.check.code)) { # Check if user code contains blanks exercise$options$exercise.blank.check.code <- exercise$options$exercise.blank.check.code %||% + knitr::opts_chunk$get("exercise.blank.check.code") %||% dput_to_string(exercise_blank_checker) checker_feedback <- try_checker( exercise, "exercise.blank.check.code", - check_code = exercise$options$exercise.blanks %||% "_{3,}", + check_code = exercise$options$exercise.blanks %||% + knitr::opts_chunk$get("exercise.blanks") %||% + "_{3,}", envir_prep = duplicate_env(envir), engine = exercise$engine ) @@ -329,6 +336,7 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { # Check if user code is parsable exercise$options$exercise.parse.check.code <- exercise$options$exercise.parse.check.code %||% + knitr::opts_chunk$get("exercise.parse.check.code") %||% dput_to_string(exercise_parse_checker) checker_feedback <- try_checker( @@ -341,10 +349,6 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { } } - if (evaluate_global_setup) { - eval(parse(text = exercise$global_setup), envir = envir) - } - # Setup a temporary directory for rendering the exercise exercise_dir <- tempfile(pattern = "lnr-ex") dir.create(exercise_dir) diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index a5727edf4..af7999427 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -642,6 +642,27 @@ test_that("evaluate_exercise() returns a message if code contains ___", { expect_match(result$feedback$message, "contains 2 blanks") }) +test_that("setting a different blank for the blank checker", { + ex <- mock_exercise(user_code = '####("test")', exercise.blanks = "#{3,}") + result <- evaluate_exercise(ex, new.env()) + expect_match(result$feedback$message, "contains 1 blank") + + ex <- mock_exercise(user_code = '####(####)', exercise.blanks = "#{3,}") + result <- evaluate_exercise(ex, new.env()) + expect_match(result$feedback$message, "contains 2 blanks") + + ex <- mock_exercise(user_code = '####("####")', exercise.blanks = "#{3,}") + result <- evaluate_exercise(ex, new.env()) + expect_match(result$feedback$message, "contains 2 blanks") + + ex <- mock_exercise( + user_code = '####("test")', + global_setup = 'knitr::opts_chunk$set(exercise.blanks = "#{3,}")' + ) + result <- evaluate_exercise(ex, new.env(), evaluate_global_setup = TRUE) + expect_match(result$feedback$message, "contains 1 blank") +}) + test_that("default message if exercise.blank.error.check is FALSE", { ex <- mock_exercise( user_code = 'print("____")', exercise.blank.check.code = FALSE From 5f5f0fb9f7e2710b53f13518280bfad6d9a1d736 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Tue, 6 Jul 2021 15:58:02 -0700 Subject: [PATCH 13/58] Refactor blank checking --- R/exercise.R | 46 ++++++++++++++-------------------- tests/testthat/test-exercise.R | 28 +++++++++++++++------ 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index e085ee724..10a49d97c 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -309,24 +309,12 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { checker_feedback <- NULL - if (!isFALSE(exercise$options$exercise.blank.check.code)) { - # Check if user code contains blanks - exercise$options$exercise.blank.check.code <- - exercise$options$exercise.blank.check.code %||% - knitr::opts_chunk$get("exercise.blank.check.code") %||% - dput_to_string(exercise_blank_checker) + exercise_blanks <- exercise$options$exercise.blanks %||% + knitr::opts_chunk$get("exercise.blanks") %||% + "_{3,}" - checker_feedback <- try_checker( - exercise, "exercise.blank.check.code", - check_code = exercise$options$exercise.blanks %||% - knitr::opts_chunk$get("exercise.blanks") %||% - "_{3,}", - envir_prep = duplicate_env(envir), - engine = exercise$engine - ) - if (is_exercise_result(checker_feedback)) { - return(checker_feedback) - } + if (isTruthy(exercise_blanks)) { + checker_feedback <- check_blanks(exercise$code, exercise_blanks) } if ( @@ -699,23 +687,27 @@ exercise_code_chunks <- function(chunks) { }, character(1)) } -exercise_blank_checker <- function(label, user_code, check_code, ...) { - n_blanks <- sum(vapply( - gregexpr(check_code, user_code), - function(x) sum(x > 0), - integer(1) - )) +check_blanks <- function(user_code, blank_regex) { + blank_regex <- paste(blank_regex, collapse = "|") - if (n_blanks == 0) { + blanks <- str_match_all(user_code, blank_regex) + + if (!length(blanks)) { return(NULL) } msg <- paste0( - "The exercise contains ", n_blanks, " blank", if (n_blanks != 1L) {"s"}, - ". Please replace the `___` with valid code." + "The exercise contains ", length(blanks), + " blank", if (length(blanks) != 1L) {"s"}, + ". Please replace ", + knitr::combine_words(unique(blanks), before = "`"), + " with valid code." ) - list(message = msg, correct = FALSE, location = "append") + rlang::return_from( + rlang::caller_env(), + exercise_result(list(message = msg, correct = FALSE, location = "append")) + ) } exercise_parse_checker <- function(label, user_code, ...) { diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index af7999427..6b0ae7059 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -632,6 +632,7 @@ test_that("evaluate_exercise() returns a message if code contains ___", { ex <- mock_exercise(user_code = '____("test")') result <- evaluate_exercise(ex, new.env()) expect_match(result$feedback$message, "contains 1 blank") + expect_match(result$feedback$message, "____") ex <- mock_exercise(user_code = '____(____)') result <- evaluate_exercise(ex, new.env()) @@ -643,35 +644,48 @@ test_that("evaluate_exercise() returns a message if code contains ___", { }) test_that("setting a different blank for the blank checker", { - ex <- mock_exercise(user_code = '####("test")', exercise.blanks = "#{3,}") + ex <- mock_exercise(user_code = '####("test")', exercise.blanks = "###") result <- evaluate_exercise(ex, new.env()) expect_match(result$feedback$message, "contains 1 blank") + expect_match(result$feedback$message, "###") - ex <- mock_exercise(user_code = '####(####)', exercise.blanks = "#{3,}") + ex <- mock_exercise(user_code = '####(####)', exercise.blanks = "###") result <- evaluate_exercise(ex, new.env()) expect_match(result$feedback$message, "contains 2 blanks") - ex <- mock_exercise(user_code = '####("####")', exercise.blanks = "#{3,}") + ex <- mock_exercise(user_code = '####("####")', exercise.blanks = "###") result <- evaluate_exercise(ex, new.env()) expect_match(result$feedback$message, "contains 2 blanks") +}) +test_that("setting a different blank for the blank checker in global setup", { ex <- mock_exercise( user_code = '####("test")', - global_setup = 'knitr::opts_chunk$set(exercise.blanks = "#{3,}")' + global_setup = 'knitr::opts_chunk$set(exercise.blanks = "###")' ) result <- evaluate_exercise(ex, new.env(), evaluate_global_setup = TRUE) expect_match(result$feedback$message, "contains 1 blank") }) -test_that("default message if exercise.blank.error.check is FALSE", { +test_that("setting a regex blank for the blank checker", { + ex <- mock_exercise( + user_code = '..function..("..string..")', + exercise.blanks = "\\.\\.\\S+?\\.\\." + ) + result <- evaluate_exercise(ex, new.env(), evaluate_global_setup = TRUE) + expect_match(result$feedback$message, "contains 2 blanks") + expect_match(result$feedback$message, "`..function..` and `..string..`") +}) + +test_that("default message if exercise.blanks is FALSE", { ex <- mock_exercise( - user_code = 'print("____")', exercise.blank.check.code = FALSE + user_code = 'print("____")', exercise.blanks = FALSE ) result <- evaluate_exercise(ex, new.env()) expect_null(result$feedback$message) ex <- mock_exercise( - user_code = '____("test")', exercise.blank.check.code = FALSE + user_code = '____("test")', exercise.blanks = FALSE ) result <- evaluate_exercise(ex, new.env()) expect_match(result$feedback$message, "this might not be valid R code") From 4c023ec8553567d82d784962352939b6618e96e4 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Tue, 6 Jul 2021 16:30:14 -0700 Subject: [PATCH 14/58] Use `as.character(user_code)` in `check_blanks()` to avoid issue with unserialized user code in R < 3.5 --- R/exercise.R | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 10a49d97c..559501651 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -688,9 +688,11 @@ exercise_code_chunks <- function(chunks) { } check_blanks <- function(user_code, blank_regex) { + # Ensure `user_code` is a character vector to avoid issue with unserialized + # code which comes as a list in R < 3.5 + user_code <- as.character(user_code) blank_regex <- paste(blank_regex, collapse = "|") - - blanks <- str_match_all(user_code, blank_regex) + blanks <- str_match_all(user_code, blank_regex) if (!length(blanks)) { return(NULL) From 0ddd0d205bb9def86645c902bfb3823f6728e3df Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 9 Jul 2021 17:22:34 -0400 Subject: [PATCH 15/58] Revert "Use `as.character(user_code)` in `check_blanks()` to avoid issue with unserialized user code in R < 3.5" This reverts commit 4c023ec8553567d82d784962352939b6618e96e4. --- R/exercise.R | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index c36c5d98b..9fe188d54 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -722,11 +722,9 @@ exercise_code_chunks <- function(chunks) { } check_blanks <- function(user_code, blank_regex) { - # Ensure `user_code` is a character vector to avoid issue with unserialized - # code which comes as a list in R < 3.5 - user_code <- as.character(user_code) blank_regex <- paste(blank_regex, collapse = "|") - blanks <- str_match_all(user_code, blank_regex) + + blanks <- str_match_all(user_code, blank_regex) if (!length(blanks)) { return(NULL) From 40685c3236fd3b99140dde67429f56e22b780279 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Wed, 7 Jul 2021 15:16:30 -0700 Subject: [PATCH 16/58] Add support for i18n in blanks error message --- R/exercise.R | 23 ++++++---- R/i18n.R | 10 +++++ R/utils.R | 4 +- data-raw/i18n_translations.yml | 60 +++++++++++++++++++++++++++ inst/internals/i18n_translations.rds | Bin 2095 -> 2317 bytes inst/lib/i18n/tutorial-i18n-init.js | 10 +++++ tests/testthat/test-exercise.R | 2 +- 7 files changed, 98 insertions(+), 11 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 559501651..98e1c9283 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -314,7 +314,7 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { "_{3,}" if (isTruthy(exercise_blanks)) { - checker_feedback <- check_blanks(exercise$code, exercise_blanks) + check_blanks(exercise$code, exercise_blanks) } if ( @@ -698,17 +698,24 @@ check_blanks <- function(user_code, blank_regex) { return(NULL) } - msg <- paste0( - "The exercise contains ", length(blanks), - " blank", if (length(blanks) != 1L) {"s"}, - ". Please replace ", - knitr::combine_words(unique(blanks), before = "`"), - " with valid code." + msg <- paste( + i18n_span( + "text.exercisecontainsblank", opts = list(count = length(blanks)) + ), + i18n_span( + "text.pleasereplaceblank", + opts = list( + count = length(blanks), + blank = i18n_combine_words(unique(blanks), before = "`") + ) + ) ) rlang::return_from( rlang::caller_env(), - exercise_result(list(message = msg, correct = FALSE, location = "append")) + exercise_result( + list(message = HTML(msg), correct = FALSE, location = "append") + ) ) } diff --git a/R/i18n.R b/R/i18n.R index 319b22a9b..cf4bce489 100644 --- a/R/i18n.R +++ b/R/i18n.R @@ -128,6 +128,16 @@ i18n_span <- function(key, ..., opts = NULL) { htmltools::HTML(format(x)) } +i18n_combine_words <- function( + words, and = c("and", "or"), before = "", after = before +) { + and <- match.arg(and) + sep <- "$t(text.listcomma) " + last <- glue::glue(" $t(text.{and}) ") + words <- glue::glue("{before}{words}{after}") + glue::glue_collapse(words, sep = sep, last = last) +} + i18n_translations <- function() { readRDS(system.file("internals", "i18n_translations.rds", package = "learnr")) } diff --git a/R/utils.R b/R/utils.R index 88ff915c3..f5c7b696b 100644 --- a/R/utils.R +++ b/R/utils.R @@ -76,9 +76,9 @@ str_match <- function(x, pattern) { ) } -str_match_all <- function(x, pattern) { +str_match_all <- function(x, pattern, ...) { if_no_match_return_null( - regmatches(x, gregexpr(pattern, x))[[1]] + regmatches(x, gregexpr(pattern, x, ...))[[1]] ) } str_replace <- function(x, pattern, replacement) { diff --git a/data-raw/i18n_translations.yml b/data-raw/i18n_translations.yml index 504dc4bd4..994088fec 100644 --- a/data-raw/i18n_translations.yml +++ b/data-raw/i18n_translations.yml @@ -280,3 +280,63 @@ text: tr: "Sınav" emo: ~ eu: "Galdetegia" + blank: + en: "blank" + fr: ~ + es: "espacio en blanco" # Added by Alex, not a real translator + pt: ~ + tr: ~ + emo: ~ + eu: ~ + blank_plural: + en: "blanks" + fr: ~ + es: "espacios en blanco" # Added by Alex, not a real translator + pt: ~ + tr: ~ + emo: ~ + eu: ~ + exercisecontainsblank: + # {{count}} - the number of blanks detected in the exercise + en: "This exercise contains {{count}} $t(text.blank)." + fr: ~ + es: "Este ejercicio contiene {{count}} $t(text.blank)." # Added by Alex, not a real translator + pt: ~ + tr: ~ + emo: ~ + eu: ~ + pleasereplaceblank: + # {{blank}} - the string representing a blank in the exercise (e.g. "___") + en: "Please replace {{blank}} with valid code." + fr: ~ + es: "Reemplace {{blank}} con código real." # Added by Alex, not a real translator + pt: ~ + tr: ~ + emo: ~ + eu: ~ + and: + en: "and" + fr: "et" # Added by Alex, not a real translator + es: "y" # Added by Alex, not a real translator + pt: "e" # Added by Alex, not a real translator + tr: "ve" # Added by Alex, not a real translator + emo: ~ + eu: "eta" # Added by Alex, not a real translator + or: + en: "or" + fr: "ou" # Added by Alex, not a real translator + es: "o" # Added by Alex, not a real translator + pt: "ou" # Added by Alex, not a real translator + tr: ~ + emo: ~ + eu: ~ + listcomma: + # Some languages (e.g. Chinese, Korean) use a different punctuation mark for + # separating list items + en: ", " + fr: ~ + es: ~ + pt: ~ + tr: ~ + emo: ~ + eu: ~ diff --git a/inst/internals/i18n_translations.rds b/inst/internals/i18n_translations.rds index e411d7e9419052d9fbd9e1909bca18f2756a491c..c28d51ef936e3a43f1890cbbc136602763db3262 100644 GIT binary patch literal 2317 zcmV+o3G((IiwFP!000001MOPvZyZM%znq=qe11FEDXm121}G$vXat}5h+NyEFA+Gl ziQp5O+nwA^Zf|BgyL-u<6{!e81qh--1rkD}5lE3MC|HpqB#@9;^N-{opnpN&ncbPa zy=Tw%+DYR?_=9~n&ph+Y%y=8STA4UAQK?SouZ!^Stob};KC9+a|1}c% zWMv9IYwM99%hF{?{>rQ2g=Q3nQe0Aza6sWAxv;g>X}Y&=T|~N*Z*UPJ*=a2ckZ^uz zv<=SHoK-+kIN_+*IC<;UKDaEkbH4CSzt8qgbT~k?~_oHWsmz!NtKHf z>Kh?dA^CQXDb#Z=XI<+^P{HPF%Tk0~L>SxIji}k>Az2c^je_Bqa)uK%nrLC$a~UL? z(&r9dcY@@&o|5LLHdUW2wI~NvC#!kC3fg7XuZu=_)`W;@&X{J%L(j@4wZqN#Y5o2C zz85L#+4;06uV2#w0lUbzeb}>YTc27?Z98vd)&c)InQzThnZGQQnP6lAj&+{dR30uH zKY7{6#J=%Pn7F`9thbXF%)Jw_K(9G9W26l$4OO3}9;iGr+1n@`pKmQGM*1=$L1ez% zpd!? z+c?B))+mlEE61hb3tWI19*}@Jdh;;-H3=&>Uf`m230IvlRqN%gEhbuAuv@nX1bPU7 zUrK5h9(yI?16;A19u*xVJd+6VV!yhnSDN9b*9d+J$kvu4BN65%PljK_OTW10F&b!m zeUCZ}#wB_%?gkIrq(?pO61{FbaDob4qN)9@Z zcCPhp!JkIWwnx)=7LpX8F+Q|l=Sss=>K=#sw(}kBi4hN&d`lXm=3lh4TEz1MUhor8 z4W{{?1hpYrW+fg^cG;zAevJTD&D@`qO1oC=(lOc=_~}qVDe$NvqemqvcinqGXv|E|R6m^v$?I}yzc<|H)WCjaEh4*zJrKTvITW-U0VR#-R zWU7(r7^sgnRee3)27qW^Z(<}=PZ;|tGxl_u=uEwKCH`s1g4cTbPzSv!rMQfMz>{17 zTVx}Fxi8aa#IeEx5GlX*S+&; zwRH-|SC?7xa|%5EMOd%{S;8_Rr#=W$(iPZyp>J*-h3RgN;Eh=?pBoN1I{cs`OxEgc5Kan4u?(bj*x2e{g`Nxv~DQf35)g zFy&I0Kv9CDHPrpC&jt{9P|#stJqb({N>>u*Svr!?2baq02|sYSZ$e&z4W{C|h16YP zO|WSyYaz&I@^)}=dNLSx259N8GC_1!Xc9`YJBjfiM?*A6L+O~a%VUdLStgoUHjqBo!3r-Jg97sB4m$tQ`SFW>u2fyxF=g*fzw)YZ3KD-wq_9R0lQ8$ zxw80P-ldkZWp;s-MYU@y6g54eb6sKm* z;X-ERUg0UMCb%SLx=gPzegHvEnh|Nn4?I|4;YsBLR5K(xn1j-(&TPPnX%CIA_VA&r z{v46cQ>K~)1nJ}_cW(a>nJULE+4x`HMdJ^8{^A&IvUc#S$r&50WgA~-EW z5fCm-G7bW;MLjptg+YgkOK}8zIf*fqln#9&=K{vDHk@yDSdq3AECgF<)XPw0I?!Q8 zv9^Bi1GD3|1fMl}oe@Pyn4)S0wNED%4bNNh8BYD1uxqIIt-_Ps38tU3o*beJG&7=h n*4{XnHCVl-GFf21`8KGlp~~>>%1lw$#Yg`GZyuWgg)RU9=U;Jp literal 2095 zcmV+~2+;Q*iwFP!000001MOK`ZyZS#p0%B1#_?q=XD zB6vbI-6!M9bXWCOS5Gn%McUO0R)8Q{Sb>BP*$AY_D{fY#2ni%4+We9H0rnRJPIXs} z+tqEm$JteamNxG%a?Bi~CBq zk?rbfeG6_XQlV^qT|%FUB(7pMH${*rCb&ai-d`*>67GUat0Ec@CCF;PqK=@_cLZiP z6_tv7yARUgoGWP8G6F?h_03hm72}EP+xg9;6Ea0sczmOz_{D6?6^L$o6e?JNm zNdvo{CKctIn!wSD;y6N^?bycD`qYk#O6EO`uQ!UVS!xt7>&g+~zy>@UJY!VRT{eG; zv;zxACORq$fmv9aQzR_BlR&KZoU1X^wxudLpm_uuk4^QqYUk%$D-y^+BqUDE!wt%n z&h^Y%qRssVxj=(}z)g@Ii-AN^bS1E~{m1ZHhvmmCm4FV^jnO?^?A%>wA2`CcT}jKR&Ikw zBg=dz+pbQ716!Ul{L^_wTWM`}g|s#Ptdf|q0X04IaZRLHxJC$}~+H$f@s+I4K_&%JbRbsM*f(~CZGRJBJk#?>cV zYIY;tK?l)+HsV04PJrbp150Ig=25$n{#Zf$T3??%kgHHpOne|n2n3S|8l#yAFqi2F zR&?FPASp_O8g9~$$uQlK!cB`WUe&67=);is=V?bsKZlxhN0rijHaMSmS+8`;@*yUc z=XR5?;D$Y{5*XnP?SrSyu+fpV@a6!>kbU%V=T7iu47|zHpnd5&cnSK3cdYX~ABed-hJKOlAc4kH+1;$dG#yZQnFMG0RJ z2rQ}ydtaJE}#i5ph-UG%xYy*s|zS|tH$M2>dj7or7;e$-2BXdWo8X;6b|i_rqza(d{6Uu z*nV)6VF&cf3g@-LBQYhtpZ?A@<48=&=YG{-N<{HgMoD^40@YcY`Slvs&?g)#_4o?@ z3?OE)05ReO2xCIKl|I03B@S~|+qhW}l?#r$jA?jV8MY8E`ZUbZYJxKV6DV^9PZ`;* z1z`nI1~A)WxK1@FKzhFzQVTU=bbk@#8Zi9=O@m8@GR!YsUd~+=UiwLPd#OO0TxL$> z_mcFFMWo4j3nDHsU8MOEH#n0O{O1odlY7hf@UonKK!d&Af8AAkyAm7v+rm#A&0}3% z-i#72an?Ay_m>Epen92kuG>hD2AYLU!6of6qBWHCE~LN2d;4MBX8J+;aG3r*be61f zds$qkT$~?hZ$twcP)GC9>lFP(InloX4QbDrU+Z(r8er`bP_9v{Nbl_J5_?Xgo;-a#IW|9Yf$vy>jujv*R^YR}y9h)> zU*8Zy#!1LxJ=WlYbwr>$1R1j-VFAbhLWmjZ(LqN9)T6!f=>`>tT08cacyHHzi_o;{ z+(u^ULFr|l2CA%PLeP6mA7YY|PC`2A!vHr}df_#V-OM2!k0tq1r#ErOv_Co8s$j5c7;}5Qm=|{hH{ZkVXaP^PgbE3WUIdgh{wXg=>AD$u;tp~DEcORsTQZm-A= z1c0ZBo_;@5a?9N7ASLbK?UoLi!>e^rev9J#vb)=tFot zxV5@50ivR|>m3lpf+>qOobjowqq*zWeBCnpO|%;OeOJG9og9-Z#LgJ8DX Date: Mon, 12 Jul 2021 12:18:14 -0700 Subject: [PATCH 17/58] Refactor parse check --- R/exercise.R | 36 ++++++++------------------- data-raw/i18n_translations.yml | 14 +++++++++++ inst/internals/i18n_translations.rds | Bin 2317 -> 2513 bytes tests/testthat/test-exercise.R | 6 ++--- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 98e1c9283..3fb6b3925 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -319,22 +319,9 @@ evaluate_exercise <- function(exercise, envir, evaluate_global_setup = FALSE) { if ( tolower(exercise$engine) == "r" && - !isFALSE(exercise$options$exercise.parse.check.code) + !isFALSE(exercise$options$exercise.parse.check) ) { - # Check if user code is parsable - exercise$options$exercise.parse.check.code <- - exercise$options$exercise.parse.check.code %||% - knitr::opts_chunk$get("exercise.parse.check.code") %||% - dput_to_string(exercise_parse_checker) - - checker_feedback <- try_checker( - exercise, "exercise.parse.check.code", - envir_prep = duplicate_env(envir), - engine = exercise$engine - ) - if (is_exercise_result(checker_feedback)) { - return(checker_feedback) - } + check_parsable(exercise$code) } # Setup a temporary directory for rendering the exercise @@ -719,20 +706,19 @@ check_blanks <- function(user_code, blank_regex) { ) } -exercise_parse_checker <- function(label, user_code, ...) { +check_parsable <- function(user_code) { error <- rlang::catch_cnd(parse(text = user_code), "error") if (is.null(error)) {return(NULL)} - msg <- paste( - "It looks like this might not be valid R code.", - "R cannot determine how to turn your text into a complete command.", - "You may have forgotten to fill in a blank,", - "to remove an underscore, to include a comma between arguments,", - "or to close an opening `\"`, `'`, `(`, or `{{`", - "with a matching `\"`, `'`, `)`, or `}}`." + rlang::return_from( + rlang::caller_env(), + exercise_result( + list( + message = HTML(i18n_span("text.unparsable")), + correct = FALSE, type = "error", location = "append" + ) + ) ) - - list(message = msg, correct = FALSE, type = "error", location = "append") } exercise_result_timeout <- function() { diff --git a/data-raw/i18n_translations.yml b/data-raw/i18n_translations.yml index 994088fec..2b8be5264 100644 --- a/data-raw/i18n_translations.yml +++ b/data-raw/i18n_translations.yml @@ -314,6 +314,20 @@ text: tr: ~ emo: ~ eu: ~ + unparsable: + en: > + It looks like this might not be valid R code. + R cannot determine how to turn your text into a complete command. + You may have forgotten to fill in a blank, + to remove an underscore, to include a comma between arguments, + or to close an opening `\"`, `'`, `(` or `{` + with a matching `\"`, `'`, `)` or `}`. + fr: ~ + es: ~ + pt: ~ + tr: ~ + emo: ~ + eu: ~ and: en: "and" fr: "et" # Added by Alex, not a real translator diff --git a/inst/internals/i18n_translations.rds b/inst/internals/i18n_translations.rds index c28d51ef936e3a43f1890cbbc136602763db3262..9c868a52a6055d77ccdfa13d22f64d333701afd5 100644 GIT binary patch literal 2513 zcmV;?2`=^@iwFP!000001MOP>ZyZMzznq=q{N1ioT8SbJrI1*1E%=EaQR-MUZ3K>O zBKi}a+nwA^Zg*xoJA2NZ6{!e81qh--1rkD}5lE3M3Ph2jN+2Pz<{!yFK>vcko86te zy|-t3?IcYi{DZx_Z{EC_dEY+o?cVptE0s#MGIDC9QXMh3^YHIVj!@s7U9g+{cKh87~?OkZdd zhOwNn@38Bd3=uZ9s#u%LC>%CeTbEv>xfF;#-(M`uIPijV%QEN?Ey=RSgSw>3MGWKX znkr4c+hz)loXJTyJQ7&2_}a1*nu`eQc4|GU`&^UrBHStnekmt7VWSBbmOb55$%YKL zgZG_ia@a^o@najRL(VrS2d$1)^L7=~XV$Ls8S!-HBH80)_Nck`EN#>nJiDJT_df`{ zNKwzOXGnSenjr`{MgBQ}GuvltQ>#<^oEI|XfPI~wZL_6%_Ohz%5e7EkSmR}f%B#!z zPo6fgaFAI?Cnm@iHpj^mrr(WNX!e|n$*8lIs#S-k7N|VTqIa&ef4+5IG18F{38U<0 ziwbRgJvl8g$Nd&LM?H_Q>r6R3WF%0sp;#D_Ee_;5Bk;fqQTpOLAk2rO5l8xg$223l ziCw&Ag<`+5betMqeY;QX<5<0hdGJOGWePPvOG&K4R zJnAr*m+-;7Ev}oSO+D@svu`x<#NtIwJSkft@py}+3iElRsfm!IXo~sZSA+@@374tR0ShJrTsU6jvUCX_rZBz5Kn_)n$Y%f)i$p5J=y) zWlEqiTLR(9W9twrOlyVDbzp&#YU#{!6SfP}^EyJJW_vuDs*kqR_*%RJ3ZflzhyhnU zu75t~`e&kyccxam9REzS@b$JSkzq6iXn-Ifc#538UvNr1y_bjbT<)#1{@92WNk9%j+{Oc5&0hifQ!YNcmUx5uf5GAZv z76A@B<`~vGs#IAtPz#ZMk%Z?CDT}HMhfP&RC=Mo`4~#^}_0)hW^`F_5Ys`s2VZ@2= zxx68f*uy~8$yI<~iioEY;voTQW}P=8Zt#W#Lu5`TGKqmL#U+lpF@s4CkMA7;l)hj< z36w!S5=aPFKqKZA0%B~9alsv6L<5Hbqr&9b711_pL-+?K`QTHRY?`Mwm_I}}ndXwg z&}#Q+E6iPy9vlp6Uoahrp~r~ylo4s>UrgEwZo1KFwrA8O9Ez_zvnBj$t zA>5PW8!I6sxV~1CV=%!=bnxl%zPiT1cBpv=DK?iwDxn^R<`W%EkSKg-o(~{>QqAVG z2GG|Z8$gdMfQ>l~28Eef160{6w{_)h#se2jguIrH`mn*$1E zF(vVB@NN_RiC(WlF8WwN#;Y8d2_FMg&*Y$59s#K{({9A~AnVJ$lr@{FSrVNx9(I`w zI5rZDCPKEUpQ8RTT0cwuXpM>8zXbpm9#y+{5&r=I zHN7r{qAgC0go51Z_&35~dyUrO_v8EB_#Zx6vPAbdvr2_L+cDY*I@F^`^4uE~^hE_R zSC9G>f9EOnIrjpCm?ydSA=#mpfnMLeOQ2fqct=E}NvoN+rNHK#v^pI!R`zr825Tuu zTNj|n?bwgH$2jz~aH#)n>OlWY8NO#3>Xw0UXklEwdkX-p>zWSD=Yse=G|vW>KHjU&7tH_$^M&io=P_(!Ii4SOaiL%=pag zF}@E@PU;b<$M-$hVBt;W2xK!vIv9h}rB1EGj%gdsuC{T}HU1od&gTp@{ht@4Djogo z*3BOwQRT3uH~&|+(fotnKkg4NAn})fMN>*NFfi@cKS#8Y2RHwMBn|sHg=PNzkBc&Y z@>?Wek6-p!8GptyMjreD>!&1M!0I3VaF0*bPa=)~U#98WelF9q!=FAxw3iw}-dzdIj1c<&bw34ceHu9SZ6M-!H@PjVb^joP!A^q%rB1!e$i7zK>vEvmSF zlp?xzi8dmkySVt7-^X~sokPlnn`opu8zFug=2J$qi&na7~i zXqlHj1+`4R!IYwPXqjo3qp?y|^jj3>7u7wx0!Gx`NKjJeF7>eNvfp5=>ZqoLT70^v zw_&4YOKGD3uAslCsd|n}>1U&qKG;9MX-(IpPzVd!~P|uA_ zVlb}aQtSa=OMFZvsl!y{T);fmG{0v9w<1j`m5!Vomei2Uf>l3BH)| zwFVd=V2Z32^n5bOXn5U{?{LPy4X1{D-zdDv9bx7@>v0iXfSCcd)3)PaR$%q2>d``b b7g(pRYSq)PFMEt8FFyPqkSR(o(=Px3Vf@9! literal 2317 zcmV+o3G((IiwFP!000001MOPvZyZM%znq=qe11FEDXm121}G$vXat}5h+NyEFA+Gl ziQp5O+nwA^Zf|BgyL-u<6{!e81qh--1rkD}5lE3MC|HpqB#@9;^N-{opnpN&ncbPa zy=Tw%+DYR?_=9~n&ph+Y%y=8STA4UAQK?SouZ!^Stob};KC9+a|1}c% zWMv9IYwM99%hF{?{>rQ2g=Q3nQe0Aza6sWAxv;g>X}Y&=T|~N*Z*UPJ*=a2ckZ^uz zv<=SHoK-+kIN_+*IC<;UKDaEkbH4CSzt8qgbT~k?~_oHWsmz!NtKHf z>Kh?dA^CQXDb#Z=XI<+^P{HPF%Tk0~L>SxIji}k>Az2c^je_Bqa)uK%nrLC$a~UL? z(&r9dcY@@&o|5LLHdUW2wI~NvC#!kC3fg7XuZu=_)`W;@&X{J%L(j@4wZqN#Y5o2C zz85L#+4;06uV2#w0lUbzeb}>YTc27?Z98vd)&c)InQzThnZGQQnP6lAj&+{dR30uH zKY7{6#J=%Pn7F`9thbXF%)Jw_K(9G9W26l$4OO3}9;iGr+1n@`pKmQGM*1=$L1ez% zpd!? z+c?B))+mlEE61hb3tWI19*}@Jdh;;-H3=&>Uf`m230IvlRqN%gEhbuAuv@nX1bPU7 zUrK5h9(yI?16;A19u*xVJd+6VV!yhnSDN9b*9d+J$kvu4BN65%PljK_OTW10F&b!m zeUCZ}#wB_%?gkIrq(?pO61{FbaDob4qN)9@Z zcCPhp!JkIWwnx)=7LpX8F+Q|l=Sss=>K=#sw(}kBi4hN&d`lXm=3lh4TEz1MUhor8 z4W{{?1hpYrW+fg^cG;zAevJTD&D@`qO1oC=(lOc=_~}qVDe$NvqemqvcinqGXv|E|R6m^v$?I}yzc<|H)WCjaEh4*zJrKTvITW-U0VR#-R zWU7(r7^sgnRee3)27qW^Z(<}=PZ;|tGxl_u=uEwKCH`s1g4cTbPzSv!rMQfMz>{17 zTVx}Fxi8aa#IeEx5GlX*S+&; zwRH-|SC?7xa|%5EMOd%{S;8_Rr#=W$(iPZyp>J*-h3RgN;Eh=?pBoN1I{cs`OxEgc5Kan4u?(bj*x2e{g`Nxv~DQf35)g zFy&I0Kv9CDHPrpC&jt{9P|#stJqb({N>>u*Svr!?2baq02|sYSZ$e&z4W{C|h16YP zO|WSyYaz&I@^)}=dNLSx259N8GC_1!Xc9`YJBjfiM?*A6L+O~a%VUdLStgoUHjqBo!3r-Jg97sB4m$tQ`SFW>u2fyxF=g*fzw)YZ3KD-wq_9R0lQ8$ zxw80P-ldkZWp;s-MYU@y6g54eb6sKm* z;X-ERUg0UMCb%SLx=gPzegHvEnh|Nn4?I|4;YsBLR5K(xn1j-(&TPPnX%CIA_VA&r z{v46cQ>K~)1nJ}_cW(a>nJULE+4x`HMdJ^8{^A&IvUc#S$r&50WgA~-EW z5fCm-G7bW;MLjptg+YgkOK}8zIf*fqln#9&=K{vDHk@yDSdq3AECgF<)XPw0I?!Q8 zv9^Bi1GD3|1fMl}oe@Pyn4)S0wNED%4bNNh8BYD1uxqIIt-_Ps38tU3o*beJG&7=h n*4{XnHCVl-GFf21`8KGlp~~>>%1lw$#Yg`GZyuWgg)RU9=U;Jp diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index eff243875..7f56fc2e8 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -705,15 +705,15 @@ test_that("evaluate_exercise() returns a message if code is unparsable", { expect_match(result$feedback$message, "this might not be valid R code") }) -test_that("default error message if exercise.parse.error.check is FALSE", { +test_that("default error message if exercise.parse.check is FALSE", { ex <- mock_exercise( - user_code = 'print("test"', exercise.parse.check.code = FALSE + user_code = 'print("test"', exercise.parse.check = FALSE ) result <- evaluate_exercise(ex, new.env()) expect_match(result$error_message, "unexpected end of input") ex <- mock_exercise( - user_code = 'mean(1:10 na.rm = TRUE)', exercise.parse.check.code = FALSE + user_code = 'mean(1:10 na.rm = TRUE)', exercise.parse.check = FALSE ) result <- evaluate_exercise(ex, new.env()) expect_match(result$error_message, "unexpected symbol") From de7198cbb65dd4facfe43014330a9adaa93e8283 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Mon, 12 Jul 2021 12:28:21 -0700 Subject: [PATCH 18/58] Update tests for i18n error messages --- tests/testthat/test-exercise.R | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index 7f56fc2e8..23f351e85 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -630,51 +630,53 @@ test_that("env vars are protected from both user and author modification", { test_that("evaluate_exercise() returns a message if code contains ___", { ex <- mock_exercise(user_code = '____("test")') - z <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, "contains 1 blank") + result <- evaluate_exercise(ex, new.env()) + expect_match(result$feedback$message, ""count":1") expect_match(result$feedback$message, "____") ex <- mock_exercise(user_code = '____(____)') result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, "contains 2 blanks") + expect_match(result$feedback$message, ""count":2") ex <- mock_exercise(user_code = '____("____")') result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, "contains 2 blanks") + expect_match(result$feedback$message, ""count":2") }) test_that("setting a different blank for the blank checker", { ex <- mock_exercise(user_code = '####("test")', exercise.blanks = "###") result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, "contains 1 blank") + expect_match(result$feedback$message, ""count":1") expect_match(result$feedback$message, "###") ex <- mock_exercise(user_code = '####(####)', exercise.blanks = "###") result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, "contains 2 blanks") + expect_match(result$feedback$message, ""count":2") ex <- mock_exercise(user_code = '####("####")', exercise.blanks = "###") result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, "contains 2 blanks") + expect_match(result$feedback$message, ""count":2") }) test_that("setting a different blank for the blank checker in global setup", { - ex <- mock_exercise( + ex <- mock_exercise( user_code = '####("test")', global_setup = 'knitr::opts_chunk$set(exercise.blanks = "###")' ) result <- evaluate_exercise(ex, new.env(), evaluate_global_setup = TRUE) - expect_match(result$feedback$message, "contains 1 blank") + expect_match(result$feedback$message, ""count":1") }) test_that("setting a regex blank for the blank checker", { - ex <- mock_exercise( + ex <- mock_exercise( user_code = '..function..("..string..")', exercise.blanks = "\\.\\.\\S+?\\.\\." ) result <- evaluate_exercise(ex, new.env(), evaluate_global_setup = TRUE) - expect_match(result$feedback$message, "contains 2 blanks") - expect_match(result$feedback$message, "`..function..` and `..string..`") + expect_match(result$feedback$message, ""count":2") + expect_match( + result$feedback$message, "`..function..` \\$t\\(text.and\\) `..string..`" + ) }) test_that("default message if exercise.blanks is FALSE", { @@ -688,21 +690,21 @@ test_that("default message if exercise.blanks is FALSE", { user_code = '____("test")', exercise.blanks = FALSE ) result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, "this might not be valid R code") + expect_match(result$feedback$message, "text.unparsable") }) test_that("evaluate_exercise() returns a message if code is unparsable", { ex <- mock_exercise(user_code = 'print("test"') result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, "this might not be valid R code") + expect_match(result$feedback$message, "text.unparsable") ex <- mock_exercise(user_code = 'print("test)') result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, "this might not be valid R code") + expect_match(result$feedback$message, "text.unparsable") ex <- mock_exercise(user_code = 'mean(1:10 na.rm = TRUE)') result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, "this might not be valid R code") + expect_match(result$feedback$message, "text.unparsable") }) test_that("default error message if exercise.parse.check is FALSE", { From c4ef322aefc6cc9558e5d058be486a935d09a2d4 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Mon, 12 Jul 2021 12:45:09 -0700 Subject: [PATCH 19/58] Simplify `try_checker()` calls --- R/exercise.R | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index a7fe965ab..9d1ccd84c 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -327,8 +327,7 @@ evaluate_exercise <- function( eval(parse(text = exercise$global_setup), envir = envir) } - checker_feedback <- NULL - + # Check if user code has unfilled blanks; if it does, early return exercise_blanks <- exercise$options$exercise.blanks %||% knitr::opts_chunk$get("exercise.blanks") %||% "_{3,}" @@ -337,6 +336,7 @@ evaluate_exercise <- function( check_blanks(exercise$code, exercise_blanks) } + # Check if user code is parsable; if not, early return if ( tolower(exercise$engine) == "r" && !isFALSE(exercise$options$exercise.parse.check) @@ -344,20 +344,15 @@ evaluate_exercise <- function( check_parsable(exercise$code) } + checker_feedback <- NULL + # Check the code pre-evaluation, if code_check is provided if (nzchar(exercise$code_check)) { checker_feedback <- try_checker( exercise, "exercise.checker", check_code = exercise$code_check, - envir_result = NULL, - evaluate_result = NULL, - envir_prep = duplicate_env(envir), - last_value = NULL, - engine = exercise$engine + envir_prep = duplicate_env(envir) ) - if (is_exercise_result(checker_feedback)) { - return(checker_feedback) - } } # Setup a temporary directory for rendering the exercise @@ -382,17 +377,12 @@ evaluate_exercise <- function( envir_result = err_render$envir_result, evaluate_result = err_render$evaluate_result, envir_prep = err_render$envir_prep, - last_value = err_render, - engine = exercise$engine + last_value = err_render ) - if (is_exercise_result(checker_feedback)) { - return(checker_feedback) - } } exercise_result_error(err_render$error_message) } ) - if (is_exercise_result(rmd_results)) { return(rmd_results) } @@ -405,8 +395,7 @@ evaluate_exercise <- function( envir_result = rmd_results$envir_result, evaluate_result = rmd_results$evaluate_result, envir_prep = rmd_results$envir_prep, - last_value = rmd_results$last_value, - engine = exercise$engine + last_value = rmd_results$last_value ) } @@ -420,7 +409,7 @@ evaluate_exercise <- function( try_checker <- function(exercise, name, check_code = NULL, envir_result = NULL, evaluate_result = NULL, envir_prep, last_value = NULL, - engine) { + engine = exercise$engine) { checker_func <- tryCatch( get_checker_func(exercise, name, envir_prep), error = function(e) { @@ -430,7 +419,7 @@ try_checker <- function(exercise, name, check_code = NULL, envir_result = NULL, ) # If retrieving checker_func fails, return an error result if (is_error_result(checker_func)) { - return(checker_func) + rlang::return_from(rlang::caller_env(), checker_func) } checker_args <- names(formals(checker_func)) args <- list( @@ -452,7 +441,7 @@ try_checker <- function(exercise, name, check_code = NULL, envir_result = NULL, name, paste(missing_args, collapse = "', '") ) message(msg) - return(exercise_result_error(msg)) + rlang::return_from(rlang::caller_env(), exercise_result_error(msg)) } # Call the check function @@ -466,11 +455,11 @@ try_checker <- function(exercise, name, check_code = NULL, envir_result = NULL, ) # If checker code fails, return an error result if (is_error_result(feedback)) { - return(feedback) + rlang::return_from(rlang::caller_env(), feedback) } # If checker doesn't return anything, there's no exercise result to return if (length(feedback)) { - exercise_result(feedback) + rlang::return_from(rlang::caller_env(), exercise_result(feedback)) } else { feedback } From d765b69b849c96596f821728b6fb3bfc2507b7af Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Mon, 12 Jul 2021 12:53:10 -0700 Subject: [PATCH 20/58] Refactor `i18n_combine_words()` to not use `glue` --- R/i18n.R | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/R/i18n.R b/R/i18n.R index cf4bce489..c6c0a6f79 100644 --- a/R/i18n.R +++ b/R/i18n.R @@ -131,11 +131,13 @@ i18n_span <- function(key, ..., opts = NULL) { i18n_combine_words <- function( words, and = c("and", "or"), before = "", after = before ) { - and <- match.arg(and) - sep <- "$t(text.listcomma) " - last <- glue::glue(" $t(text.{and}) ") - words <- glue::glue("{before}{words}{after}") - glue::glue_collapse(words, sep = sep, last = last) + and <- match.arg(and) + last <- paste0(" $t(text.", and, ") ") + knitr::combine_words( + words, + sep = "$t(text.listcomma) ", + last = last, before = before, after = after, oxford_comma = FALSE + ) } i18n_translations <- function() { From ff4c9a021118c401a9ef2a6a2b9a063132d086b6 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Mon, 12 Jul 2021 12:57:34 -0700 Subject: [PATCH 21/58] Fix arguments to `knitr::combine_words()` --- R/i18n.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/i18n.R b/R/i18n.R index c6c0a6f79..b89a07b69 100644 --- a/R/i18n.R +++ b/R/i18n.R @@ -131,12 +131,12 @@ i18n_span <- function(key, ..., opts = NULL) { i18n_combine_words <- function( words, and = c("and", "or"), before = "", after = before ) { - and <- match.arg(and) - last <- paste0(" $t(text.", and, ") ") + and <- match.arg(and) + and <- paste0(" $t(text.", and, ") ") knitr::combine_words( words, sep = "$t(text.listcomma) ", - last = last, before = before, after = after, oxford_comma = FALSE + and = and, before = before, after = after, oxford_comma = FALSE ) } From c55d8ab846b7dd33f0565c40910f1623292fefe0 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Mon, 12 Jul 2021 15:42:35 -0700 Subject: [PATCH 22/58] Include error message in parse check results --- R/exercise.R | 5 +++-- tests/testthat/test-exercise.R | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 9d1ccd84c..e30cbd6a5 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -733,10 +733,11 @@ check_parsable <- function(user_code) { rlang::return_from( rlang::caller_env(), - exercise_result( + exercise_result_error( + error$message, list( message = HTML(i18n_span("text.unparsable")), - correct = FALSE, type = "error", location = "append" + correct = FALSE, location = "append" ) ) ) diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index 945ea3f48..4fb9264b8 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -856,14 +856,17 @@ test_that("evaluate_exercise() returns a message if code is unparsable", { ex <- mock_exercise(user_code = 'print("test"') result <- evaluate_exercise(ex, new.env()) expect_match(result$feedback$message, "text.unparsable") + expect_match(result$error_message, "unexpected end of input") ex <- mock_exercise(user_code = 'print("test)') result <- evaluate_exercise(ex, new.env()) expect_match(result$feedback$message, "text.unparsable") + expect_match(result$error_message, "unexpected INCOMPLETE_STRING") ex <- mock_exercise(user_code = 'mean(1:10 na.rm = TRUE)') result <- evaluate_exercise(ex, new.env()) expect_match(result$feedback$message, "text.unparsable") + expect_match(result$error_message, "unexpected symbol") }) test_that("default error message if exercise.parse.check is FALSE", { From 07a57e5ed4d5418b38495f542f81cc7391ecfad0 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Tue, 13 Jul 2021 10:14:19 -0700 Subject: [PATCH 23/58] Add support for Oxford commas in `i18n_combine_words()` in languages that use Oxford commas (i.e. only English) --- R/i18n.R | 15 +++++++++++---- data-raw/i18n_translations.yml | 8 ++++++++ inst/internals/i18n_translations.rds | Bin 2513 -> 2532 bytes 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/R/i18n.R b/R/i18n.R index b89a07b69..8ad3fe141 100644 --- a/R/i18n.R +++ b/R/i18n.R @@ -129,14 +129,21 @@ i18n_span <- function(key, ..., opts = NULL) { } i18n_combine_words <- function( - words, and = c("and", "or"), before = "", after = before + words, and = c("and", "or"), before = "", after = before, oxford_comma = TRUE ) { - and <- match.arg(and) - and <- paste0(" $t(text.", and, ") ") + and <- match.arg(and) + and <- paste0(" $t(text.", and, ") ") + words <- paste0(before, words, after) + + n <- length(words) + if (oxford_comma && n > 2) { + words[n - 1] <- paste0(words[n - 1], "$t(text.oxfordcomma)") + } + knitr::combine_words( words, sep = "$t(text.listcomma) ", - and = and, before = before, after = after, oxford_comma = FALSE + and = and, oxford_comma = FALSE ) } diff --git a/data-raw/i18n_translations.yml b/data-raw/i18n_translations.yml index 2b8be5264..723b00e5f 100644 --- a/data-raw/i18n_translations.yml +++ b/data-raw/i18n_translations.yml @@ -354,3 +354,11 @@ text: tr: ~ emo: ~ eu: ~ + oxfordcomma: + en: "," + fr: "" + es: "" + pt: "" + tr: "" + emo: "" + eu: "" diff --git a/inst/internals/i18n_translations.rds b/inst/internals/i18n_translations.rds index 9c868a52a6055d77ccdfa13d22f64d333701afd5..4e2099a0273db83569c78fe458cafd9a7952070a 100644 GIT binary patch literal 2532 zcmVZyZMzKcAh%=WjX3DXm12hEhl(xe@%tkH~c*nl=K* zHWB>^&+Sg`Cbv7Yot-`B&aza5paKL@p#ljZ(g>s|6$PwFQ6-R&So4qMAE19h;LYyN z-QL@Cymp!<5&prqyKmlmGxNTE-t7AOQN5MxX_4p%+NwaJULL> zf?=$u_8oRZlM%{hR}|}T8HJ+;C)cDGX)Xn#FAOFNGY&lA{IU$XL`$;l@t`58auLJS znx;yV?{=6%Bd2rP4UYsCtbTG?3e81?Z9BUbHGHngk_a~oieJnrPS|L|g>}#MO|mWn z?%;JNmK-%wQhjY*b;(kbaVe9`EPGFtj?Xuj6eC?3kub_` zHmT4i)|1^5v)ylzv()nlyTO#hLq-B6n~H@Y+2lZOFai&(kfkrY4a$7j8gZl_cuX^* zTR6lgtx_CUmX1rqD_npY9+HqbX7e!pH3=)vyud~CJgzz;s@6+eTTC>$VB6aS0$qc_ z&nLAD%U+3i4_B;)M@1VEk0(UD*srgfmG~vav`u&X}LG8-{6PYa_K1g@tLSg_zEzOxqjhr;yRdVMB;il{jsxiv&@|7Sjqi z_TOya)LwODiYf*sH3?_&4jxUWSi2UIBV)7A!!>ahyaEo+62eAquFlrdSa)b(2j4Ma zJrvGa@k$#{uCW4A^xLuGX07mJ8x}8zEKoK2K)q!%a1{ z8s7y2(XQFVP^cdDNS||$G*c!zQ?Ff#f2LXZTF11}Fq(omz!VTX$ra#PUlMwy%e3Di znY@GQNDbnd4j}%J^y0fp;;{JARioNv9o8f68QOpXn1XD4R~N#4$~%*mWT$XoaG7m4 zoI>aH6r`bpZK^UtlnTm=ndbvWM}ph4 zfn{lk)h}LWP6Re2PJGYhO^MVV1kH}E0BBQ0Jf08_h+!w!cr)S#l1MN@=Uk$b7-Umi z;p%@X0j{K#Nz7>sWksE5&e ztcwYFT@{qh_2&a?pVX1{Il%B6pBaWvT!4ciq8&4|(g>ceX%@~*_ik_i8@H%%)+l@= ze8+dx;4)xQ<;0Vy4rBmphfPMuUy5#T)2o8YGAlgHA zJ$X^e;yZblT2z<)W2hw87&ou<4UQ*(&fUTp^z>Ef#2+Px&=H`X^32Heco~132laGp zPKLQb4C-GN8Kfr(|Kt&U)bVo+x4}oJ&&ub7Tnq7g)Z5+p_bt7f~uzlRnz6;GiCUmWoTFi!r>s~^4(j2 zaXr_Z^L#Fd&qMQUa0~K?ux5{`koO4pnDUs!@34)rD6-NEmI zCapSL$SmC}yzVs^nB18myRhkaV?! zr>LOdpMLIe>1m?TJhJ-4r?+nY2&pOuKYjUM-A3~Jz36;20)fU~{uPOoI0k{(uYZnc z;}35B1!)@f?;n==_dhP`{K;>Tf<68d$?Ets)-nFz57<5>Aq8gt;D^ZD^G~qS|6f+} z+M`0I?}tBqfM_p1_}z#9`Q#?~^w#@7g`Yd`KX~^SkO_aU6lzt;=Rr1M9cA163JR6l zvs)LRjdncS6d)F(fV8|#71xi`icemq%}D4TZXx%g?^7r1OOaBnEr~Z;eEtnHE}DBX zOkQlzUcdyVm*~gqORB*<1`SKwyj?1oW#)CJ6m7uSoc1^xE7gm?NkP77?)pV=qV7e4 zk_LCFhjo|z9%M~NH7#`XbA7vw8ZFz98v}49Nk~)k95v+6Mnk@Te16lqu0^4p7PQ5Q z8}%aSxuFOMmnInp0obIT8=1mjLdB&x0=|~Sm`X~A>Fc=wIo2}&q6c9`T2e3#x{%aM zP-NOLkV&zY`342E<1ZFp%=p?vjF2!z)(Uz)mQ*x6Z%GuH`ZnwuAfQ=zWjxLbFLWzM ut#~;k8e%(Pk2cIItX@-nT4?VA8~0VM`Y!NgpV5@ghyMfEiEQ@CFaQ8m0L`ud literal 2513 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 b7g(pRYSq)PFMEt8FFyPqkSR(o(=Px3Vf@9! From a8f08958ae08ebf2a1b5abc9d3ab9d1a9456ed41 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes <44556601+rossellhayes@users.noreply.github.com> Date: Tue, 13 Jul 2021 11:57:44 -0700 Subject: [PATCH 24/58] Namespace `isTruthy()` Co-authored-by: Garrick Aden-Buie --- R/exercise.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/exercise.R b/R/exercise.R index e30cbd6a5..aa265fdca 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -332,7 +332,7 @@ evaluate_exercise <- function( knitr::opts_chunk$get("exercise.blanks") %||% "_{3,}" - if (isTruthy(exercise_blanks)) { + if (shiny::isTruthy(exercise_blanks)) { check_blanks(exercise$code, exercise_blanks) } From 237cbce31b3f199dc49daa2a0f58c87b63a6f6bf Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Tue, 13 Jul 2021 11:59:47 -0700 Subject: [PATCH 25/58] Use `sprintf()` in `i18n_combine_words()` --- R/i18n.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/i18n.R b/R/i18n.R index 8ad3fe141..a79c0425d 100644 --- a/R/i18n.R +++ b/R/i18n.R @@ -132,7 +132,7 @@ i18n_combine_words <- function( words, and = c("and", "or"), before = "", after = before, oxford_comma = TRUE ) { and <- match.arg(and) - and <- paste0(" $t(text.", and, ") ") + and <- sprintf(" $t(text.%s) ", and) words <- paste0(before, words, after) n <- length(words) From afe015d40b8e03120521e7a47c755cf64a1244d3 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Tue, 13 Jul 2021 13:10:05 -0700 Subject: [PATCH 26/58] Make `name = "exercise.checker"` default in `try_checker()` --- R/exercise.R | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index aa265fdca..6ebd72b56 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -349,7 +349,7 @@ evaluate_exercise <- function( # Check the code pre-evaluation, if code_check is provided if (nzchar(exercise$code_check)) { checker_feedback <- try_checker( - exercise, "exercise.checker", + exercise, check_code = exercise$code_check, envir_prep = duplicate_env(envir) ) @@ -372,7 +372,7 @@ evaluate_exercise <- function( # Check the error thrown by the submitted code when there's error # checking: the exercise could be to throw an error! checker_feedback <- try_checker( - exercise, "exercise.checker", + exercise, check_code = exercise$error_check, envir_result = err_render$envir_result, evaluate_result = err_render$evaluate_result, @@ -390,7 +390,7 @@ evaluate_exercise <- function( # Run the checker post-evaluation (for checking code results) if (nzchar(exercise$check)) { checker_feedback <- try_checker( - exercise, "exercise.checker", + exercise, check_code = exercise$check, envir_result = rmd_results$envir_result, evaluate_result = rmd_results$evaluate_result, @@ -407,13 +407,15 @@ evaluate_exercise <- function( } -try_checker <- function(exercise, name, check_code = NULL, envir_result = NULL, - evaluate_result = NULL, envir_prep, last_value = NULL, - engine = exercise$engine) { +try_checker <- function( + exercise, name = "exercise.checker", check_code = NULL, envir_result = NULL, + evaluate_result = NULL, envir_prep, last_value = NULL, + engine = exercise$engine +) { checker_func <- tryCatch( get_checker_func(exercise, name, envir_prep), error = function(e) { - message("Error occurred while retrieving 'exercise.checker'. Error:\n", e) + message("Error occurred while retrieving '", name, "'. Error:\n", e) exercise_result_error(e$message) } ) From bde61e578ea7b793d34df8bd52d5e01faee7d403 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 15 Jul 2021 17:50:04 -0400 Subject: [PATCH 27/58] Restructure flow of blank check results 1. Check for blanks first, store feedback and disable regular checking if any 2. Check for parse error, if an error: return blanks %||% parse feedback 3. Check for code feedback, if any: return blanks %||% code feedback 4. Evaluate the exercise and the rest of checking, using blank feedback if we found some --- R/exercise.R | 71 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 6ebd72b56..57e9c3dff 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -332,26 +332,53 @@ evaluate_exercise <- function( knitr::opts_chunk$get("exercise.blanks") %||% "_{3,}" + blank_feedback <- NULL if (shiny::isTruthy(exercise_blanks)) { - check_blanks(exercise$code, exercise_blanks) + blank_feedback <- check_blanks(exercise$code, exercise_blanks) } + if (!is.null(blank_feedback)) { + # Don't need to do exercise checking, since it will be replaced with + # feedback about the detected blanks + exercise$check <- "" + } + + return_if_exercise_result <- function(res) { + # early return if the we've received an exercise result, + # but also replace the feedback with the blank feedback if any were found + if (!is_exercise_result(res)) { + return(NULL) + } + + if (!is.null(blank_feedback$feedback)) { + res$feedback <- blank_feedback$feedback + } + + rlang::return_from(rlang::caller_env(), res) + } + + # Check if user code is parsable; if not, early return if ( tolower(exercise$engine) == "r" && !isFALSE(exercise$options$exercise.parse.check) ) { - check_parsable(exercise$code) + return_if_exercise_result(check_parsable(exercise$code)) } checker_feedback <- NULL # Check the code pre-evaluation, if code_check is provided if (nzchar(exercise$code_check)) { - checker_feedback <- try_checker( - exercise, - check_code = exercise$code_check, - envir_prep = duplicate_env(envir) + # treat the blank check like a code check, if blanks were detected + return_if_exercise_result(blank_feedback) + + return_if_exercise_result( + try_checker( + exercise, + check_code = exercise$code_check, + envir_prep = duplicate_env(envir) + ) ) } @@ -379,13 +406,13 @@ evaluate_exercise <- function( envir_prep = err_render$envir_prep, last_value = err_render ) + return(checker_feedback) } exercise_result_error(err_render$error_message) } ) - if (is_exercise_result(rmd_results)) { - return(rmd_results) - } + + return_if_exercise_result(rmd_results) # Run the checker post-evaluation (for checking code results) if (nzchar(exercise$check)) { @@ -401,7 +428,7 @@ evaluate_exercise <- function( # Return checker feedback (if any) with the exercise results exercise_result( - feedback = checker_feedback$feedback, + feedback = checker_feedback$feedback %||% blank_feedback$feedback, html_output = rmd_results$html_output ) } @@ -461,7 +488,7 @@ try_checker <- function( } # If checker doesn't return anything, there's no exercise result to return if (length(feedback)) { - rlang::return_from(rlang::caller_env(), exercise_result(feedback)) + exercise_result(feedback) } else { feedback } @@ -721,27 +748,17 @@ check_blanks <- function(user_code, blank_regex) { ) ) - rlang::return_from( - rlang::caller_env(), - exercise_result( - list(message = HTML(msg), correct = FALSE, location = "append") - ) + exercise_result( + list(message = HTML(msg), correct = FALSE, location = "prepend", type = "error") ) } check_parsable <- function(user_code) { error <- rlang::catch_cnd(parse(text = user_code), "error") - if (is.null(error)) {return(NULL)} - - rlang::return_from( - rlang::caller_env(), - exercise_result_error( - error$message, - list( - message = HTML(i18n_span("text.unparsable")), - correct = FALSE, location = "append" - ) - ) + if (is.null(error)) { + return(NULL) + } + ) } From a07db75c9e1cca8a37b6b316b3e3d1ba6c17ce20 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 15 Jul 2021 17:50:45 -0400 Subject: [PATCH 28/58] Use html code style around blanks --- R/exercise.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/exercise.R b/R/exercise.R index 57e9c3dff..b2cc78f6a 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -743,7 +743,8 @@ check_blanks <- function(user_code, blank_regex) { "text.pleasereplaceblank", opts = list( count = length(blanks), - blank = i18n_combine_words(unique(blanks), before = "`") + blank = i18n_combine_words(unique(blanks), before = "", after = ""), + interpolation = list(escapeValue = FALSE) ) ) ) From fd4b19a2b7401aa669e65e2706c0ddc1047bf629 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 15 Jul 2021 17:51:32 -0400 Subject: [PATCH 29/58] Return parse error result as regular html output, rather than in an error alert div --- R/exercise.R | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/R/exercise.R b/R/exercise.R index b2cc78f6a..fbd8422e2 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -760,6 +760,17 @@ check_parsable <- function(user_code) { return(NULL) } + exercise_result( + list( + message = HTML(i18n_span("text.unparsable")), + correct = FALSE, + location = "append", + type = "error" + ), + html_output = HTML( + paste0("
", htmltools::htmlEscape(error$message), "
") + ), + error_message = error$message ) } From 65829c0ffb2677e3fd533ea979f6d12ae65da8e1 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Fri, 16 Jul 2021 09:09:01 -0700 Subject: [PATCH 30/58] Test if `blank_feedback` exists directly before `exercise$check`; if so, skip `exercise$check` --- R/exercise.R | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index fbd8422e2..2bf7ccbc8 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -337,12 +337,6 @@ evaluate_exercise <- function( blank_feedback <- check_blanks(exercise$code, exercise_blanks) } - if (!is.null(blank_feedback)) { - # Don't need to do exercise checking, since it will be replaced with - # feedback about the detected blanks - exercise$check <- "" - } - return_if_exercise_result <- function(res) { # early return if the we've received an exercise result, # but also replace the feedback with the blank feedback if any were found @@ -415,7 +409,8 @@ evaluate_exercise <- function( return_if_exercise_result(rmd_results) # Run the checker post-evaluation (for checking code results) - if (nzchar(exercise$check)) { + # Don't need to do exercise checking if there are blanks + if (is.null(blank_feedback) && nzchar(exercise$check)) { checker_feedback <- try_checker( exercise, check_code = exercise$check, From 1fadf913c2e67f1a83614e16cc159bba11a4a58c Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Fri, 16 Jul 2021 09:11:50 -0700 Subject: [PATCH 31/58] Update `check_blanks` test --- tests/testthat/test-exercise.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index 4fb9264b8..2fdfd5e34 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -834,7 +834,8 @@ test_that("setting a regex blank for the blank checker", { result <- evaluate_exercise(ex, new.env(), evaluate_global_setup = TRUE) expect_match(result$feedback$message, ""count":2") expect_match( - result$feedback$message, "`..function..` \\$t\\(text.and\\) `..string..`" + result$feedback$message, + "\\.\\.function\\.\\..*\\$t\\(text.and\\).*\\.\\.string\\.\\." ) }) From d73b52b5797e38151654da0f1e5c601709f2e9f8 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 16 Jul 2021 12:58:18 -0400 Subject: [PATCH 32/58] Restyle error_message_html() as literal code by default Co-authored-by: Alex Rossell Hayes --- R/exercise.R | 8 +++----- R/feedback.R | 12 ++++++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 2bf7ccbc8..3aaad25a4 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -762,9 +762,7 @@ check_parsable <- function(user_code) { location = "append", type = "error" ), - html_output = HTML( - paste0("
", htmltools::htmlEscape(error$message), "
") - ), + html_output = error_message_html(error$message), error_message = error$message ) } @@ -778,12 +776,12 @@ exercise_result_timeout <- function() { # @param timeout_exceeded represents whether or not the error was triggered # because the exercise exceeded the timeout. Use NA if unknown -exercise_result_error <- function(error_message, feedback = NULL, timeout_exceeded = NA) { +exercise_result_error <- function(error_message, feedback = NULL, timeout_exceeded = NA, style = "code") { exercise_result( feedback = feedback, timeout_exceeded = timeout_exceeded, error_message = error_message, - html_output = error_message_html(error_message) + html_output = error_message_html(error_message, style = style) ) } diff --git a/R/feedback.R b/R/feedback.R index 3b11257c1..6849cea5d 100644 --- a/R/feedback.R +++ b/R/feedback.R @@ -63,6 +63,14 @@ feedback_as_html <- function(feedback) { } # helper function to create tags for error message -error_message_html <- function(message) { - div(class = "alert alert-danger", role = "alert", message) +error_message_html <- function(message, style = "code") { + switch( + style, + alert = div(class = "alert alert-danger", role = "alert", message), + code = , + pre( + code(class = "text-danger", message, .noWS = c("before", "after")), + .noWS = c("before", "after") + ) + ) } From bcf328d14ca30213c894b563e465a07957277d01 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 16 Jul 2021 12:59:54 -0400 Subject: [PATCH 33/58] Include error message output in error checker feedback Co-authored-by: Alex Rossell Hayes --- R/exercise.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 3aaad25a4..6d9901d72 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -389,10 +389,11 @@ evaluate_exercise <- function( rmd_results <- tryCatch( render_exercise(exercise, envir), error = function(err_render) { + error_feedback <- NULL if (nzchar(exercise$error_check)) { # Check the error thrown by the submitted code when there's error # checking: the exercise could be to throw an error! - checker_feedback <- try_checker( + error_feedback <- try_checker( exercise, check_code = exercise$error_check, envir_result = err_render$envir_result, @@ -400,9 +401,8 @@ evaluate_exercise <- function( envir_prep = err_render$envir_prep, last_value = err_render ) - return(checker_feedback) } - exercise_result_error(err_render$error_message) + exercise_result_error(err_render$error_message, error_feedback$feedback) } ) From 44a68532591f107ae0a9e698a13466f3965ea163 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Fri, 16 Jul 2021 10:45:54 -0700 Subject: [PATCH 34/58] Move `checker_feedback <- NULL` immediately before it is possibly set by `exercise$check` --- R/exercise.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 2bf7ccbc8..bb7c82224 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -360,8 +360,6 @@ evaluate_exercise <- function( return_if_exercise_result(check_parsable(exercise$code)) } - checker_feedback <- NULL - # Check the code pre-evaluation, if code_check is provided if (nzchar(exercise$code_check)) { # treat the blank check like a code check, if blanks were detected @@ -410,6 +408,7 @@ evaluate_exercise <- function( # Run the checker post-evaluation (for checking code results) # Don't need to do exercise checking if there are blanks + checker_feedback <- NULL if (is.null(blank_feedback) && nzchar(exercise$check)) { checker_feedback <- try_checker( exercise, From 1dd4f11240870dd5a935b06dcd3e866a845a75a5 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Fri, 16 Jul 2021 11:14:31 -0700 Subject: [PATCH 35/58] Use alert style for timeout error --- R/exercise.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/exercise.R b/R/exercise.R index ca1cc1dc5..8e3452db9 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -769,7 +769,8 @@ check_parsable <- function(user_code) { exercise_result_timeout <- function() { exercise_result_error( "Error: Your code ran longer than the permitted timelimit for this exercise.", - timeout_exceeded = TRUE + timeout_exceeded = TRUE, + style = "alert" ) } From 9c5e618b1d1acb4261e5f714666893bfe3551a6c Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Fri, 16 Jul 2021 11:14:51 -0700 Subject: [PATCH 36/58] If `exercise.blanks = TRUE`, use underscores --- R/exercise.R | 3 ++- tests/testthat/test-exercise.R | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/R/exercise.R b/R/exercise.R index 8e3452db9..efea7e074 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -330,7 +330,8 @@ evaluate_exercise <- function( # Check if user code has unfilled blanks; if it does, early return exercise_blanks <- exercise$options$exercise.blanks %||% knitr::opts_chunk$get("exercise.blanks") %||% - "_{3,}" + TRUE + if (isTRUE(exercise_blanks)) exercise_blanks <- "_{3,}" blank_feedback <- NULL if (shiny::isTruthy(exercise_blanks)) { diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index 2fdfd5e34..d6171672e 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -853,6 +853,22 @@ test_that("default message if exercise.blanks is FALSE", { expect_match(result$feedback$message, "text.unparsable") }) +test_that("use underscores as blanks if exercise.blanks is TRUE", { + ex <- mock_exercise( + user_code = 'print("____")', exercise.blanks = TRUE + ) + result <- evaluate_exercise(ex, new.env()) + expect_match(result$feedback$message, ""count":1") + expect_match(result$feedback$message, "____") + + ex <- mock_exercise( + user_code = '____("test")', exercise.blanks = TRUE + ) + result <- evaluate_exercise(ex, new.env()) + expect_match(result$feedback$message, ""count":1") + expect_match(result$feedback$message, "____") +}) + test_that("evaluate_exercise() returns a message if code is unparsable", { ex <- mock_exercise(user_code = 'print("test"') result <- evaluate_exercise(ex, new.env()) From fde70e80740e2fefb7de1850f87e24f4ac9cc63c Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Tue, 20 Jul 2021 09:42:12 -0700 Subject: [PATCH 37/58] `i18n_set_language_option()` sets env var to translate R messages --- R/i18n.R | 1 + tests/testthat/test-i18n.R | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/R/i18n.R b/R/i18n.R index 319b22a9b..46341bc0b 100644 --- a/R/i18n.R +++ b/R/i18n.R @@ -150,6 +150,7 @@ i18n_set_language_option <- function(language = NULL) { } knitr::opts_knit$set(tutorial.language = language) + Sys.setenv("LANGUAGE" = language) invisible(current) } diff --git a/tests/testthat/test-i18n.R b/tests/testthat/test-i18n.R index 3aa3a589d..18f05a077 100644 --- a/tests/testthat/test-i18n.R +++ b/tests/testthat/test-i18n.R @@ -202,3 +202,17 @@ test_that("i18n_span() returns an i18n span", { expect_match(span, ">DEFAULT") expect_match(span, 'data-i18n-opts="{"interp":"STRING"}"', fixed = TRUE) }) + +test_that("i18n_set_language_option() changes message language", { + withr::local_envvar(list("LANGUAGE" = Sys.getenv("LANGUAGE"))) + + i18n_set_language_option("fr") + expect_equal(knitr::opts_knit$get("tutorial.language"), "fr") + expect_equal(Sys.getenv("LANGUAGE"), "fr") + expect_error(mean$x, "objet de type 'closure' non indiçable") + + i18n_set_language_option("pt_BR") + expect_equal(knitr::opts_knit$get("tutorial.language"), "pt_BR") + expect_equal(Sys.getenv("LANGUAGE"), "pt_BR") + expect_error(mean$x, "objeto de tipo 'closure' não possível dividir em subconjuntos") +}) From 39c0fe4805a2c02bc873495745a1c15ed9e027e2 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 12 Aug 2021 14:38:16 -0400 Subject: [PATCH 38/58] Refactor and add internal documention about exercise blanks checking --- R/exercise.R | 49 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index efea7e074..257688465 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -316,10 +316,9 @@ evaluate_exercise <- function( i18n_set_language_option(exercise$tutorial$language) - # return immediately and clear visible results - # do not consider this an exercise submission if (!nzchar(exercise$code)) { - # " " since html_output needs to pass a req() + # return immediately and clear visible results - do not consider this an + # exercise submission but return " " since html_output needs to pass a req() return(exercise_result(html_output = " ")) } @@ -327,20 +326,20 @@ evaluate_exercise <- function( eval(parse(text = exercise$global_setup), envir = envir) } - # Check if user code has unfilled blanks; if it does, early return - exercise_blanks <- exercise$options$exercise.blanks %||% - knitr::opts_chunk$get("exercise.blanks") %||% - TRUE - if (isTRUE(exercise_blanks)) exercise_blanks <- "_{3,}" - + # Check if user code has unfilled blanks ---------------------------------- + # If blanks are detected we store the feedback for use at the standard + # feedback-returning exit points, but still try to render the user code since + # the output may still be valid even if the user needs to fill in some blanks blank_feedback <- NULL - if (shiny::isTruthy(exercise_blanks)) { - blank_feedback <- check_blanks(exercise$code, exercise_blanks) + exercise_blanks_pattern <- exercise_get_blanks_pattern(exercise) + if (shiny::isTruthy(exercise_blanks_pattern)) { + blank_feedback <- check_blanks(exercise$code, exercise_blanks_pattern) } + here <- rlang::current_env() return_if_exercise_result <- function(res) { - # early return if the we've received an exercise result, - # but also replace the feedback with the blank feedback if any were found + # early return if we've received an exercise result, but also replace the + # feedback with the blank feedback if any blanks were found if (!is_exercise_result(res)) { return(NULL) } @@ -349,11 +348,10 @@ evaluate_exercise <- function( res$feedback <- blank_feedback$feedback } - rlang::return_from(rlang::caller_env(), res) + rlang::return_from(here, res) } - - # Check if user code is parsable; if not, early return + # Check that user R code is parsable ------------------------------------- if ( tolower(exercise$engine) == "r" && !isFALSE(exercise$options$exercise.parse.check) @@ -361,7 +359,7 @@ evaluate_exercise <- function( return_if_exercise_result(check_parsable(exercise$code)) } - # Check the code pre-evaluation, if code_check is provided + # Code check, pre-evaluation --------------------------------------------- if (nzchar(exercise$code_check)) { # treat the blank check like a code check, if blanks were detected return_if_exercise_result(blank_feedback) @@ -375,6 +373,7 @@ evaluate_exercise <- function( ) } + # Render user code -------------------------------------------------------- # Setup a temporary directory for rendering the exercise exercise_dir <- withr::local_tempdir(pattern = "lrn-ex") @@ -390,6 +389,7 @@ evaluate_exercise <- function( error = function(err_render) { error_feedback <- NULL if (nzchar(exercise$error_check)) { + # Error check ------------------------------------------------------- # Check the error thrown by the submitted code when there's error # checking: the exercise could be to throw an error! error_feedback <- try_checker( @@ -407,6 +407,7 @@ evaluate_exercise <- function( return_if_exercise_result(rmd_results) + # Check ------------------------------------------------------------------- # Run the checker post-evaluation (for checking code results) # Don't need to do exercise checking if there are blanks checker_feedback <- NULL @@ -679,6 +680,20 @@ render_exercise <- function(exercise, envir) { ) } +exercise_get_blanks_pattern <- function(exercise) { + exercise_blanks_opt <- + exercise$options$exercise.blanks %||% + knitr::opts_chunk$get("exercise.blanks") %||% + TRUE + + if (isTRUE(exercise_blanks_opt)) { + # TRUE is a stand-in for the default ___+ + return("_{3,}") + } + + exercise_blanks_opt +} + exercise_get_chunks <- function(exercise, type = c("all", "prep", "user")) { type <- match.arg(type) if (type == "all") { From 73e4d713a57dbb7b13dd7f767a37bc14d7c5a807 Mon Sep 17 00:00:00 2001 From: Alex Rossell Hayes Date: Thu, 12 Aug 2021 15:11:41 -0700 Subject: [PATCH 39/58] Remove temporary translations --- data-raw/i18n_translations.yml | 24 ++++++++++++------------ inst/internals/i18n_translations.rds | Bin 2532 -> 2460 bytes 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/data-raw/i18n_translations.yml b/data-raw/i18n_translations.yml index 723b00e5f..9d9a1caac 100644 --- a/data-raw/i18n_translations.yml +++ b/data-raw/i18n_translations.yml @@ -283,7 +283,7 @@ text: blank: en: "blank" fr: ~ - es: "espacio en blanco" # Added by Alex, not a real translator + es: ~ pt: ~ tr: ~ emo: ~ @@ -291,7 +291,7 @@ text: blank_plural: en: "blanks" fr: ~ - es: "espacios en blanco" # Added by Alex, not a real translator + es: ~ pt: ~ tr: ~ emo: ~ @@ -300,7 +300,7 @@ text: # {{count}} - the number of blanks detected in the exercise en: "This exercise contains {{count}} $t(text.blank)." fr: ~ - es: "Este ejercicio contiene {{count}} $t(text.blank)." # Added by Alex, not a real translator + es: ~ pt: ~ tr: ~ emo: ~ @@ -309,7 +309,7 @@ text: # {{blank}} - the string representing a blank in the exercise (e.g. "___") en: "Please replace {{blank}} with valid code." fr: ~ - es: "Reemplace {{blank}} con código real." # Added by Alex, not a real translator + es: ~ pt: ~ tr: ~ emo: ~ @@ -330,17 +330,17 @@ text: eu: ~ and: en: "and" - fr: "et" # Added by Alex, not a real translator - es: "y" # Added by Alex, not a real translator - pt: "e" # Added by Alex, not a real translator - tr: "ve" # Added by Alex, not a real translator + fr: "et" + es: "y" + pt: "e" + tr: ~ emo: ~ - eu: "eta" # Added by Alex, not a real translator + eu: ~ or: en: "or" - fr: "ou" # Added by Alex, not a real translator - es: "o" # Added by Alex, not a real translator - pt: "ou" # Added by Alex, not a real translator + fr: "ou" + es: "o" + pt: "ou" tr: ~ emo: ~ eu: ~ diff --git a/inst/internals/i18n_translations.rds b/inst/internals/i18n_translations.rds index 4e2099a0273db83569c78fe458cafd9a7952070a..d8767efb71920e9f1256abb51388c6ce0c91d3a8 100644 GIT binary patch literal 2460 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 From bdf29c61ca45074b6bda3a753f94ac8a23b007cd Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 19 Aug 2021 15:58:09 -0400 Subject: [PATCH 40/58] refactor: `exercise_should_check_parsability()` --- R/exercise.R | 25 +++++++++++++++++++++---- R/utils.R | 4 ---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index c6a9a85c0..ff0a37714 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -372,10 +372,7 @@ evaluate_exercise <- function( } # Check that user R code is parsable ------------------------------------- - if ( - tolower(exercise$engine) == "r" && - !isFALSE(exercise$options$exercise.parse.check) - ) { + if (exercise_should_check_parsability(exercise)) { return_if_exercise_result(check_parsable(exercise$code)) } @@ -748,6 +745,26 @@ exercise_get_chunks <- function(exercise, type = c("all", "prep", "user")) { } } +exercise_should_check_parsability <- function(exercise) { + if (!identical(tolower(exercise$engine), "r")) { + return(FALSE) + } + + opt <- + exercise$options$exercise.parse.check %||% + knitr::opts_chunk$get("exercise.parse.check") %||% + TRUE + + if (isTRUE(opt)) { + return(TRUE) + } + + skip_parse_check <- identical(opt, "FALSE") || + (is.logical(opt) && length(opt) == 1L && !is.na(opt) && !opt) + + !skip_parse_check +} + exercise_code_chunks_prep <- function(exercise) { exercise_code_chunks(exercise_get_chunks(exercise, "prep")) } diff --git a/R/utils.R b/R/utils.R index 2e7bdbe9c..362c48da5 100644 --- a/R/utils.R +++ b/R/utils.R @@ -108,10 +108,6 @@ is_installed <- function(package, version = NULL) { TRUE } -isFALSE <- function(x) { - is.logical(x) && length(x) == 1L && !is.na(x) && !x -} - timestamp_utc <- function() { strftime(Sys.time(), "%F %H:%M:%OS3 %Z", tz = "UTC") } From 4766531be339fcbd005e669340b10f7c848ebd4e Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 19 Aug 2021 16:29:52 -0400 Subject: [PATCH 41/58] Use HTML instead of markdown in text.unparsable translation --- data-raw/i18n_translations.yml | 5 +++-- inst/internals/i18n_translations.rds | Bin 2460 -> 2477 bytes 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/data-raw/i18n_translations.yml b/data-raw/i18n_translations.yml index 9d9a1caac..026a07ee6 100644 --- a/data-raw/i18n_translations.yml +++ b/data-raw/i18n_translations.yml @@ -320,8 +320,9 @@ text: R cannot determine how to turn your text into a complete command. You may have forgotten to fill in a blank, to remove an underscore, to include a comma between arguments, - or to close an opening `\"`, `'`, `(` or `{` - with a matching `\"`, `'`, `)` or `}`. + or to close an opening ", ', ( + or { with a matching ", ', + ) or }. fr: ~ es: ~ pt: ~ diff --git a/inst/internals/i18n_translations.rds b/inst/internals/i18n_translations.rds index d8767efb71920e9f1256abb51388c6ce0c91d3a8..82421005465d881a07bafb76cf12efd980537609 100644 GIT binary patch delta 2443 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 From 1f2d3937ee9006f7d2cd529ab9f23c5ecf20d43c Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 19 Aug 2021 16:30:13 -0400 Subject: [PATCH 42/58] Don't escape HTML in the translation JSON dependency --- R/html-dependencies.R | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/R/html-dependencies.R b/R/html-dependencies.R index cee67fe84..cdfce88fb 100644 --- a/R/html-dependencies.R +++ b/R/html-dependencies.R @@ -93,9 +93,11 @@ tutorial_i18n_html_dependency <- function(language = NULL) { head = format(htmltools::tags$script( id = "i18n-cstm-trns", type = "application/json", - jsonlite::toJSON( - i18n_process_language_options(language), - auto_unbox = TRUE + htmltools::HTML( + jsonlite::toJSON( + i18n_process_language_options(language), + auto_unbox = TRUE + ) ) )) ) From 5014bec218bd0a3e5da585afe7c0757847ff6570 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 19 Aug 2021 16:30:25 -0400 Subject: [PATCH 43/58] minor style adjustment --- R/exercise.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/exercise.R b/R/exercise.R index 04b1e517b..ec4737349 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -801,7 +801,8 @@ check_blanks <- function(user_code, blank_regex) { msg <- paste( i18n_span( - "text.exercisecontainsblank", opts = list(count = length(blanks)) + "text.exercisecontainsblank", + opts = list(count = length(blanks)) ), i18n_span( "text.pleasereplaceblank", From dbd02ce97d3f23453799501f88bb366d6ba668a4 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 19 Aug 2021 16:44:50 -0400 Subject: [PATCH 44/58] Always check R code parsability prior to evaluation --- R/exercise.R | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index ec4737349..57dab84e9 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -372,8 +372,10 @@ evaluate_exercise <- function( } # Check that user R code is parsable ------------------------------------- - if (exercise_should_check_parsability(exercise)) { - return_if_exercise_result(check_parsable(exercise$code)) + if (identical(tolower(exercise$engine), "r")) { + return_if_exercise_result( + exercise_check_code_is_parsable(exercise$code) + ) } # Code check, pre-evaluation --------------------------------------------- @@ -745,26 +747,6 @@ exercise_get_chunks <- function(exercise, type = c("all", "prep", "user")) { } } -exercise_should_check_parsability <- function(exercise) { - if (!identical(tolower(exercise$engine), "r")) { - return(FALSE) - } - - opt <- - exercise$options$exercise.parse.check %||% - knitr::opts_chunk$get("exercise.parse.check") %||% - TRUE - - if (isTRUE(opt)) { - return(TRUE) - } - - skip_parse_check <- identical(opt, "FALSE") || - (is.logical(opt) && length(opt) == 1L && !is.na(opt) && !opt) - - !skip_parse_check -} - exercise_code_chunks_prep <- function(exercise) { exercise_code_chunks(exercise_get_chunks(exercise, "prep")) } From 5814717d74fb2854f152b33525a9ea176465e373 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 19 Aug 2021 16:45:40 -0400 Subject: [PATCH 45/58] Rename exercise checking functions - `check_blanks()` -> `exercise_check_code_for_blanks()` - `check_parsable()` -> `exercise_check_code_is_parsable()` --- R/exercise.R | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index 57dab84e9..c4ce06867 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -353,7 +353,10 @@ evaluate_exercise <- function( blank_feedback <- NULL exercise_blanks_pattern <- exercise_get_blanks_pattern(exercise) if (shiny::isTruthy(exercise_blanks_pattern)) { - blank_feedback <- check_blanks(exercise$code, exercise_blanks_pattern) + blank_feedback <- exercise_check_code_for_blanks( + user_code = exercise$code, + blank_regex = exercise_blanks_pattern + ) } here <- rlang::current_env() @@ -772,7 +775,7 @@ exercise_code_chunks <- function(chunks) { }, character(1)) } -check_blanks <- function(user_code, blank_regex) { +exercise_check_code_for_blanks <- function(user_code, blank_regex) { blank_regex <- paste(blank_regex, collapse = "|") blanks <- str_match_all(user_code, blank_regex) @@ -801,7 +804,7 @@ check_blanks <- function(user_code, blank_regex) { ) } -check_parsable <- function(user_code) { +exercise_check_code_is_parsable <- function(user_code) { error <- rlang::catch_cnd(parse(text = user_code), "error") if (is.null(error)) { return(NULL) From 1148e331b4a9825894025da595f83194b8ae6ef0 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 20 Aug 2021 17:30:32 -0400 Subject: [PATCH 46/58] Update `exercise_check_code_*()` functions to use the `exercise` object directly --- R/exercise.R | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/R/exercise.R b/R/exercise.R index c4ce06867..881254881 100644 --- a/R/exercise.R +++ b/R/exercise.R @@ -349,15 +349,9 @@ evaluate_exercise <- function( # Check if user code has unfilled blanks ---------------------------------- # If blanks are detected we store the feedback for use at the standard # feedback-returning exit points, but still try to render the user code since - # the output may still be valid even if the user needs to fill in some blanks - blank_feedback <- NULL - exercise_blanks_pattern <- exercise_get_blanks_pattern(exercise) - if (shiny::isTruthy(exercise_blanks_pattern)) { - blank_feedback <- exercise_check_code_for_blanks( - user_code = exercise$code, - blank_regex = exercise_blanks_pattern - ) - } + # the output may still be valid even if the user needs to fill in some blanks. + # Importantly, `blank_feedback` is `NULL` if no blanks are detected. + blank_feedback <- exercise_check_code_for_blanks(exercise) here <- rlang::current_env() return_if_exercise_result <- function(res) { @@ -377,7 +371,7 @@ evaluate_exercise <- function( # Check that user R code is parsable ------------------------------------- if (identical(tolower(exercise$engine), "r")) { return_if_exercise_result( - exercise_check_code_is_parsable(exercise$code) + exercise_check_code_is_parsable(exercise) ) } @@ -775,9 +769,16 @@ exercise_code_chunks <- function(chunks) { }, character(1)) } -exercise_check_code_for_blanks <- function(user_code, blank_regex) { +exercise_check_code_for_blanks <- function(exercise) { + blank_regex <- exercise_get_blanks_pattern(exercise) + + if (!shiny::isTruthy(blank_regex)) { + return(NULL) + } + blank_regex <- paste(blank_regex, collapse = "|") + user_code <- exercise$code blanks <- str_match_all(user_code, blank_regex) if (!length(blanks)) { @@ -804,8 +805,8 @@ exercise_check_code_for_blanks <- function(user_code, blank_regex) { ) } -exercise_check_code_is_parsable <- function(user_code) { - error <- rlang::catch_cnd(parse(text = user_code), "error") +exercise_check_code_is_parsable <- function(exercise) { + error <- rlang::catch_cnd(parse(text = exercise$code), "error") if (is.null(error)) { return(NULL) } From 60fbd45a56f8f8e81c2f5ae55acd6345f3ba1b02 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 20 Aug 2021 17:30:45 -0400 Subject: [PATCH 47/58] Test `exercise_check_code_*()` functions directly in tests --- tests/testthat/test-exercise.R | 61 +++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/tests/testthat/test-exercise.R b/tests/testthat/test-exercise.R index 185273719..83d37691d 100644 --- a/tests/testthat/test-exercise.R +++ b/tests/testthat/test-exercise.R @@ -785,44 +785,54 @@ test_that("env vars are protected from both user and author modification", { expect_equal(res$after_eval, "APP") }) -# unparsable input ----------------------------------------------------------- +# Blanks ------------------------------------------------------------------ test_that("evaluate_exercise() returns a message if code contains ___", { ex <- mock_exercise(user_code = '____("test")') result <- evaluate_exercise(ex, new.env()) + expect_equal(result$feedback, exercise_check_code_for_blanks(ex)$feedback) expect_match(result$feedback$message, ""count":1") expect_match(result$feedback$message, "____") ex <- mock_exercise(user_code = '____(____)') result <- evaluate_exercise(ex, new.env()) + expect_equal(result$feedback, exercise_check_code_for_blanks(ex)$feedback) expect_match(result$feedback$message, ""count":2") ex <- mock_exercise(user_code = '____("____")') result <- evaluate_exercise(ex, new.env()) + expect_equal(result$feedback, exercise_check_code_for_blanks(ex)$feedback) expect_match(result$feedback$message, ""count":2") }) test_that("setting a different blank for the blank checker", { ex <- mock_exercise(user_code = '####("test")', exercise.blanks = "###") result <- evaluate_exercise(ex, new.env()) + expect_equal(result$feedback, exercise_check_code_for_blanks(ex)$feedback) expect_match(result$feedback$message, ""count":1") expect_match(result$feedback$message, "###") ex <- mock_exercise(user_code = '####(####)', exercise.blanks = "###") result <- evaluate_exercise(ex, new.env()) + expect_equal(result$feedback, exercise_check_code_for_blanks(ex)$feedback) expect_match(result$feedback$message, ""count":2") ex <- mock_exercise(user_code = '####("####")', exercise.blanks = "###") result <- evaluate_exercise(ex, new.env()) + expect_equal(result$feedback, exercise_check_code_for_blanks(ex)$feedback) expect_match(result$feedback$message, ""count":2") }) test_that("setting a different blank for the blank checker in global setup", { + # global setup code, when evaluated, pollutes our global knitr options + withr::defer(knitr::opts_chunk$set(exercise.blanks = NULL)) + ex <- mock_exercise( user_code = '####("test")', global_setup = 'knitr::opts_chunk$set(exercise.blanks = "###")' ) result <- evaluate_exercise(ex, new.env(), evaluate_global_setup = TRUE) + expect_equal(result$feedback, exercise_check_code_for_blanks(ex)$feedback) expect_match(result$feedback$message, ""count":1") }) @@ -832,6 +842,7 @@ test_that("setting a regex blank for the blank checker", { exercise.blanks = "\\.\\.\\S+?\\.\\." ) result <- evaluate_exercise(ex, new.env(), evaluate_global_setup = TRUE) + expect_equal(result$feedback, exercise_check_code_for_blanks(ex)$feedback) expect_match(result$feedback$message, ""count":2") expect_match( result$feedback$message, @@ -839,66 +850,64 @@ test_that("setting a regex blank for the blank checker", { ) }) -test_that("default message if exercise.blanks is FALSE", { +test_that("use underscores as blanks if exercise.blanks is TRUE", { ex <- mock_exercise( - user_code = 'print("____")', exercise.blanks = FALSE + user_code = 'print("____")', exercise.blanks = TRUE ) result <- evaluate_exercise(ex, new.env()) - expect_null(result$feedback$message) + expect_equal(result$feedback, exercise_check_code_for_blanks(ex)$feedback) + expect_match(result$feedback$message, ""count":1") + expect_match(result$feedback$message, "____") ex <- mock_exercise( - user_code = '____("test")', exercise.blanks = FALSE + user_code = '____("test")', exercise.blanks = TRUE ) result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, "text.unparsable") + expect_equal(result$feedback, exercise_check_code_for_blanks(ex)$feedback) + expect_match(result$feedback$message, ""count":1") + expect_match(result$feedback$message, "____") }) -test_that("use underscores as blanks if exercise.blanks is TRUE", { +test_that("default message if exercise.blanks is FALSE", { ex <- mock_exercise( - user_code = 'print("____")', exercise.blanks = TRUE + user_code = 'print("____")', exercise.blanks = FALSE ) result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, ""count":1") - expect_match(result$feedback$message, "____") + expect_null(result$feedback$message) + expect_null(exercise_check_code_for_blanks(ex)) ex <- mock_exercise( - user_code = '____("test")', exercise.blanks = TRUE + user_code = '____("test")', exercise.blanks = FALSE ) result <- evaluate_exercise(ex, new.env()) - expect_match(result$feedback$message, ""count":1") - expect_match(result$feedback$message, "____") + expect_null(exercise_check_code_for_blanks(ex)) + expect_match(result$feedback$message, "text.unparsable") + expect_equal(result$feedback, exercise_check_code_is_parsable(ex)$feedback) }) + +# Unparsable Code --------------------------------------------------------- + test_that("evaluate_exercise() returns a message if code is unparsable", { ex <- mock_exercise(user_code = 'print("test"') result <- evaluate_exercise(ex, new.env()) + expect_equal(result$feedback, exercise_check_code_is_parsable(ex)$feedback) expect_match(result$feedback$message, "text.unparsable") expect_match(result$error_message, "unexpected end of input") ex <- mock_exercise(user_code = 'print("test)') result <- evaluate_exercise(ex, new.env()) + expect_equal(result$feedback, exercise_check_code_is_parsable(ex)$feedback) expect_match(result$feedback$message, "text.unparsable") expect_match(result$error_message, "unexpected INCOMPLETE_STRING") ex <- mock_exercise(user_code = 'mean(1:10 na.rm = TRUE)') result <- evaluate_exercise(ex, new.env()) + expect_equal(result$feedback, exercise_check_code_is_parsable(ex)$feedback) expect_match(result$feedback$message, "text.unparsable") expect_match(result$error_message, "unexpected symbol") }) -test_that("default error message if exercise.parse.check is FALSE", { - ex <- mock_exercise( - user_code = 'print("test"', exercise.parse.check = FALSE - ) - result <- evaluate_exercise(ex, new.env()) - expect_match(result$error_message, "unexpected end of input") - - ex <- mock_exercise( - user_code = 'mean(1:10 na.rm = TRUE)', exercise.parse.check = FALSE - ) - result <- evaluate_exercise(ex, new.env()) - expect_match(result$error_message, "unexpected symbol") -}) # Timelimit --------------------------------------------------------------- From fa6c5475f6dfe3b93bcf4bb55c69058f1f509731 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 20 Aug 2021 17:57:49 -0400 Subject: [PATCH 48/58] Add exercise.blanks argument to `tutorial_options()` --- R/options.R | 6 ++++++ man/tutorial_options.Rd | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/R/options.R b/R/options.R index 6f4d461ee..b41f0ff80 100644 --- a/R/options.R +++ b/R/options.R @@ -11,6 +11,10 @@ #' (defaults to \code{30}). #' @param exercise.lines Lines of code for exercise editor (defaults to the #' number of lines in the code chunk). +#' @param exercise.blanks A regular expression to be used to identify blanks in +#' submitted code that the user should fill in. If `TRUE` (default), blanks +#' are three or more underscores in a row. If `FALSE`, blank checking is not +#' performed. #' @param exercise.checker Function used to check exercise answers #' (e.g., `gradethis::grade_learnr()`). #' @param exercise.error.check.code A string containing R code to use for checking @@ -26,6 +30,7 @@ tutorial_options <- function(exercise.cap = NULL, exercise.eval = FALSE, exercise.timelimit = 30, exercise.lines = NULL, + exercise.blanks = NULL, exercise.checker = NULL, exercise.error.check.code = NULL, exercise.completion = TRUE, @@ -41,6 +46,7 @@ tutorial_options <- function(exercise.cap = NULL, eval(parse(text = sprintf(set_option_code, "exercise.eval"))) eval(parse(text = sprintf(set_option_code, "exercise.timelimit"))) eval(parse(text = sprintf(set_option_code, "exercise.lines"))) + eval(parse(text = sprintf(set_option_code, "exercise.blaanks"))) eval(parse(text = sprintf(set_option_code, "exercise.checker"))) eval(parse(text = sprintf(set_option_code, "exercise.error.check.code"))) eval(parse(text = sprintf(set_option_code, "exercise.completion"))) diff --git a/man/tutorial_options.Rd b/man/tutorial_options.Rd index 15e44dfb5..7e6878a2d 100644 --- a/man/tutorial_options.Rd +++ b/man/tutorial_options.Rd @@ -9,6 +9,7 @@ tutorial_options( exercise.eval = FALSE, exercise.timelimit = 30, exercise.lines = NULL, + exercise.blanks = NULL, exercise.checker = NULL, exercise.error.check.code = NULL, exercise.completion = TRUE, @@ -29,6 +30,11 @@ see some default output (defaults to \code{FALSE}).} \item{exercise.lines}{Lines of code for exercise editor (defaults to the number of lines in the code chunk).} +\item{exercise.blanks}{A regular expression to be used to identify blanks in +submitted code that the user should fill in. If \code{TRUE} (default), blanks +are three or more underscores in a row. If \code{FALSE}, blank checking is not +performed.} + \item{exercise.checker}{Function used to check exercise answers (e.g., \code{gradethis::grade_learnr()}).} From 1c79d27543595953dc502e8bb0ea513d0753c24c Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 20 Aug 2021 17:58:21 -0400 Subject: [PATCH 49/58] Document `exercise.blanks` in learnr webpage --- docs/exercises.Rmd | 26 +++++++++++++++++++++----- docs/images/exercise-blanks.png | Bin 0 -> 48994 bytes docs/snippets/exerciseblanks.md | 9 +++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 docs/images/exercise-blanks.png create mode 100644 docs/snippets/exerciseblanks.md diff --git a/docs/exercises.Rmd b/docs/exercises.Rmd index 653258bfb..71aff5545 100644 --- a/docs/exercises.Rmd +++ b/docs/exercises.Rmd @@ -35,26 +35,30 @@ Exercises are interactive R code chunks that allow readers to directly execute R Function used to check exercise answers (e.g., gradethis::grade_learnr()). +exercise.blanks +Regular expression to find blanks requiring replacement in the exercise code. See [Checking Blanks](#checking-blanks) below. + + exercise.error.check.code A string containing R code to use for checking code when an exercise evaluation error occurs (e.g., "gradethis::grade_code()"). - + exercise.completion Whether to enable code completion in the exercise editor. - + exercise.diagnostics Whether to enable code diagnostics in the exercise editor. - + exercise.startover Whether to include a "Start Over" button for the exercise. - + exercise.warn_invisible Whether to display an invisible result warning if the last value returned is invisible. - + exercise.reveal_solution Whether or not the solution should be revealed to the user (defaults to `TRUE`). See [Hiding Solutions](#hiding-solutions) below. @@ -252,6 +256,18 @@ Checking of exercise *code* may be done through a `*-code-check` chunk. With a * It's worth noting that, when a `*-code-check` chunk is supplied, the check is done *prior* to evaluation of the exercise submission, meaning that if the `*-code-check` chunk returns feedback, then that feedback is displayed, no exercise code is evaluated, and no result check is performed. +### Checking Blanks + +Occasionally, you may include blanks in the pre-filled code in your exercise prompts --- sections of the code in the exercise prompt that students should fill in. By default, **learnr** will detect sequences of three or more underscores, e.g. `____` as blanks, regardless of where they appear in the user's submitted code. + +
+ + + + +You can choose your own blank pattern by setting the `exercise.blanks` chunk option to a regular expression that identifies your blanks. You may also set `exercise.blanks = TRUE` to use the default **learnr** blank pattern, or `exercise.blanks = FALSE` to skip blank checking altogether. Globally `tutorial_options()` can be used to set the value of this argument for all exercises. + +Submitted code with blanks will still be evaluated, but the other exercise checks will not be performed. This lets the student see the output of their code---which may produce a valid result---but still draws the student's attention to the code that needs to be completed. ### Checking Errors diff --git a/docs/images/exercise-blanks.png b/docs/images/exercise-blanks.png new file mode 100644 index 0000000000000000000000000000000000000000..126261c1bd1ead27e40f6a926a02a7ea75ae8a6b GIT binary patch literal 48994 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 A^8f$< literal 0 HcmV?d00001 diff --git a/docs/snippets/exerciseblanks.md b/docs/snippets/exerciseblanks.md new file mode 100644 index 000000000..3124d9b3c --- /dev/null +++ b/docs/snippets/exerciseblanks.md @@ -0,0 +1,9 @@ +Fill in the blank to create an expression that adds up to **4**. + +```{r blank, exercise = TRUE, exercise.blanks = "___+"} +1 + ____ +``` + +```{r blank-solution} +1 + 3 +``` From bfbf7bbc8cfe73b30de58ff3504cdbb2cf258709 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Fri, 20 Aug 2021 17:58:47 -0400 Subject: [PATCH 50/58] Rebuild docs/ --- docs/examples.html | 3 +- docs/exercises.html | 34 +- docs/formats.html | 3 +- docs/index.html | 3 +- docs/publishing.html | 5 +- docs/questions.html | 3 +- .../header-attrs-2.10.3/header-attrs.js | 12 + docs/site_libs/jquery-1.11.3/jquery.min.js | 5 - docs/site_libs/jquery-3.6.0/jquery-3.6.0.js | 10881 ++++++++++++++++ .../jquery-3.6.0/jquery-3.6.0.min.js | 2 + .../jquery-3.6.0/jquery-3.6.0.min.map | 1 + 11 files changed, 10934 insertions(+), 18 deletions(-) create mode 100644 docs/site_libs/header-attrs-2.10.3/header-attrs.js delete mode 100644 docs/site_libs/jquery-1.11.3/jquery.min.js create mode 100644 docs/site_libs/jquery-3.6.0/jquery-3.6.0.js create mode 100644 docs/site_libs/jquery-3.6.0/jquery-3.6.0.min.js create mode 100644 docs/site_libs/jquery-3.6.0/jquery-3.6.0.min.map diff --git a/docs/examples.html b/docs/examples.html index bfc81bf36..1a7bda46f 100644 --- a/docs/examples.html +++ b/docs/examples.html @@ -13,7 +13,8 @@ Examples - + + diff --git a/docs/exercises.html b/docs/exercises.html index 96c6da82e..5744a895a 100644 --- a/docs/exercises.html +++ b/docs/exercises.html @@ -13,7 +13,8 @@ Exercises - + + @@ -428,13 +429,21 @@

Overview

+exercise.blanks + + +Regular expression to find blanks requiring replacement in the exercise code. See Checking Blanks below. + + + + exercise.error.check.code A string containing R code to use for checking code when an exercise evaluation error occurs (e.g., “gradethis::grade_code()”). - + exercise.completion @@ -442,7 +451,7 @@

Overview

Whether to enable code completion in the exercise editor. - + exercise.diagnostics @@ -450,7 +459,7 @@

Overview

Whether to enable code diagnostics in the exercise editor. - + exercise.startover @@ -458,7 +467,7 @@

Overview

Whether to include a “Start Over” button for the exercise. - + exercise.warn_invisible @@ -466,7 +475,7 @@

Overview

Whether to display an invisible result warning if the last value returned is invisible. - + exercise.reveal_solution @@ -588,7 +597,7 @@

Markdown Hints

Multiple Hints

-

For R code hints you can provide a sequence of hints that reveal progressively more of the solution as desired by the user. To do this create a sequence of indexed hint chunks (e.g. “-hint-1”, “-hint-2,”-hint-3", etc.) for your exercise chunk. For example:

+

For R code hints you can provide a sequence of hints that reveal progressively more of the solution as desired by the user. To do this create a sequence of indexed hint chunks (e.g. “-hint-1”, “-hint-2,”-hint-3”, etc.) for your exercise chunk. For example:

@@ -659,6 +668,17 @@

Checking Code

It’s worth noting that, when a *-code-check chunk is supplied, the check is done prior to evaluation of the exercise submission, meaning that if the *-code-check chunk returns feedback, then that feedback is displayed, no exercise code is evaluated, and no result check is performed.

+
+

Checking Blanks

+

Occasionally, you may include blanks in the pre-filled code in your exercise prompts — sections of the code in the exercise prompt that students should fill in. By default, learnr will detect sequences of three or more underscores, e.g. ____ as blanks, regardless of where they appear in the user’s submitted code.

+
+ +
+ +

+

You can choose your own blank pattern by setting the exercise.blanks chunk option to a regular expression that identifies your blanks. You may also set exercise.blanks = TRUE to use the default learnr blank pattern, or exercise.blanks = FALSE to skip blank checking altogether. Globally tutorial_options() can be used to set the value of this argument for all exercises.

+

Submitted code with blanks will still be evaluated, but the other exercise checks will not be performed. This lets the student see the output of their code—which may produce a valid result—but still draws the student’s attention to the code that needs to be completed.

+

Checking Errors

In the event that an exercise submission generates an error, checking of the code (or its result, which is an error condition) may be done through either a *-error-check chunk or through the global exercise.error.check.code option. If an *-error-check chunk is provided, you must also include a *-check chunk, typically to provide feedback in case the submission doesn’t generate an error.

diff --git a/docs/formats.html b/docs/formats.html index 6f0c838d5..51af1dd70 100644 --- a/docs/formats.html +++ b/docs/formats.html @@ -13,7 +13,8 @@ Formats - + + diff --git a/docs/index.html b/docs/index.html index 34d538b24..8872f448e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -13,7 +13,8 @@ Interactive Tutorials for R - + + diff --git a/docs/publishing.html b/docs/publishing.html index 9e98abc7a..189fa2626 100644 --- a/docs/publishing.html +++ b/docs/publishing.html @@ -13,7 +13,8 @@ Publishing - + + @@ -387,7 +388,7 @@

R Package

install.packages("learnr")
 learnr::run_tutorial("introduction", package = "mypackage")
-

Exercise Checkers

+

Exercise Checkers

Note that if your tutorial performs exercise checking via an external package then you should be sure to add the package you use for checking as an Imports dependency of your package so it’s installed automatically along with your package.

Note that it’s likely that the learnr package will eventually include or depend on another package that provides checking functions. For the time being though explicit installation of external checking packages is a requirement.

diff --git a/docs/questions.html b/docs/questions.html index 5f5c1c672..42a238df4 100644 --- a/docs/questions.html +++ b/docs/questions.html @@ -13,7 +13,8 @@ Questions - + + diff --git a/docs/site_libs/header-attrs-2.10.3/header-attrs.js b/docs/site_libs/header-attrs-2.10.3/header-attrs.js new file mode 100644 index 000000000..dd57d92e0 --- /dev/null +++ b/docs/site_libs/header-attrs-2.10.3/header-attrs.js @@ -0,0 +1,12 @@ +// Pandoc 2.9 adds attributes on both header and div. We remove the former (to +// be compatible with the behavior of Pandoc < 2.8). +document.addEventListener('DOMContentLoaded', function(e) { + var hs = document.querySelectorAll("div.section[class*='level'] > :first-child"); + var i, h, a; + for (i = 0; i < hs.length; i++) { + h = hs[i]; + if (!/^h[1-6]$/i.test(h.tagName)) continue; // it should be a header h1-h6 + a = h.attributes; + while (a.length > 0) h.removeAttribute(a[0].name); + } +}); diff --git a/docs/site_libs/jquery-1.11.3/jquery.min.js b/docs/site_libs/jquery-1.11.3/jquery.min.js deleted file mode 100644 index 0f60b7bd0..000000000 --- a/docs/site_libs/jquery-1.11.3/jquery.min.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! jQuery v1.11.3 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ -!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.3",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b="length"in a&&a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1; - -return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function aa(){return!0}function ba(){return!1}function ca(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ha=/^\s+/,ia=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ja=/<([\w:]+)/,ka=/\s*$/g,ra={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:k.htmlSerialize?[0,"",""]:[1,"X
","
"]},sa=da(y),ta=sa.appendChild(y.createElement("div"));ra.optgroup=ra.option,ra.tbody=ra.tfoot=ra.colgroup=ra.caption=ra.thead,ra.th=ra.td;function ua(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ua(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function va(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wa(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xa(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function ya(a){var b=pa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function za(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Aa(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Ba(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xa(b).text=a.text,ya(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!ga.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ta.innerHTML=a.outerHTML,ta.removeChild(f=ta.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ua(f),h=ua(a),g=0;null!=(e=h[g]);++g)d[g]&&Ba(e,d[g]);if(b)if(c)for(h=h||ua(a),d=d||ua(f),g=0;null!=(e=h[g]);g++)Aa(e,d[g]);else Aa(a,f);return d=ua(f,"script"),d.length>0&&za(d,!i&&ua(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=da(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(la.test(f)){h=h||o.appendChild(b.createElement("div")),i=(ja.exec(f)||["",""])[1].toLowerCase(),l=ra[i]||ra._default,h.innerHTML=l[1]+f.replace(ia,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&ha.test(f)&&p.push(b.createTextNode(ha.exec(f)[0])),!k.tbody){f="table"!==i||ka.test(f)?""!==l[1]||ka.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ua(p,"input"),va),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ua(o.appendChild(f),"script"),g&&za(h),c)){e=0;while(f=h[e++])oa.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ua(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&za(ua(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ua(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fa,""):void 0;if(!("string"!=typeof a||ma.test(a)||!k.htmlSerialize&&ga.test(a)||!k.leadingWhitespace&&ha.test(a)||ra[(ja.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ia,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ua(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ua(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&na.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ua(i,"script"),xa),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ua(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,ya),j=0;f>j;j++)d=g[j],oa.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qa,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Ca,Da={};function Ea(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fa(a){var b=y,c=Da[a];return c||(c=Ea(a,b),"none"!==c&&c||(Ca=(Ca||m("