diff --git a/lib/PGcore.pm b/lib/PGcore.pm index 5927faeb59..f6e1b4f09f 100755 --- a/lib/PGcore.pm +++ b/lib/PGcore.pm @@ -29,7 +29,9 @@ use Tie::IxHash; use WeBWorK::Debug; use MIME::Base64(); use PGUtil(); - +use Encode qw(encode_utf8 decode_utf8); +use utf8; +binmode(STDOUT, ":uft8"); ################################## # PGcore object ################################## @@ -565,13 +567,15 @@ sub PG_restricted_eval { sub decode_base64 ($) { my $self = shift; my $str = shift; - MIME::Base64::decode_base64($str); + $str = MIME::Base64::decode_base64($str); + decode_utf8($str); } sub encode_base64 ($;$) { my $self = shift; my $str = shift; my $option = shift; + $str = encode_utf8($str); MIME::Base64::encode_base64($str); } diff --git a/lib/PGloadfiles.pm b/lib/PGloadfiles.pm index 1445cc734e..a262b75c72 100644 --- a/lib/PGloadfiles.pm +++ b/lib/PGloadfiles.pm @@ -232,7 +232,7 @@ sub compile_file { local($/); $/ = undef; # allows us to treat the file as a single line - open(MACROFILE, "<$filePath") || die "Cannot open file: $filePath"; + open(MACROFILE, "<:utf8", $filePath) || die "Cannot open file: $filePath"; my $string = 'BEGIN {push @__eval__, __FILE__};' . "\n" . ; #warn "compiling $string"; my ($result,$error,$fullerror) = $self->PG_macro_file_eval($string); diff --git a/lib/Units.pm b/lib/Units.pm index 60b6582492..556693f47e 100644 --- a/lib/Units.pm +++ b/lib/Units.pm @@ -283,7 +283,7 @@ our %known_units = ('m' => { 'm' => 1 }, 'parsec' => { - 'factor' => 30.857E15, + 'factor' => 3.08567758149137E16, #30.857E15, 'm' => 1 }, # VOLUME diff --git a/lib/Value/String.pm b/lib/Value/String.pm index 438a10f8b0..bf9022f705 100644 --- a/lib/Value/String.pm +++ b/lib/Value/String.pm @@ -72,7 +72,7 @@ sub compare { # # Mark a string to be display verbatim # -sub verb {shift; return "\\verb".chr(0x85).(shift).chr(0x85)} +sub verb {shift; return "\\verb".chr(0x1F).(shift).chr(0x1F)} # # Put normal strings into \text{} and others into \verb diff --git a/lib/WeBWorK/PG/IO.pm b/lib/WeBWorK/PG/IO.pm index 0990606be6..74ce7251cf 100644 --- a/lib/WeBWorK/PG/IO.pm +++ b/lib/WeBWorK/PG/IO.pm @@ -139,7 +139,7 @@ sub read_whole_file { unless path_is_course_subdir($filePath); local (*INPUT); - open(INPUT, "<$filePath") || die "$0: read_whole_file subroutine:
Can't read file $filePath"; + open(INPUT, "<:encoding(utf8)", $filePath) || die "$0: read_whole_file subroutine:
Can't read file $filePath"; local($/)=undef; my $string = ; # can't append spaces because this causes trouble with <<'EOF' \nEOF construction close(INPUT); @@ -201,7 +201,7 @@ sub createFile { die 'Path is unsafe' unless path_is_course_subdir($fileName); - open(TEMPCREATEFILE, ">$fileName") + open(TEMPCREATEFILE, ">:encoding(UTF-8)",$fileName) or die "Can't open $fileName: $!"; my @stat = stat TEMPCREATEFILE; close(TEMPCREATEFILE); diff --git a/lib/WeBWorK/PG/Translator.pm b/lib/WeBWorK/PG/Translator.pm index 1779bdc20c..62d793e10a 100644 --- a/lib/WeBWorK/PG/Translator.pm +++ b/lib/WeBWorK/PG/Translator.pm @@ -14,7 +14,9 @@ use Net::SMTP; use PGcore; use PGUtil qw(pretty_print); use WeBWorK::PG::IO qw(fileFromPath); - +use utf8; +use v5.12; +binmode(STDOUT,":utf8"); #use PadWalker; # used for processing error messages #use Data::Dumper; @@ -388,7 +390,7 @@ sub pre_load_macro_files { local(*MACROFILE); local($/); $/ = undef; # allows us to treat the file as a single line - open(MACROFILE, "<$filePath") || die "Cannot open file: $filePath"; + open(MACROFILE, "<:encoding(utf8)", $filePath) || die "Cannot open file: $filePath"; my $string = ; close(MACROFILE); @@ -524,7 +526,7 @@ sub source_file { local($/); $/ = undef; # allows us to treat the file as a single line my $err = ""; - if ( open(SOURCEFILE, "<$filePath") ) { + if ( open(SOURCEFILE, "<:encoding(utf8)", $filePath) ) { $self -> {source} = ; close(SOURCEFILE); } else { diff --git a/macros/PG.pl b/macros/PG.pl index 4e59dc5fb2..c4d5b1b806 100644 --- a/macros/PG.pl +++ b/macros/PG.pl @@ -5,10 +5,9 @@ # initialize PGcore and PGrandom -$main::VERSION ="PG-2.13+"; sub _PG_init{ - $main::VERSION ="PG-2.13+"; + $main::VERSION ="PG-2.14+"; # # Set up MathObject context for use in problems # that don't load MathObjects.pl @@ -142,6 +141,62 @@ sub POST_HEADER_TEXT { $PG->POST_HEADER_TEXT(@_); } +# We expect valid HTML language codes, but there can also include a region code, or other +# settings. +# See https://www.w3.org/International/questions/qa-choosing-language-tags +# Example settings: en-US, en-UK, he-IL +# Some special language codes (zh-Hans) are longer +# http://www.rfc-editor.org/rfc/bcp/bcp47.txt +# https://www.w3.org/International/articles/language-tags/ +# https://www.w3.org/International/questions/qa-lang-2or3.en.html +# http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry +# https://www.w3schools.com/tags/ref_language_codes.asp +# https://www.w3schools.com/tags/ref_country_codes.asp +# Tester at https://r12a.github.io/app-subtags/ + +sub SET_PROBLEM_LANGUAGE { + my $requested_lang = shift; + + # Clean it up for safety + my $selected_lang = $requested_lang; + $selected_lang =~ s/[^a-zA-Z0-9-]//g ; # Drop any characters not permitted. + + if ( $selected_lang ne $requested_lang ) { + warn "PROBLEM_LANGUAGE was edited. Requested: $requested_lang which was replaced by $selected_lang"; + } + $PG->{flags}->{"language"} = $selected_lang; +} + +# SET_PROBLEM_TEXTDIRECTION to set the HTML DIRection attribute to be applied +# to the DIV element containing this problem. + +# We only permit valid settings for the HTML direction attribute: +# dir="ltr|rtl|auto" +# https://www.w3schools.com/tags/att_global_dir.asp + +# It is likely that only problems written in RTL scripts +# will need to call the following function to set the base text direction +# for the problem. + +# Note the flag may not be set, and then webwork2 will use default behavior. + +sub SET_PROBLEM_TEXTDIRECTION { + my $requested_dir = shift; + + # Only allow valid values: + + if ( $requested_dir =~ /^ltr$/i ) { + $PG->{flags}->{"textdirection"} = "ltr"; + } elsif ( $requested_dir =~ /^rtl$/i ) { + $PG->{flags}->{"textdirection"} = "rtl"; + } elsif ( $requested_dir =~ /^auto$/i ) { + $PG->{flags}->{"textdirection"} = "auto"; # NOT RECOMMENDED + } else { + warn " INVALID setting for PROBLEM_TEXTDIRECTION: $requested_dir was DROPPED."; + } +} + + sub AskSage { my $python = shift; my $options = shift; diff --git a/t/pg_test_problems/arabic_test.pg b/t/pg_test_problems/arabic_test.pg new file mode 100644 index 0000000000..a16001f13c --- /dev/null +++ b/t/pg_test_problems/arabic_test.pg @@ -0,0 +1,83 @@ +## Translator(Kamal Saleh) +## DESCRIPTION +## Linear Algebra +## ENDDESCRIPTION + +## Tagged by tda2d + +## DBsubject(Lineare Algebra) +## DBchapter(Lineare Gleichungssysteme) +## DBsection(Matrix-Vektor-Form) +## Date(July 2013) +## Institution(TCNJ and Hope College) +## Author(Paul Pearson) +## Level(4) +## MO(1) +## KEYWORDS('matrix' 'equation') + + +## Comments from Mike Gage +## This problem won't render correctly until all of the utf8 changes are in place +## It's a good test for utf8. + +## This problem should be rendered using the math4-ar template as a test of whether +## the right to left format is working properly for rendering arabic (or hebrew). + +DOCUMENT(); # This should be the first executable line in the problem. + +loadMacros( +"PGstandard.pl", +"MathObjects.pl", +"PGcourse.pl", +); + +TEXT(beginproblem()); +$showPartialCorrectAnswers = 1; + +Context('Matrix'); + +foreach $i (1..2) { + $u[$i] = non_zero_random(-5,5,1); + $v[$i] = non_zero_random(-5,5,1); +} +$m = random(-1,1,2) * random(2,5,1); +$n = random(-1,1,2) * random(2,5,1); + +$U = Matrix([[$u[1]], [$u[2]]]); +$V = Matrix([[$v[1]], [$v[2]]]); + +$answer = Matrix([ $m*$u[1]+$n*$v[1], $m*$u[2]+$n*$v[2] ])->transpose; + +Context()->texStrings; +BEGIN_TEXT +لتكن +\( A \) + مصفوفة تطبيق خطي أبعادها +\( 3 \times 2 \) +و أعمدتها مستقلة خطياًًًََُ ً +. +ليكن +\( \vec{u} = $U \) +و +\( \vec{v} = $V \) +شعاعين يحققان المعادلتين +\( A\vec{u} = \vec{a} \) +و +\( A\vec{v} = \vec{b} \) +. +أوجد حل +\( \vec{x} \) +للمعادلة +\( A\vec{x} = $m \vec{a} + $n \vec{b} \). +$BR +$BR +$BLTR +\(\vec{x} = \) \{ $answer->ans_array(20) \} +$ELTR +. +END_TEXT +Context()->normalStrings; + +ANS($answer->cmp); +; +ENDDOCUMENT(); diff --git a/t/pg_test_problems/chem_react1.pg b/t/pg_test_problems/chem_react1.pg new file mode 100644 index 0000000000..9fe95de5e6 --- /dev/null +++ b/t/pg_test_problems/chem_react1.pg @@ -0,0 +1,57 @@ +DOCUMENT(); +loadMacros( +"PGstandard.pl", +"MathObjects.pl", +"contextReaction.pl" +); +# +# +TEXT(&beginproblem); +# +$showPartialCorrectAnswers = 1; +Context("Reaction"); +# +$named_reactants ="Silver nitrate + Sodium chloride"; +$named_products = "Silver chloride + Sodium nitrate"; +# +# ID react, prod for complete molecular, total ionic, and net ionic equations. +# +$COMPLETE_REACTION=Formula("AgNO_{3} (aq) + NaCl (aq) --> AgCl (s) + NaNO_{3} (aq)"); +# +$TOTAL_IONIC = Formula("Ag^{+1}(aq) + NO_{3}^{-1}(aq) + Na^{+1}(aq) + Cl^{-1}(aq) --> AgCl(s)+Na^{+1}(aq) + NO_{3}^{-1}(aq)"); +# +$NET_IONIC=Formula("Ag^{+1}(aq) + Cl^{-1}(aq) --> AgCl(s)"); +# +COMMENT("Reaction arrow is --> NOTE: two dashes and >. Charges for ions should be entered +1, +2, -1, -2, etc. Note the order and the 1 for the + and - cases"); +Context()->texStrings; +BEGIN_TEXT +$BBOLD Answers should be entered using guidlines discussed in lecture. $EBOLD $BR +Consider the reaction shown below.$BR +$named_reactants \(\longrightarrow\) $named_products $BR +Write the complete molecular reaction equation.$BR +\{ans_rule(60)\}$BR +Write the total ionic equation.$BR +\{ans_rule(60)\}$BR +Write the net ionic equation for the reaction.$BR +\{ans_rule(60)\} +END_TEXT + +BEGIN_TEXT + +Solutions: +\($COMPLETE_REACTION\) +$PAR +\($TOTAL_IONIC\) +$PAR +\($NET_IONIC\) + +END_TEXT + +Context()->normalStrings; + +# +ANS($COMPLETE_REACTION->cmp); +ANS($TOTAL_IONIC->cmp); +ANS($NET_IONIC->cmp); +# +ENDDOCUMENT(); \ No newline at end of file diff --git a/t/pg_test_problems/chem_react2.pg b/t/pg_test_problems/chem_react2.pg new file mode 100644 index 0000000000..5e2bba3fb5 --- /dev/null +++ b/t/pg_test_problems/chem_react2.pg @@ -0,0 +1,56 @@ +DOCUMENT(); +loadMacros( +"PGstandard.pl", +"MathObjects.pl", +"contextReaction.pl" +); +# +# +TEXT(&beginproblem); +# +$showPartialCorrectAnswers = 1; +Context("Reaction"); +Context()->variables->add('e' => $context::Reaction::ELEMENT); +# +$named_reactants ="Zinc + Copper(II) sulfate"; +$named_products = "Zinc sulfate + Copper"; +# +$COMPLETE_REACTION=Formula("Zn(s) + CuSO_{4}(aq) --> ZnSO_{4}(aq) + Cu(s)"); +# +$OXIDATION = Formula("Zn(s) --> Zn^{+2} + 2e^{-1}"); +# +$REDUCTION=Formula("Cu^{+2} + 2e^{-1} --> Cu(s)"); +# +COMMENT("Reaction arrow is --> NOTE: two dashes and >. Charges for ions should be entered +1, +2, -1, -2, etc. Note the order and the 1 for the + and - cases. Electrons are added as e^{-1}"); +Context()->texStrings; +BEGIN_TEXT +$BBOLD Answers should be entered using guidlines discussed in lecture. $EBOLD $BR +Consider the reaction shown below.$BR +$named_reactants \(\longrightarrow\) $named_products $BR +Write the complete molecular reaction equation.$BR +\{ans_rule(60)\}$BR +Write the oxidation half-reaction.$BR +\{ans_rule(60)\}$BR +Write the reduction half-reaction.$BR +\{ans_rule(60)\} +END_TEXT + +BEGIN_TEXT + +Solutions: +\($COMPLETE_REACTION\) +$PAR +\($OXIDATION\) +$PAR +\($REDUCTION\) + +END_TEXT + +Context()->normalStrings; + +# +ANS($COMPLETE_REACTION->cmp); +ANS($OXIDATION->cmp); +ANS($REDUCTION->cmp); +# +ENDDOCUMENT(); \ No newline at end of file diff --git a/t/pg_test_problems/matrix_inverse.pg b/t/pg_test_problems/matrix_inverse.pg new file mode 100644 index 0000000000..af0af40031 --- /dev/null +++ b/t/pg_test_problems/matrix_inverse.pg @@ -0,0 +1,70 @@ +##DESCRIPTION +## +## +## +##ENDDESCRIPTION +## +## +DOCUMENT(); # This should be the first executable line in the problem. + +loadMacros( + "PGstandard.pl", # Standard macros for PG language + "MathObjects.pl", + "PGML.pl", + #"source.pl", # used to display problem source button + "PGcourse.pl", # Customization file for the course +); + +TEXT(beginproblem()); +$showPartialCorrectAnswers = 1; + +############################################################## +############################################################### +# Setup +# # +# ## +## Context("Numeric"); + +############################################################## +############################################################### +# Text +# # +# ## +# ## +Context("Matrix"); +Context()->constants->add(A=>Matrix([1,2],[3,4])); +Context()->variables->add(M=>Matrix([1,2],[3,4])); +TEXT(Formula("A^(-1)")->reduce,$BR); +TEXT(Formula("M^(-1)")->reduce,$BR); +TEXT(Formula("[[x,2],[3,x]]^(-1)")->reduce,$BR); +TEXT(Formula("x^(-1)")->reduce,$BR); +TEXT(Formula("x^(-2)")->reduce,$BR); + +BEGIN_PGML +Without the patch, the first three reductions will produce an error about only being able +to divide by a number. With the patch, all three should succeed, and you should get + +* A^(-1) +* M^(-1) +* [`[[x,2],[3,x]]^{(-1)}`] +* 1/x +* 1/(x^2) + +as the output. + +Note that this does change the name of the reduction rule from x^(-1) to x^(-a). I've +checked the macro files and the OPL and don't see any uses of reduction rule x^(-1), so I +think this should not be a problem (though some Wiki pages may need to be updated). + +------------ +END_PGML + +############################################################# +############################################################### +# Answers +# # +# ## +# ## +# ## +# ## +ENDDOCUMENT(); # This should be the last executable line in the problem. \ No newline at end of file diff --git a/t/pg_test_problems/matrix_inverse2.pg b/t/pg_test_problems/matrix_inverse2.pg new file mode 100644 index 0000000000..b59571e199 --- /dev/null +++ b/t/pg_test_problems/matrix_inverse2.pg @@ -0,0 +1,83 @@ +##DESCRIPTION + + + +##ENDDESCRIPTION + + +DOCUMENT(); # This should be the first executable line in the problem. + +loadMacros( + "PGstandard.pl", # Standard macros for PG language + "MathObjects.pl", + "PGML.pl", + #"source.pl", # used to display problem source button + "PGcourse.pl", # Customization file for the course +); + +TEXT(beginproblem()); +$showPartialCorrectAnswers = 1; + +############################################################## +# +# Setup +# +# +Context("Numeric"); + +############################################################## +# +# Text +# +# + +Context("Matrix"); +Context()->constants->add( + A => Matrix([[1,2,3],[4,5,6]]), + B => Matrix([0,0],[0,0]), +); +Context()->variables->add( + M => Matrix([[1,2,3],[4,5,6]]), + N => Matrix([[1,2],[3,4]]), +); + +#Formula("A^(-1)"); +#Formula("M^(-1)"); +#Formula("[[1,2,3],[4,5,x]]^(-1)"); +#Formula("[[1,2,3],[4,5,6]]^(-1)"); + +#Formula("B^(-1)")->eval; +#Formula("N^(-1)")->eval(N => Matrix([[0,0],[0,0]])); +#Formula("[[0,0],[0,0]]^(-1)"); +#Formula("[[x,0],[0,x]]^(-1)")->eval(x => 0); +#Matrix([[0,0],[0,0]]) ** -1; + +Matrix([[0,0],[0,0]])->inverse; +Matrix([[0,0],[0,0]])->solve_LR(Vector(1,1)); + + +BEGIN_PGML +Without the patch, the first four should produce errors +about 2x3 and 2x3 matrixes not being able to be multiplied, +while the next five and final two should produce errors +about matrices needing at least one entry. + +With the patch, the first four should produce messages +that only a square matrix can be inverted, the next +five should say that the matrix is not invertible, +and the last two should produce an undefined value +and a list of three undefined values, respectively. + + +END_PGML +# + +############################################################## +# +# Answers +# +# + + + +ENDDOCUMENT(); # This should be the last executable line in the problem. \ No newline at end of file diff --git a/t/pg_test_problems/test_units1.pg b/t/pg_test_problems/test_units1.pg new file mode 100644 index 0000000000..d0db0f0442 --- /dev/null +++ b/t/pg_test_problems/test_units1.pg @@ -0,0 +1,96 @@ +## DESCRIPTION +## Functions: Input and Output + + +## DBsubject(WeBWorK) +## DBchapter(TEST) +## DBsection(Units) +## Date(01/01/10) +## Institution(U of R) +## Author(Gage) +## Level() +## MO(1) + +## KEYWORDS('test','units',) + +DOCUMENT(); + +loadMacros( +"PGstandard.pl", +"MathObjects.pl", +"AnswerFormatHelp.pl", +"PGML.pl", +"parserNumberWithUnits.pl", +"parserFormulaWithUnits.pl", +"PGcourse.pl", +); + +TEXT(beginproblem()); + +################################## +# Setup + +Context("Numeric"); + + +$one_mph = NumberWithUnits(1,"mph"); +$x2_mph = FormulaWithUnits("x^2", "mph"); + +$hz_0_05 = NumberWithUnits(.05, "Hz"); +$five_percent_per_second = NumberWithUnits(5, "%/s"); +$five_percent = NumberWithUnits(5, "%"); +$one_degree = NumberWithUnits(1, "deg"); +$one_radian = NumberWithUnits(1, "rad"); +$one_hour = NumberWithUnits(1,"hours"); +$one_inch = NumberWithUnits(1,"inches"); +$one_foot = NumberWithUnits(1,"feet"); +$one_minute = NumberWithUnits(1,"minute"); +$one_second = NumberWithUnits(1,"s"); #only s works +$one_cup = NumberWithUnits(.236588 ,"L"); +$one_footpersecond = NumberWithUnits(1,"feet/s"); +$x2_footpersecond = FormulaWithUnits("x^2","feet/s"); + + +##################################### +# Main text + +Context()->texStrings; +BEGIN_PGML +These units are equivalent: +* deg, degree, degrees +* rad, radian, radians, +* in, inch, inches +* ft, feet, foot +* min, minute, minutes +* h, hr, hour, hours +* s, sec,?? +* cup, cups, 0.000236588 m^3 or .236588 L + + +* sr ( steradian or square radian a measure of solid angle. 1 sr = rad^2 ?) The actual definition is the central solid angle of a patch on a sphere of radius r whose area is r^2. A full sphere measures 4pi steradian's. + +The definition here is 1 sr = 1 rad^2 --- is that really right? + +TESTS: + +These should be equivalent: +* Enter .05 Hz in %/s [_________]{$hz_0_05} as [`5*2\pi %*rad/s`] +* Enter 5 %/s as Hz [__________]{$five_percent_per_second} as[`.05/(2pi) Hz/rad`] +* Enter 5 % as [__________]{$five_percent} as [`.05 rad/rad`] +* Enter 1 mph [__________]{$one_mph} as 5280 ft/hr +* Enter x^2 mph [__________]{$x2_mph} + +* Enter one degree: [__________]{$one_degree} +* Enter one radian: [__________]{$one_radian} as [`(2\pi)^-1 cycles`] +* Enter one hour: [__________]{$one_hour} +* Enter one inch: [__________]{$one_inch} +* Enter one foot: [__________]{$one_foot} +* Enter one minute: [__________]{$one_minute} +* Enter one second: [__________]{$one_second} +* Enter one cup: [__________]{$one_cup} +* Enter one ft/sec [__________]{$one_footpersecond} +* [`x^2`] ft/sec [__________]{$x2_footpersecond} +END_PGML + + +ENDDOCUMENT(); \ No newline at end of file diff --git a/t/test_units.t b/t/test_units.t index 76d6d0ecce..605dcfdd8c 100755 --- a/t/test_units.t +++ b/t/test_units.t @@ -5,6 +5,7 @@ #use Test::More tests => 5; use Test::More qw( no_plan ); use lib "../lib"; # location of Units.pm module +use lib "lib"; # location of Units.pm module we run perl t/test_units.t BEGIN { use_ok('Units'); @@ -67,7 +68,7 @@ is_deeply( {evaluate_units('c*yr')}, {evaluate_units('light-year')}, 'light year is_deeply( multiply_by((180/$Units::PI)**2, evaluate_units('deg^2')), {evaluate_units('sr')}, 'solid angle conversion'); -is_deeply( multiply_by(0.01), {evaluate_units('%')}, 'percent conversion'); +is_deeply( multiply_by(0.01,evaluate_units('rad/rad')), {evaluate_units('%')}, 'percent conversion'); }