diff --git a/LICENSE b/LICENSE index 8df1667545..980aae59d3 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ Online Homework Delivery System Version 2.* - Copyright 2000-2018, The WeBWorK Project + Copyright 2000-2022, The WeBWorK Project All rights reserved. This program is free software; you can redistribute it and/or modify diff --git a/README b/README index 2326906afb..45ed6d2993 100644 --- a/README +++ b/README @@ -6,6 +6,6 @@ http://webwork.maa.org/wiki/Category:Release_Notes - Copyright 2000-2017, The WeBWorK Project + Copyright 2000-2022, The WeBWorK Project http://webwork.maa.org All rights reserved. diff --git a/VERSION b/VERSION index e9d9c39ee9..0048b529d4 100644 --- a/VERSION +++ b/VERSION @@ -1,4 +1,4 @@ -$PG_VERSION ='2.16+develop'; -$PG_COPYRIGHT_YEARS = '1996-2021'; +$PG_VERSION ='2.17'; +$PG_COPYRIGHT_YEARS = '1996-2022'; 1; diff --git a/htdocs/js/apps/GraphTool/cubictool.js b/htdocs/js/apps/GraphTool/cubictool.js index a07f026580..b059f9724a 100644 --- a/htdocs/js/apps/GraphTool/cubictool.js +++ b/htdocs/js/apps/GraphTool/cubictool.js @@ -38,6 +38,7 @@ } el.setPosition(JXG.COORDS_BY_USER, [x, el.Y()]); + gt.board.update(); } }, diff --git a/htdocs/js/apps/GraphTool/graphtool.js b/htdocs/js/apps/GraphTool/graphtool.js index 0885a92bf2..4bd6f88b62 100644 --- a/htdocs/js/apps/GraphTool/graphtool.js +++ b/htdocs/js/apps/GraphTool/graphtool.js @@ -452,6 +452,7 @@ window.graphTool = (containerId, options) => { else if (y > boundingBox[1]) y = boundingBox[1] - gt.snapSizeY; point1.setPosition(JXG.COORDS_BY_USER, [x, y]); + gt.board.update(); }; // Prevent paired points from being moved into the same position by a drag. This @@ -664,6 +665,11 @@ window.graphTool = (containerId, options) => { )); this.definingPts.push(center, point); this.focusPoint = center; + + // Redefine the circle's hasPoint method to return true if the center point has the given coordinates, so + // that a pointer over the center point will give focus to the object with the center point activated. + const circleHasPoint = this.baseObj.hasPoint.bind(this.baseObj); + this.baseObj.hasPoint = (x, y) => circleHasPoint(x, y) || center.hasPoint(x, y); } handleKeyEvent(e, el) { @@ -1174,7 +1180,8 @@ window.graphTool = (containerId, options) => { if (e.key !== 'Tab' || (index === 0 && e.shiftKey) || (index === gt.graphedObjs.length - 1 && !e.shiftKey) || - (a.length > 1 && ((pIndex === 0 && !e.shiftKey) || (pIndex === 1 && e.shiftKey))) + (a.length > 1 && + ((pIndex === 0 && !e.shiftKey) || (pIndex === a.length - 1 && e.shiftKey))) ) return; @@ -1194,8 +1201,8 @@ window.graphTool = (containerId, options) => { obj.blur(); }; + point.rendNode.addEventListener('keydown', point.focusOutHandler); } - point.rendNode.addEventListener('keydown', point.focusOutHandler); // Attach a focusin handler to all points to update the coordinates display. point.focusInHandler = (e) => gt.setTextCoords(point.X(), point.Y()); diff --git a/htdocs/js/apps/GraphTool/quadratictool.js b/htdocs/js/apps/GraphTool/quadratictool.js index 8b7530dc50..a487077b6e 100644 --- a/htdocs/js/apps/GraphTool/quadratictool.js +++ b/htdocs/js/apps/GraphTool/quadratictool.js @@ -38,6 +38,7 @@ } el.setPosition(JXG.COORDS_BY_USER, [x, el.Y()]); + gt.board.update(); } }, diff --git a/htdocs/js/apps/Knowls/knowl.js b/htdocs/js/apps/Knowls/knowl.js index bdfcbac8c4..57b8388a40 100644 --- a/htdocs/js/apps/Knowls/knowl.js +++ b/htdocs/js/apps/Knowls/knowl.js @@ -15,6 +15,11 @@ }; const initializeKnowl = (knowl) => { + if (getComputedStyle(knowl)?.display === '') { + setTimeout(() => initializeKnowl(knowl), 100); + return; + } + knowl.dataset.bsToggle = 'collapse'; if (!knowl.knowlContainer) { knowl.knowlContainer = document.createElement('div'); diff --git a/htdocs/js/apps/MathQuill/mqeditor.css b/htdocs/js/apps/MathQuill/mqeditor.css index 57d4889c72..1cfc8b61db 100644 --- a/htdocs/js/apps/MathQuill/mqeditor.css +++ b/htdocs/js/apps/MathQuill/mqeditor.css @@ -15,6 +15,7 @@ span[id^="mq-answer"].incorrect { } span[id^="mq-answer"] { + /*rtl:ignore*/ direction: ltr; padding: 4px 5px 2px 5px; border-radius: 4px !important; @@ -31,6 +32,7 @@ input[type="text"].codeshard.mq-edit { max-height: 95vh; position: fixed; font-size: .75em; + /*rtl:ignore*/ direction: ltr; display: flex; flex-direction: column; diff --git a/htdocs/js/apps/MathQuill/mqeditor.js b/htdocs/js/apps/MathQuill/mqeditor.js index 4793f2654f..1da4b9482e 100644 --- a/htdocs/js/apps/MathQuill/mqeditor.js +++ b/htdocs/js/apps/MathQuill/mqeditor.js @@ -84,8 +84,11 @@ { id: 'text', latex: '\\text', tooltip: 'text mode (")', icon: 'Tt' } ]; + answerQuill.hasFocus = false; + // Open the toolbar when the mathquill answer box gains focus. answerQuill.textarea.addEventListener('focusin', () => { + answerQuill.hasFocus = true; if (answerQuill.toolbar) return; answerQuill.toolbar = document.createElement('div'); @@ -114,6 +117,7 @@ })); button.addEventListener('click', () => { + answerQuill.hasFocus = true; answerQuill.mathField.cmd(button.dataset.latex); answerQuill.textarea.focus(); }) @@ -136,15 +140,15 @@ }); answerQuill.textarea.addEventListener('focusout', (e) => { - if (e.relatedTarget && (e.relatedTarget.closest('.quill-toolbar') || - e.relatedTarget.classList.contains('symbol-button'))) - return; - if (answerQuill.toolbar) { - window.removeEventListener('resize', answerQuill.toolbar.adjustWidth); - answerQuill.toolbar.tooltips.forEach((tooltip) => tooltip.dispose()); - answerQuill.toolbar.remove(); - delete answerQuill.toolbar; - } + answerQuill.hasFocus = false; + setTimeout(function() { + if (!answerQuill.hasFocus && answerQuill.toolbar) { + window.removeEventListener('resize', answerQuill.toolbar.adjustWidth); + answerQuill.toolbar.tooltips.forEach((tooltip) => tooltip.dispose()); + answerQuill.toolbar.remove(); + delete answerQuill.toolbar; + } + }, 200); }); // Trigger an answer preview when the enter key is pressed in an answer box. @@ -161,7 +165,7 @@ document.querySelector('input[name=previewAnswers]')?.click(); // For ww3 const previewButtonId = - answerQuill.textarea.closest('[name=problemMainForm]')[0]?.id + answerQuill.textarea.closest('[name=problemMainForm]')?.id .replace('problemMainForm', 'previewAnswers'); if (previewButtonId) document.getElementById(previewButtonId)?.click(); } diff --git a/htdocs/js/apps/Problem/problem.scss b/htdocs/js/apps/Problem/problem.scss index 465f12e7e9..1073c66f15 100644 --- a/htdocs/js/apps/Problem/problem.scss +++ b/htdocs/js/apps/Problem/problem.scss @@ -1,5 +1,5 @@ /* WeBWorK Online Homework Delivery System - * Copyright © 2000-2021 The WeBWorK Project, https://github.com/openwebwork + * Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork * * This program is free software; you can redistribute it and/or modify it under * the terms of either: (a) the GNU General Public License as published by the @@ -34,7 +34,6 @@ /* Problem elements */ label, input[type=text], select, textarea { - font-size: 16px; font-weight: normal; line-height: 18px; width: auto; @@ -48,10 +47,13 @@ vertical-align: middle; border: 1px solid #ccc; border-radius: 4px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; background-color: white; } + textarea, input[type=text] { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + } + input[type=text] { height: 30px; font-size: 14px; diff --git a/htdocs/js/apps/Scaffold/scaffold.scss b/htdocs/js/apps/Scaffold/scaffold.scss index 1c7d5da450..a62cbb3a5f 100644 --- a/htdocs/js/apps/Scaffold/scaffold.scss +++ b/htdocs/js/apps/Scaffold/scaffold.scss @@ -33,7 +33,7 @@ & > button.accordion-button { background: #eee; - &::focus { + &:focus { box-shadow: none; } diff --git a/lib/AnswerHash.pm b/lib/AnswerHash.pm index 8a029cc8a4..f199b4266f 100755 --- a/lib/AnswerHash.pm +++ b/lib/AnswerHash.pm @@ -3,16 +3,26 @@ ## ## Provides a data structure for answer hashes. Currently just a wrapper ## for the hash, but that might change -#################################################################### -# Copyright @ 1995-2002 WeBWorK Team -# All Rights Reserved -#################################################################### +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. +################################################################################ #$Id$ =head1 NAME AnswerHash.pm -- located in the courseScripts directory - + This file contains the packages/classes: AnswerHash and AnswerEvaluator @@ -20,14 +30,14 @@ AnswerHash -- this class stores information related to the student's answer. It is little more than a standard perl hash with - a special name, but it does have some access and + a special name, but it does have some access and manipulation methods. More of these may be added as it becomes necessary. - + Usage: $rh_ans = new AnswerHash; - + AnswerEvaluator -- this class organizes the construction of - answer evaluator subroutines which check the + answer evaluator subroutines which check the student's answer. By plugging filters into the answer evaluator class you can customize the way the student's answer is normalized and checked. Our hope @@ -36,7 +46,7 @@ combinations to obtain different answer evaluators, thus greatly reducing the programming and maintenance required for constructing answer evaluators. - + Usage: $ans_eval = new AnswerEvaluator; =cut @@ -72,16 +82,16 @@ The answer hash class is guaranteed to contain the following instance variables: This is displayed in the section reporting the results of checking the student answers. - $ans_hash->{original_student_ans} -- This is the original student answer. + $ans_hash->{original_student_ans} -- This is the original student answer. This is displayed on the preview page and may be used for sticky answers. - $ans_hash->{ans_message} -- Any error message, or hint provided by + $ans_hash->{ans_message} -- Any error message, or hint provided by the answer evaluator. This is also displayed in the section reporting the results of checking the student answers. - $ans_hash->{type} -- A string indicating the type of answer evaluator. + $ans_hash->{type} -- A string indicating the type of answer evaluator. This helps in preprocessing the student answer for errors. Some examples: 'number_with_units' @@ -97,13 +107,13 @@ The answer hash class is guaranteed to contain the following instance variables: same as $ans_hash{student_ans}. - $ans_hash->{preview_latex_string} -- + $ans_hash->{preview_latex_string} -- THIS IS OPTIONAL. This is latex version of the student answer which is used to show a typeset view on the answer on the preview page. For a student answer of 2/3, this would be \frac{2}{3}. 'ans_message' => '', # null string - + 'preview_text_string' => undef, 'preview_latex_string' => undef, 'error_flag' => undef, @@ -116,7 +126,7 @@ The answer hash class is guaranteed to contain the following instance variables: BEGIN { # main::be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. - + } package AnswerHash; @@ -140,14 +150,14 @@ my %fields = ( 'score' => undef, =head4 new Useage $rh_anshash = new AnswerHash; - + returns an object of type AnswerHash. - + =cut sub new { my $class = shift @_; - + my $self = { 'score' => 0, 'correct_ans' => 'No correct answer specified', 'student_ans' => undef, @@ -161,10 +171,10 @@ sub new { 'error_message' => '', }; # return a reference to a hash. - + bless $self, $class; $self -> setKeys(@_); - + return $self; } @@ -172,26 +182,26 @@ sub new { ## Checks to make sure that the keys are valid, ## then sets their value -=head4 setKeys - - $rh_ans->setKeys(score=>1, student_answer => "yes"); +=head4 setKeys + + $rh_ans->setKeys(score=>1, student_answer => "yes"); Sets standard elements in the AnswerHash (the ones defined above). Will give error if one attempts to set non-standard keys. - + To set a non-standard element in a hash use - + $rh_ans->{non-standard-key} = newValue; - + There are no safety checks when using this method. =cut - + sub setKeys { my $self = shift; my %inits = @_; foreach my $item (keys %inits) { - if ( exists $fields{$item} ) { + if ( exists $fields{$item} ) { $self -> {$item} = $inits{$item}; } else { @@ -206,14 +216,14 @@ sub setKeys { Usage: $rh_ans->data('foo'); set $rh_ans->{student_ans} = 'foo'; $student_input = $rh_ans->data(); retrieve value of $rh_ans->{student_ans} - + synonym for input -=head4 input +=head4 input Usage: $rh_ans->input('foo') sets $rh_ans->{student_ans} = 'foo'; $student_input = $rh_ans->input(); - + synonym for data =cut @@ -230,16 +240,16 @@ sub input { #$rh_ans->input('foo') is a synonym for $rh_ans->{student_ans}=' $self->{student_ans} } -=head4 input +=head4 input - Usage: $rh_ans->score(1) + Usage: $rh_ans->score(1) $score = $rh_ans->score(); - + Retrieve or set $rh_ans->{score}, the student's score on the problem. =cut -sub score { +sub score { my $self = shift; my $score = shift; $self->{score} = $score if defined($score); @@ -276,20 +286,20 @@ sub stringify_hash { =head4 throw_error Usage: $rh_ans->throw_error("FLAG", "message"); - - FLAG is a distinctive word that describes the type of error. + + FLAG is a distinctive word that describes the type of error. Examples are EVAL for an evaluation error or "SYNTAX" for a syntax error. The entry $rh_ans->{error_flag} is set to "FLAG". - + The catch_error and clear_error methods use this entry. - + message is a descriptive message for the end user, defining what error occured. =head4 catch_error Usage: $rh_ans->catch_error("FLAG2"); - + Returns true (1) if $rh_ans->{error_flag} equals "FLAG2", otherwise it returns false (empty string). @@ -298,8 +308,8 @@ sub stringify_hash { =head4 clear_error Usage: $rh_ans->clear_error("FLAG2"); - - If $rh_ans->{error_flag} equals "FLAG2" then the {error_flag} entry is set to + + If $rh_ans->{error_flag} equals "FLAG2" then the {error_flag} entry is set to the empty string as is the entry {error_message} =head4 error_flag @@ -307,11 +317,11 @@ sub stringify_hash { =head4 error_message Usage: $flag = $rh_ans -> error_flag(); - + $message = $rh_ans -> error_message(); - Retrieve or set the {error_flag} and {error_message} entries. - + Retrieve or set the {error_flag} and {error_message} entries. + Use catch_error and throw_error where possible. =cut @@ -359,15 +369,15 @@ sub error_message { # error print out method # =head4 pretty_print -# -# +# +# # Usage: $rh_ans -> pretty_print(); -# -# +# +# # Returns a string containing a representation of the AnswerHash as an HTML table. -# +# # =cut -# +# # sub pretty_print { # my $r_input = shift; # my $level = shift; @@ -391,7 +401,7 @@ sub error_message { # while (@array) { # $out .= pretty_print(shift @array, $level) . " , "; # } -# $out .= " )"; +# $out .= " )"; # } elsif (ref($r_input) eq 'CODE') { # $out = "$r_input"; # } else { @@ -401,14 +411,14 @@ sub error_message { # $out; # } -# action methods +# action methods =head4 OR Usage: $rh_ans->OR($rh_ans2); - + Returns a new AnswerHash whose score is the maximum of the scores in $rh_ans and $rh_ans2. - The correct answers for the two hashes are combined with "OR". + The correct answers for the two hashes are combined with "OR". The types are concatenated with "OR" as well. Currently nothing is done with the error flags and messages. @@ -418,9 +428,9 @@ sub error_message { Usage: $rh_ans->AND($rh_ans2); - + Returns a new AnswerHash whose score is the minimum of the scores in $rh_ans and $rh_ans2. - The correct answers for the two hashes are combined with "AND". + The correct answers for the two hashes are combined with "AND". The types are concatenated with "AND" as well. Currently nothing is done with the error flags and messages. @@ -433,11 +443,11 @@ sub error_message { sub OR { my $self = shift; - + my $rh_ans2 = shift; my %options = @_; return($self) unless defined($rh_ans2) and ref($rh_ans2) eq 'AnswerHash'; - + my $out_hash = new AnswerHash; # score is the maximum of the two scores $out_hash->{score} = ( $self->{score} < $rh_ans2->{score} ) ? $rh_ans2->{score} :$self->{score}; @@ -498,17 +508,17 @@ use PGUtil qw(not_null pretty_print); sub new { my $class = shift @_; - + my $self = { pre_filters => [ [\&blank_prefilter] ], evaluators => [], post_filters => [ [\&blank_postfilter] ], debug => 0, rh_ans => new AnswerHash, - + }; - + bless $self, $class; - $self->rh_ans(@_); #initialize answer hash + $self->rh_ans(@_); #initialize answer hash return $self; } sub clone { @@ -527,15 +537,15 @@ sub dereference_array_ans { } $rh_ans; } - + sub get_student_answer { my $self = shift; - my $input = shift; + my $input = shift; my %answer_options = @_; my $display_input = $input; $display_input =~ s/\0/\\0/g; # make null spacings visible eval (q!main::DEBUG_MESSAGE( "Raw student answer is |$display_input|")!) if $self->{debug}; - $input = '' unless defined($input); + $input = '' unless defined($input); if (ref($input) =~/AnswerHash/) { # in this case nothing needs to be done, since the student's answer is already in an answerhash. # This is useful when an AnswerEvaluator is used as a filter in another answer evaluator. @@ -544,17 +554,17 @@ sub get_student_answer { $self-> {rh_ans} -> {original_student_ans} = " ( " .join(", ",@input) . " ) "; $input = \@input; $self-> {rh_ans} -> {student_ans} = $input; - } elsif (ref($input) eq 'ARRAY' ) { # sometimes the answer may already be decoded into an array. + } elsif (ref($input) eq 'ARRAY' ) { # sometimes the answer may already be decoded into an array. my @input = @$input; $self-> {rh_ans} -> {original_student_ans} = " ( " .join(", ",@input) . " ) "; $input = \@input; $self-> {rh_ans} -> {student_ans} = $input; } else { - + $self-> {rh_ans} -> {original_student_ans} = $input; $self-> {rh_ans} -> {student_ans} = $input; } - $self->{rh_ans}->{ans_label} = $answer_options{ans_label} if defined($answer_options{ans_label}); + $self->{rh_ans}->{ans_label} = $answer_options{ans_label} if defined($answer_options{ans_label}); $self->{rh_ans}->{_filter_name} = 'get_student_answer'; $input; } @@ -572,12 +582,12 @@ sub evaluate { $self->get_student_answer(@_); # dereference $self->{rh_ans}; my $rh_ans = $self ->{rh_ans}; - $rh_ans->{error_flag}=undef; #reset the error flags in case + $rh_ans->{error_flag}=undef; #reset the error flags in case $rh_ans->{done}=undef; #the answer evaluator is called twice - + eval (q!main::DEBUG_MESSAGE( "

Answer evaluator information:

")!) if defined($self->{debug}) and $self->{debug}>0; $self->print_result_if_debug('pre_filter',$rh_ans); - + my @prefilters = @{$self -> {pre_filters}}; $count = 0; # the get student answer filter is counted as filter -1 foreach my $i (@prefilters) { @@ -630,7 +640,7 @@ sub print_result_if_debug { my $name = (defined($rh_ans->{_filter_name})) ? $rh_ans->{_filter_name}: 'unnamed'; eval (q! main::DEBUG_MESSAGE( "\n $count. Result from queue $queue: name: \"$name\"n", pretty_print($rh_ans,'html',4)) !); - ++$count; + ++$count; } $rh_ans->{_filter_name} = undef; } @@ -639,7 +649,7 @@ sub print_result_if_debug { # sub correct_answer_evaluate { # my $self = shift; # $self-> {rh_ans} -> {correct_ans} = shift @_; -# my $rh_ans = $self ->{rh_ans}; +# my $rh_ans = $self ->{rh_ans}; # my @prefilters = @{$self -> {correct_answer_pre_filters}}; # my $count = -1; # the blank filter is counted as filter 0 # foreach my $i (@prefilters) { @@ -669,7 +679,7 @@ sub print_result_if_debug { # $rh_ans = &$filter($rh_ans,@array); # warn "Filter Name:", $rh_ans->{_filter_name},"
\n" if $self->{debug}>0 and defined($rh_ans->{_filter_name}) # } -# $rh_ans = $self->dereference_array_ans($rh_ans); +# $rh_ans = $self->dereference_array_ans($rh_ans); # # make sure that the student answer is not an array so that it is reported correctly in answer section. # warn "final result: ", $self->{rh_ans}->pretty_print() if defined($self->{debug}) and $self->{debug}>0; # $self ->{rh_ans} = $rh_ans; @@ -811,7 +821,7 @@ sub withPostFilter { sub ans_hash { #alias for rh_ans my $self = shift; $self->rh_ans(@_); -} +} sub rh_ans { my $self = shift; my %in_hash = @_; @@ -823,11 +833,11 @@ sub rh_ans { =head1 Description: Filters -A filter is a subroutine which takes one AnswerHash as an input, followed by +A filter is a subroutine which takes one AnswerHash as an input, followed by a hash of options. Usage: filter($ans_hash, option1 =>value1, option2=> value2 ); - + The filter performs some operations on the input AnswerHash and returns an AnswerHash as output. @@ -837,17 +847,17 @@ three queues: pre_filters: these normalize student input, prepare text and so forth evaluators: these decide whether or not an answer is correct - post_filters: typically these clean up error messages or process errors + post_filters: typically these clean up error messages or process errors and generate error messages. If a filter detects an error it can throw an error message using the C<$rh_ans->throw_error()> method. This skips the AnswerHash by all remaining pre_filter C<$rh_ans->catch_error>, decides how ( or whether) it is supposed to handle the error and then passes the result on -to the next post_filter. +to the next post_filter. -Setting the flag C<$rh_ans->{done} = 1> will skip -the AnswerHash past the remaining post_filters. +Setting the flag C<$rh_ans->{done} = 1> will skip +the AnswerHash past the remaining post_filters. =head3 Built in filters @@ -867,7 +877,7 @@ the AnswerHash past the remaining post_filters. sub blank_prefilter { # check for blanks - my $rh_ans = shift; + my $rh_ans = shift; $rh_ans->{_filter_name} = 'blank_prefilter'; # undefined answers are BLANKS ( not defined($rh_ans->{student_ans}) ) && do {$rh_ans->throw_error("BLANK", 'The answer is blank'); @@ -882,7 +892,7 @@ sub blank_prefilter { # check for blanks $rh_ans; }; -sub blank_postfilter { +sub blank_postfilter { my $rh_ans=shift; $rh_ans->{_filter_name} = 'blank_postfilter'; return($rh_ans) unless defined($rh_ans->{error_flag}) and $rh_ans->{error_flag} eq 'BLANK'; diff --git a/lib/DragNDrop.pm b/lib/DragNDrop.pm index 591e1e4cf8..fc08c37317 100644 --- a/lib/DragNDrop.pm +++ b/lib/DragNDrop.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2021 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/LaTeXImage.pm b/lib/LaTeXImage.pm index aec9db1b4c..43ea3317f1 100644 --- a/lib/LaTeXImage.pm +++ b/lib/LaTeXImage.pm @@ -1,13 +1,13 @@ #!/bin/perl ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the # Free Software Foundation; either version 2, or (at your option) any later # version, or (b) the "Artistic License" which comes with this package. -# +# # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the diff --git a/lib/PGEnvironment.pm b/lib/PGEnvironment.pm index e4944b9a16..51c382c13e 100644 --- a/lib/PGEnvironment.pm +++ b/lib/PGEnvironment.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2021 The WeBWorK Project, http://github.com/openwebwork +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the @@ -62,7 +62,8 @@ sub new { # Load from the conf file. $self->{pg_dir} = $ENV{PG_ROOT}; - my $defaults_file = $self->{pg_dir} . "/conf/pg_defaults.yml"; + my $defaults_file = "$self->{pg_dir}/conf/pg_defaults.yml"; + $defaults_file = "$self->{pg_dir}/conf/pg_defaults.dist.yml" unless -r $defaults_file; die "Cannot read the configuration file $defaults_file" unless -r $defaults_file; my $options = LoadFile($defaults_file); diff --git a/lib/PGUtil.pm b/lib/PGUtil.pm index 06dcee6c75..b2a92e0ad0 100644 --- a/lib/PGUtil.pm +++ b/lib/PGUtil.pm @@ -1,13 +1,12 @@ ############################################################################### # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGcore.pm,v 1.6 2010/05/25 22:47:52 gage Exp $ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the # Free Software Foundation; either version 2, or (at your option) any later # version, or (b) the "Artistic License" which comes with this package. -# +# # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the diff --git a/lib/PGalias.pm b/lib/PGalias.pm index a2b1556719..9216e69120 100644 --- a/lib/PGalias.pm +++ b/lib/PGalias.pm @@ -1,7 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGalias.pm,v 1.6 2010/05/15 18:41:23 gage Exp $ +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/PGanswergroup.pm b/lib/PGanswergroup.pm index 14a8eff61c..bfd84893b0 100644 --- a/lib/PGanswergroup.pm +++ b/lib/PGanswergroup.pm @@ -1,13 +1,12 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGanswergroup.pm,v 1.1 2010/05/14 11:39:02 gage Exp $ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the # Free Software Foundation; either version 2, or (at your option) any later # version, or (b) the "Artistic License" which comes with this package. -# +# # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the diff --git a/lib/PGcore.pm b/lib/PGcore.pm index 446010a40b..57e44d9319 100755 --- a/lib/PGcore.pm +++ b/lib/PGcore.pm @@ -1,7 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGcore.pm,v 1.6 2010/05/25 22:47:52 gage Exp $ +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/PGloadfiles.pm b/lib/PGloadfiles.pm index 035329d2d0..f13c339a6d 100644 --- a/lib/PGloadfiles.pm +++ b/lib/PGloadfiles.pm @@ -1,13 +1,12 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.org -# $CVSHeader: pg/lib/PGloadfiles.pm,v 1.1 2010/05/14 11:39:02 gage Exp $ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the # Free Software Foundation; either version 2, or (at your option) any later # version, or (b) the "Artistic License" which comes with this package. -# +# # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the diff --git a/lib/PGresource.pm b/lib/PGresource.pm index 2d35fdb943..d6208f9ac1 100644 --- a/lib/PGresource.pm +++ b/lib/PGresource.pm @@ -1,13 +1,12 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGalias.pm,v 1.6 2010/05/15 18:41:23 gage Exp $ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the # Free Software Foundation; either version 2, or (at your option) any later # version, or (b) the "Artistic License" which comes with this package. -# +# # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the diff --git a/lib/PGresponsegroup.pm b/lib/PGresponsegroup.pm index 27d093efb7..33f236ef56 100644 --- a/lib/PGresponsegroup.pm +++ b/lib/PGresponsegroup.pm @@ -1,13 +1,12 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: pg/lib/PGresponsegroup.pm,v 1.2 2010/05/25 22:13:52 gage Exp $ -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the # Free Software Foundation; either version 2, or (at your option) any later # version, or (b) the "Artistic License" which comes with this package. -# +# # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the diff --git a/lib/Parser/UOP/factorial.pm b/lib/Parser/UOP/factorial.pm index 65600fec45..a5f8add846 100644 --- a/lib/Parser/UOP/factorial.pm +++ b/lib/Parser/UOP/factorial.pm @@ -25,6 +25,7 @@ sub _eval { my $n = shift; my $f = 1; $self->Error("Factorial can only be taken of (non-negative) integers") unless $n =~ m/^\d+$/; + return $self->Package("Infinity")->new() if $n > 170; while ($n > 0) {$f *= $n; $n--} return $f; } diff --git a/lib/Statistics.pm b/lib/Statistics.pm index b06084abd1..b759ff3a56 100644 --- a/lib/Statistics.pm +++ b/lib/Statistics.pm @@ -1,15 +1,13 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2013 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: $ -# Author: Kelly Black - kjblack@gmail.com -# +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the # Free Software Foundation; either version 2, or (at your option) any later # version, or (b) the "Artistic License" which comes with this package. -# +# # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the diff --git a/lib/Units.pm b/lib/Units.pm index 50830b868f..a2dc07505f 100644 --- a/lib/Units.pm +++ b/lib/Units.pm @@ -475,9 +475,12 @@ our %known_units = ('m' => { # cal -- calorie # kcal -- kilocalorie # eV -- electron volt +# keV -- kilo electron volt +# MeV -- mega electron volt +# GeV -- giga electron volt # kWh -- kilo Watt hour # - 'J' => { + 'J' => { 'factor' => 1, 'm' => 2, 'kg' => 1, @@ -501,13 +504,13 @@ our %known_units = ('m' => { 'kg' => 1, 's' => -2 }, - 'kt' => { + 'kt' => { 'factor' => 4.184E12, 'm' => 2, 'kg' => 1, 's' => -2 }, - 'Mt' => { + 'Mt' => { 'factor' => 4.184E15, 'm' => 2, 'kg' => 1, @@ -525,8 +528,26 @@ our %known_units = ('m' => { 'kg' => 1, 's' => -2 }, - 'eV' => { - 'factor' => 1.60E-9, + 'eV' => { + 'factor' => 1.6022E-19, + 'm' => 2, + 'kg' => 1, + 's' => -2 + }, + 'keV' => { + 'factor' => 1.6022E-16, + 'm' => 2, + 'kg' => 1, + 's' => -2 + }, + 'MeV' => { + 'factor' => 1.6022E-13, + 'm' => 2, + 'kg' => 1, + 's' => -2 + }, + 'GeV' => { + 'factor' => 1.6022E-10, 'm' => 2, 'kg' => 1, 's' => -2 diff --git a/lib/WeBWorK/EquationCache.pm b/lib/WeBWorK/EquationCache.pm index db144de5f6..9e886c423b 100644 --- a/lib/WeBWorK/EquationCache.pm +++ b/lib/WeBWorK/EquationCache.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod_perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. ################################################################################ package WeBWorK::EquationCache; @@ -63,7 +73,7 @@ sub new { my $self = { %options, }; - + bless $self, $class; } @@ -90,14 +100,14 @@ sub lookup { # $tex =~ s/\s+//g; my $md5 = md5_hex(encode_utf8($tex)); - + my $db = $self->{cacheDB}; unless($db) { return($md5 ."1"); } sysopen(DB, $db, O_RDWR|O_CREAT) or die "failed to create/open cacheDB $db: $!"; flock(DB, LOCK_EX) or die "failed to write-lock cacheDB $db: $!"; - + my $line = 0; my $max = 0; my $match = 0; @@ -114,14 +124,14 @@ sub lookup { $max = $1 if $1 > $max; } } - + unless ($match) { # no match: invent a new instance number and add TeX string to DB $match = $max + 1; seek(DB, 0, 2); # we should already be at EOF, but what the hell. print DB "$md5\t$match\t$tex\n"; } - + close(DB); return "$md5$match"; } diff --git a/lib/WeBWorK/PG/IO.pm b/lib/WeBWorK/PG/IO.pm index 4d30da0bed..4364e6c7fd 100644 --- a/lib/WeBWorK/PG/IO.pm +++ b/lib/WeBWorK/PG/IO.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod_perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. ################################################################################ package WeBWorK::PG::IO; diff --git a/lib/WeBWorK/PG/IO/Daemon2.pm b/lib/WeBWorK/PG/IO/Daemon2.pm index 8b446acf9f..7afd5e8939 100644 --- a/lib/WeBWorK/PG/IO/Daemon2.pm +++ b/lib/WeBWorK/PG/IO/Daemon2.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod-perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. ################################################################################ package WeBWorK::PG::IO::Daemon2; diff --git a/lib/WeBWorK/PG/IO/WW1.pm b/lib/WeBWorK/PG/IO/WW1.pm index e400aeb22d..faf06ca36b 100644 --- a/lib/WeBWorK/PG/IO/WW1.pm +++ b/lib/WeBWorK/PG/IO/WW1.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod-perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. ################################################################################ package WeBWorK::PG::IO::WW1; @@ -22,7 +32,7 @@ BEGIN { send_mail_to surePathToTmpFile ); - + $WeBWorK::PG::IO::SHARE{$_} = __PACKAGE__ foreach @EXPORT; } @@ -46,30 +56,30 @@ in a new environment. It uses the C module. sub send_mail_to { my $user_address = shift; # user must be an instructor my %options = @_; - + my $subject = ''; $subject = $options{'subject'} if defined($options{'subject'}); - + my $msg_body = ''; $msg_body =$options{'body'} if defined($options{'body'}); - + my @mail_to_allowed_list = (); @mail_to_allowed_list = @{ $options{'ALLOW_MAIL_TO'} } if defined($options{'ALLOW_MAIL_TO'}); my $out; - + # check whether user is an instructor my $mailing_allowed_flag = 0; - + while (@mail_to_allowed_list) { if ($user_address eq shift @mail_to_allowed_list ) { $mailing_allowed_flag =1; last; } } - + my $REMOTE_HOST = (defined( $ENV{'REMOTE_HOST'} ) ) ? $ENV{'REMOTE_HOST'}: 'unknown host'; my $REMOTE_ADDR = (defined( $ENV{'REMOTE_ADDR'}) ) ? $ENV{'REMOTE_ADDR'}: 'unknown address'; - + if ($mailing_allowed_flag) { ## mail header text: my $email_msg ="To: $user_address\n" @@ -79,7 +89,7 @@ sub send_mail_to { my $smtp = Net::SMTP->new($Global::smtpServer, Timeout=>10) or warn "Couldn't contact SMTP server."; $smtp->mail($Global::webmaster); - + if ( $smtp->recipient($user_address)) { # this one's okay, keep going $smtp->data( $email_msg) or warn("Unknown problem sending message data to SMTP server."); @@ -95,7 +105,7 @@ sub send_mail_to { . "Permitted addresses are specified in the courseWeBWorK.ph file."; $out = 0; } - + return $out; } diff --git a/lib/WeBWorK/PG/IO/WW2.pm b/lib/WeBWorK/PG/IO/WW2.pm index 0c7676c16c..223d950c97 100644 --- a/lib/WeBWorK/PG/IO/WW2.pm +++ b/lib/WeBWorK/PG/IO/WW2.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod-perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. ################################################################################ package WeBWorK::PG::IO::WW2; diff --git a/lib/WeBWorK/PG/ImageGenerator.pm b/lib/WeBWorK/PG/ImageGenerator.pm index 5d38de4053..2b9d4068ce 100644 --- a/lib/WeBWorK/PG/ImageGenerator.pm +++ b/lib/WeBWorK/PG/ImageGenerator.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod_perl (c) 2000-2002 WeBWorK Project -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. ################################################################################ package WeBWorK::PG::ImageGenerator; @@ -175,14 +185,14 @@ sub new { $self->{dvipng_align} = 'absmiddle' unless defined($self->{dvipng_align}); $self->{store_depths} = 1 if ($self->{dvipng_align} eq 'mysql'); $self->{useMarkers} = $self->{useMarkers} || 0; - + if ($self->{useCache}) { $self->{dir} = $self->{cacheDir}; $self->{url} = $self->{cacheURL}; $self->{basename} = ""; $self->{equationCache} = WeBWorK::EquationCache->new(cacheDB => $self->{cacheDB}); } - + bless $self, $class; } @@ -194,7 +204,7 @@ For example Will define a question wide style for interpreting \( \myVec{v} \) - + If this statement is placed in PGcourse.pl then the backslashes must be doubled since it is a .pl file not a .pg file @@ -231,7 +241,7 @@ for displaying the image. sub add { my ($self, $string, $mode) = @_; - + my $names = $self->{names}; my $strings = $self->{strings}; my $dir = $self->{dir}; @@ -239,7 +249,7 @@ sub add { my $basename = $self->{basename}; my $useCache = $self->{useCache}; my $depths = $self->{depths}; - + # if the string came in with delimiters, chop them off and set the mode # based on whether they were \[ .. \] or \( ... \). this means that if # the string has delimiters, the mode *argument* is ignored. @@ -249,16 +259,16 @@ sub add { $mode = "inline"; } # otherwise, leave the string and the mode alone. - + # assume that a bare string with no mode specified is inline $mode ||= "inline"; - + # now that we know what mode we're dealing with, we can generate a "real" # string to pass to latex my $realString = ($mode eq "display") ? '\(\displaystyle{' . $string . '}\)' : '\(' . $string . '\)'; - + # alignment tag could be a fixed default my ($imageNum, $aligntag) = (0, qq|align="$self->{dvipng_align}"|); # if the default is for variable heights, the default should be meaningful @@ -277,7 +287,7 @@ sub add { } else { $imageNum = @$strings + 1; } - + # We are banking on the fact that if useCache is true, then basename is empty. # Maybe we should simplify and drop support for useCache =0 and having a basename. @@ -285,15 +295,15 @@ sub add { my $imageName = ($basename) ? "$basename.$imageNum.png" : "$imageNum.png"; - + # store the full file name of the image, and the "real" tex string to the object push @$names, $imageName; push @$strings, $realString; #warn "ImageGenerator: added string $realString with name $imageName\n"; - + # ... and the full URL. my $imageURL = "$url/$imageName"; - + my $safeString = PGcore::encode_pg_and_html($string); my $imageTag = ($mode eq "display") @@ -305,7 +315,7 @@ sub add { =item render(%options) -Uses LaTeX and dvipng to render the equations stored in the object. +Uses LaTeX and dvipng to render the equations stored in the object. The option C is a reference to the text of the problem's text. After rendering the images and figuring out their depths, we go through and fix the tags @@ -328,7 +338,7 @@ NOTE: It's not clear to me that mtime has been implemented -- MEG - 2011/06 sub render { my ($self, %options) = @_; - + my $tempDir = $self->{tempDir}; my $dir = $self->{dir}; my $basename = $self->{basename}; @@ -347,8 +357,8 @@ sub render { my $success = mkdir "$dir"; warn "Could not make directory $dir" unless $success; } - - ############################################### + + ############################################### # determine which images need to be generated ############################################### my (@newStrings, @newNames); @@ -363,12 +373,12 @@ sub render { push @newNames, $name; } } - + if(@newStrings) { # Don't run latex if there are no images to generate - + # create temporary directory in which to do TeX processing my $wd = makeTempDirectory($tempDir, "ImageGenerator"); - + # store equations in a tex file my $texFile = "$wd/equation.tex"; open my $tex, ">", $texFile @@ -379,19 +389,19 @@ sub render { print $tex $TexPostamble; close $tex; warn "tex file $texFile was not written" unless -e $texFile; - + ############################################### # call LaTeX ############################################### my $latexCommand = "cd $wd && $latex equation > latex.out 2> latex.err"; my $latexStatus = system $latexCommand; - + if ($latexStatus and $latexStatus !=256) { warn "$latexCommand returned non-zero status $latexStatus: $!"; warn "cd $wd failed" if system "cd $wd"; warn "Unable to write to directory $wd. " unless -w $wd; warn "Unable to execute $latex " unless -e $latex ; - + warn `ls -l $wd`; my $errorMessage = ''; if (-r "$wd/equation.log") { @@ -401,10 +411,10 @@ sub render { warn "Unable to read logfile $wd/equation.log "; } } - + warn "$latexCommand failed to generate a DVI file" unless -e "$wd/equation.dvi"; - + ############################################ # call dvipng ############################################ @@ -418,29 +428,29 @@ sub render { my @dvipngdepths = ($dvipngout =~ /depth=(\d+)/g); # kill them all if something goes wrnog @dvipngdepths = () if(scalar(@dvipngdepths) != scalar(@newNames)); - + ############################################ # move/rename images ############################################ - + chmod (0664,<$wd/*>); # first make everything group writable so that a WeBWorK admin can delete images foreach my $image (readDirectory($wd)) { # only work on equation#.png files next unless $image =~ m/^equation(\d+)\.png$/; - + # get image number from above match my $imageNum = $1; # note, problems with solutions/hints can have empty values in newNames next unless $newNames[$imageNum-1]; - + # record the dvipng offset my $hashkey = $newNames[$imageNum-1]; $hashkey =~ s|/||; $hashkey =~ s|\.png$||; $depths->{"$hashkey"} = $dvipngdepths[$imageNum-1] if(defined($dvipngdepths[$imageNum-1])); - + #warn "ImageGenerator: found generated image $imageNum with name $newNames[$imageNum-1]\n"; - + # move/rename image #my $mvCommand = "cd $wd && /bin/mv $wd/$image $dir/$basename.$imageNum.png"; # check to see if this requires a directory we haven't made yet @@ -457,12 +467,12 @@ sub render { warn "$mvCommand returned non-zero status $mvStatus: $!"; warn "Can't write to tmp/equations directory $dir" unless -w $dir; } - + } ############################################ # remove temporary directory (and its contents) ############################################ - + if ($PreserveTempFiles) { warn "ImageGenerator: preserved temp files in working directory '$wd'.\n"; chmod (0775,$wd); diff --git a/lib/WeBWorK/PG/Translator.pm b/lib/WeBWorK/PG/Translator.pm index 15725609e0..92fb7f50a6 100644 --- a/lib/WeBWorK/PG/Translator.pm +++ b/lib/WeBWorK/PG/Translator.pm @@ -1,6 +1,16 @@ ################################################################################ -# WeBWorK mod_perl (c) 2000-2012 The Open WeBWorK Project (openwebwork.org) -# $Id$ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of either: (a) the GNU General Public License as published by the +# Free Software Foundation; either version 2, or (at your option) any later +# version, or (b) the "Artistic License" which comes with this package. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the +# Artistic License for more details. ################################################################################ package WeBWorK::PG::Translator; @@ -46,7 +56,7 @@ WeBWorK::PG::Translator - Evaluate PG code and evaluate answers safely # other macros are loaded from within the problem using loadMacros $pt ->translate(); # translate the problem (the out following 4 pieces of information are created) - + $PG_PROBLEM_TEXT_ARRAY_REF = $pt->ra_text(); # output text for the body of the HTML file (in array form) $PG_PROBLEM_TEXT_REF = $pt->r_text(); # output text for the body of the HTML file $PG_HEADER_TEXT_REF = $pt->r_header;#\$PG_HEADER_TEXT; # text for the header of the HTML file @@ -55,7 +65,7 @@ WeBWorK::PG::Translator - Evaluate PG code and evaluate answers safely $PG_FLAGS_REF = $pt ->rh_flags; # misc. status flags. $pt -> process_answers(\%inputs); # evaluates all of the answers using submitted answers from %input - + my $rh_answer_results = $pt->rh_evaluated_answers; # provides a hash of the results of evaluating the answers. my $rh_problem_result = $pt->grade_problem; # grades the problem using the default problem grading method. @@ -149,7 +159,7 @@ BEGIN { require 'ww_strict.pm'; strict::import(); } - + # also define in Main::, for PG modules. sub Main::be_strict { &be_strict } } @@ -175,7 +185,7 @@ sub evaluate_modules { s/\.pm$// and warn "fixing your broken package name: $_.pm => $_"; # call runtime_use on the package name # don't worry -- runtime_use won't load a package twice! - #eval { runtime_use $_ }; # + #eval { runtime_use $_ }; # eval "package Main; require $_; import $_"; # change for WW1 warn "Failed to evaluate module $_: $@" if $@; # record this in the appropriate place @@ -198,7 +208,7 @@ sub load_extra_packages{ my $self = shift; my @package_list = @_; my $package_name; - + foreach (@package_list) { # ensure that the name is in fact a base name s/\.pm$// and warn "fixing your broken package name: $_.pm => $_"; @@ -308,7 +318,7 @@ The macros shared with the safe compartment are =cut # SHARE variables and routines with safe compartment -# +# # Some symbols are defined here (or in the IO module), and used inside the safe # compartment. Under WeBWorK 1.x, functions defined here had access to the # Global:: namespace, which contained course-specific data such things as @@ -317,16 +327,16 @@ The macros shared with the safe compartment are # which need access to course-specific data are now defined in the IO.pl macro # file, which has access to the problem environment. Several entries have been # added to the problem environment to support this move. -# +# # Useful for timing portions of the translating process # The timer $WeBWorK::timer is defined in the module WeBWorK.pm -# You must make sure that the code in that script for initialzing the +# You must make sure that the code in that script for initialzing the # timer is active. sub time_it { - my $msg = shift; + my $msg = shift; $WeBWorK::timer->continue("PG macro:". $msg) if defined($WeBWorK::timer); } @@ -335,14 +345,14 @@ my %Translator_shared_subroutine_hash = ( 'time_it' => __PACKAGE__, '&PG_answer_eval' => __PACKAGE__, '&PG_restricted_eval' => __PACKAGE__, - '&PG_macro_file_eval' => __PACKAGE__, + '&PG_macro_file_eval' => __PACKAGE__, '&be_strict' => __PACKAGE__, '&PGsort' => __PACKAGE__, '&dumpvar' => __PACKAGE__, ); # add names from WeBWorK::PG::IO and WeBWorK::PG::IO::* -my %IO_shared_subroutine_hash = %WeBWorK::PG::IO::SHARE; +my %IO_shared_subroutine_hash = %WeBWorK::PG::IO::SHARE; sub initialize { my $self = shift; @@ -363,7 +373,7 @@ sub initialize { #$safe_cmpt -> share('$rf_answer_eval'); #$safe_cmpt -> share('$rf_restricted_eval'); use strict; - + # The standalone renderer does this when the module is compiled. unless (exists($ENV{MOJO_MODE})) { $safe_cmpt -> share_from('main', $self->{ra_included_modules} ); @@ -461,7 +471,7 @@ sub unrestricted_load { my $store_mask = $safe_cmpt->mask(); $safe_cmpt->mask(Opcode::empty_opset()); my $safe_cmpt_package_name = $safe_cmpt->root(); - + my $macro_file_name = fileFromPath($filePath); $macro_file_name =~s/\.pl//; # trim off the extenstion my $export_subroutine_name = "_${macro_file_name}_export"; @@ -476,11 +486,11 @@ sub unrestricted_load { my $macro_file_loaded = ref($init_subroutine) =~ /CODE/ && defined(&$init_subroutine); - #print STDERR "$macro_file_name has not yet been loaded\n" unless $macro_file_loaded; + #print STDERR "$macro_file_name has not yet been loaded\n" unless $macro_file_loaded; unless ($macro_file_loaded) { ## load the $filePath file ## Using rdo insures that the $filePath file is loaded for every problem, allowing initializations to occur. - ## Ordinary mortals should not be fooling with the fundamental macros in these files. + ## Ordinary mortals should not be fooling with the fundamental macros in these files. my $local_errors = ""; if (-r $filePath ) { my $rdoResult = $safe_cmpt->rdo($filePath); @@ -493,7 +503,7 @@ sub unrestricted_load { $self ->{errors} .= $local_errors if $local_errors; } $safe_cmpt -> mask($store_mask); - + } # try again to define the initization subroutine $init_subroutine = eval { \&{"$init_subroutine_name"} }; @@ -668,7 +678,7 @@ that, so we remove it as well. File names are shortened, when possible, by replacing the templates directory with [TMPL], the WeBWorK root directory by [WW] and the PG root directory by [PG]. - + =cut sub PG_errorMessage { @@ -726,7 +736,7 @@ sub PG_errorMessage { '$GLOBAL_VARIABLE' => \'global', '$t' => \undef, '$s' => \'regular output' - + @@ -740,7 +750,7 @@ sub PG_undef_var_check { local $Data::Dumper::Maxdepth = 2; # takes all lexical variables from caller-nemaspace my $possibles = Data::Dumper::Dumper({ %{PadWalker::peek_my(1)}, %{PadWalker::peek_our(1)} }); - + $possibles ne "\$VAR1 = {};\n" ? ($possibles =~ s/^.*?\n(.*)\n.*?\n$/$1/ms) : ($possibles = ''); return "Warning: " . join(', ', @_) . "Possible variables are:\n$possibles\n"; } @@ -761,20 +771,20 @@ sub translate { $self ->{errors} .= qq{ERROR: This problem file was empty!\n} unless ($evalString) ; $self ->{errors} .= qq{ERROR: You must define the environment before translating.} unless defined( $self->{envir} ); - + # install handlers for warn and die that call PG_errorMessage. # if the existing signal handler is not a coderef, the built-in warn or # die function is called. this does not account for the case where the # handler is set to "IGNORE" or to the name of a function. in these cases # the built-in function will be called. - + my $outer_sig_warn = $SIG{__WARN__}; local $SIG{__WARN__} = sub { ref $outer_sig_warn eq "CODE" ? &$outer_sig_warn(PG_errorMessage('message', $_[0])) : warn PG_errorMessage('message', $_[0]); }; - + my $outer_sig_die = $SIG{__DIE__}; local $SIG{__DIE__} = sub { ref $outer_sig_die eq "CODE" @@ -847,10 +857,10 @@ without warnings. ########################################## ###### PG preprocessing code ############# ########################################## - + $evalString = &{$self->{preprocess_code}}($evalString); - + # # default_preprocess_code # # BEGIN_TEXT and END_TEXT must occur on a line by themselves. # $evalString =~ s/\n\s*END_TEXT[\s;]*\n/\nEND_TEXT\n/g; @@ -873,7 +883,7 @@ case the previously defined safe compartment is used. (See item 1.) my ($PG_PROBLEM_TEXT_REF, $PG_HEADER_TEXT_REF, $PG_POST_HEADER_TEXT_REF,$PG_ANSWER_HASH_REF, $PG_FLAGS_REF, $PGcore) =$safe_cmpt->reval(" $evalString"); - + # This section could use some more error messages. In particular if a problem doesn't produce the right output, the user needs # information about which problem was at fault. @@ -1000,7 +1010,7 @@ the errors. $self ->{ rh_correct_answers } = $PG_ANSWER_HASH_REF; $self ->{ PG_FLAGS_REF } = $PG_FLAGS_REF; $self ->{ rh_pgcore } = $PGcore; - + #warn "PGcore is ", ref($PGcore), " in Translator"; #$self ->{errors}; } # end translate @@ -1082,29 +1092,29 @@ sub process_answers{ # the foreach loop around the conditional involving $rf_fun, but for efficiency # we've moved it out here. This means that the handlers will be active during the # code before and after the actual answer evaluation. - + my $outer_sig_warn = $SIG{__WARN__}; local $SIG{__WARN__} = sub { ref $outer_sig_warn eq "CODE" ? &$outer_sig_warn(PG_errorMessage('message', $_[0])) : warn PG_errorMessage('message', $_[0]); }; - + # the die handler is a closure over %errorTable and $outer_sig_die. - # + # # %errorTable accumulates a "full" error message for each error that occurs during # answer evaluation. then, right after the evaluation (which is done within a call # to Safe::reval), $@ is checked and it's value is looked up in %errorTable to get # the full error to report. - # + # # my question: why is this a hash? this is die, so once one occurs, we exit the reval. # wouldn't it be sufficient to have a scalar like $backtrace_for_last_error? - # + # # Note that %errorTable is cleared for each answer. my %errorTable; my $outer_sig_die = $SIG{__DIE__}; local $SIG{__DIE__} = sub { - + # this chunk taken from dpvc's original handler my $fullerror = PG_errorMessage('traceback', @_); my ($error,$traceback) = split /\n/, $fullerror, 2; @@ -1112,7 +1122,7 @@ sub process_answers{ $error .= "\n"; $errorTable{$error} = $fullerror; # end of dpvc's original code - + ref $outer_sig_die eq "CODE" ? &$outer_sig_die($error) : die $error; @@ -1121,11 +1131,11 @@ sub process_answers{ # apply each instructors answer to the corresponding student answer # my $printAnsHash = 1; # this turns on error printing of the answer hashes - + #$PG->debug_message("Student answers are: ", join(" ", %h_student_answers) ) if $printAnsHash==1; # we will want to redefine the entry order in terms of the answer evaluators - - #### foreach my $ans_name ( @answer_entry_order ) { + + #### foreach my $ans_name ( @answer_entry_order ) { #### @answer_entry_order was created from PG_ANSWERS_HASH which maintains the #### order of its keys @@ -1139,7 +1149,7 @@ sub process_answers{ #################################### # local($rf_fun,$temp_ans)= (undef,undef); -# +# # my ($ans, $errors) = $self->filter_answer( $h_student_answers{$ans_name} ); # $temp_ans = $ans; # $temp_ans = '' unless defined($temp_ans); #make sure that answer is always defined @@ -1147,33 +1157,33 @@ sub process_answers{ # #################################### # # get answer evaluator (old method) # #################################### -# +# # if ( defined($rh_correct_answers ->{$ans_name} ) ) { # $rf_fun = $rh_correct_answers->{$ans_name}; # } else { # warn "There is no answer evaluator for the question labeled $ans_name"; # } # $self->{safe}->share('$rf_fun','$temp_ans'); -# +# #################################### # gather answers and answer evaluator (new method) #################################### local($new_rf_fun,$new_temp_ans) = (undef,undef); - my $answergrp = $PG->{PG_ANSWERS_HASH}->{$ans_name}; #this has all answer evaluators AND answer blanks (just to be sure ) + my $answergrp = $PG->{PG_ANSWERS_HASH}->{$ans_name}; #this has all answer evaluators AND answer blanks (just to be sure ) my $responsegrp = $answergrp->response_obj; ################# # refactor to answer group? ################## - $new_rf_fun = $answergrp->ans_eval; - my ($ans, $errors) = $self->filter_answer( $responsegrp->get_response($ans_name) ); + $new_rf_fun = $answergrp->ans_eval; + my ($ans, $errors) = $self->filter_answer( $responsegrp->get_response($ans_name) ); $new_temp_ans = $ans; # avoid undefined errors in translator my $skip_evaluation=0; if (not defined($new_rf_fun) ) { $PG->warning_message( "No answer evaluator for the question labeled: $ans_name "); $skip_evaluation=1; } elsif (not ref($new_rf_fun) =~ /AnswerEvaluator/ ) { - $PG->warning_message( "Error in Translator.pm::process_answers: Answer $ans_name: + $PG->warning_message( "Error in Translator.pm::process_answers: Answer $ans_name: Unrecognized evaluator type |". ref($rf_fun). "|"); $skip_evaluation=1; } @@ -1182,7 +1192,7 @@ sub process_answers{ $skip_evaluation=1; } $PG->debug_message("Answers associated with $ans_name are $new_temp_ans ref=". ref($new_temp_ans) ) if defined $new_temp_ans and $local_debug; - #FIXME -- this is a hack for handling check boxes. + #FIXME -- this is a hack for handling check boxes. if (ref( $new_temp_ans) =~/HASH/i) { my @tmp = (); foreach my $key (sort keys %$new_temp_ans) { @@ -1192,7 +1202,7 @@ sub process_answers{ $PG->debug_message("Hash answer associated with $ans_name is $new_temp_ans. ref=". ref($new_temp_ans)."
".pretty_print($new_temp_ans) ) if $local_debug; } #FIXME -- hack to allow answers such as <4,6,7> -- < and > have been escaped. - unless( $skip_evaluation) { + unless( $skip_evaluation) { # $new_temp_ans =~ s/\<\;//g; # > # $new_temp_ans =~ s/\&\#91\;/\[/g; # < @@ -1204,12 +1214,12 @@ sub process_answers{ $self->{safe}->share('$new_rf_fun','$new_temp_ans'); - + # clear %errorTable for each problem %errorTable = (); # is the error table being used? perhaps by math objects? - + my ($rh_ans_evaluation_result, $new_rh_ans_evaluation_result); - + if (ref($new_rf_fun) eq 'CODE' ) { # $rh_ans_evaluation_result = $self->{safe} ->reval( '&{ $rf_fun }($temp_ans, ans_label => \''.$ans_name.'\')' ) ; # warn "Error in Translator.pm::process_answers: Answer $ans_name: |$temp_ans|\n $@\n" if $@; @@ -1225,13 +1235,13 @@ sub process_answers{ # warn "Evaluation error: Answer $ans_name:
\n", # $rh_ans_evaluation_result->error_flag(), " :: ", # $rh_ans_evaluation_result->error_message(),"
\n" -# if defined($rh_ans_evaluation_result) +# if defined($rh_ans_evaluation_result) # and defined($rh_ans_evaluation_result->error_flag()); -# +# # $rh_ans_evaluation_result = {%$rh_ans_evaluation_result}; #needed to protect result # # from next evaluation apparently ######################################### - + # Get full traceback, but save it in local variable $errorTable so that # we can add it later. This is because some evaluators use # eval to trap errors and then report them in the message @@ -1246,22 +1256,22 @@ sub process_answers{ $PG->warning_message( "Evaluation error in new process: Answer $ans_name:
\n", $new_rh_ans_evaluation_result->error_flag(), " :: ", $new_rh_ans_evaluation_result->error_message(),"
\n") - if defined($new_rh_ans_evaluation_result) && ref($new_rh_ans_evaluation_result) + if defined($new_rh_ans_evaluation_result) && ref($new_rh_ans_evaluation_result) && defined($new_rh_ans_evaluation_result->error_flag()); } else { $PG->warning_message(" The evaluated answer is not an answer hash ".($new_rh_ans_evaluation_result//'').': |'.ref($new_rh_ans_evaluation_result)."|."); - } + } # $PG->debug_message( $self->{envir}->{'probFileName'} ." new_temp_ans and temp_ans don't agree: ". -# ref($new_temp_ans)." $new_temp_ans ". ref($temp_ans). " $temp_ans".length($new_temp_ans).length($temp_ans)) +# ref($new_temp_ans)." $new_temp_ans ". ref($temp_ans). " $temp_ans".length($new_temp_ans).length($temp_ans)) # unless $new_temp_ans eq $temp_ans; } else { $PG->warning_message( "Answer evaluator $ans_name was not executed due to errors. ", "========"); } -######################################################### +######################################################### # End refactor to answer group ? ############################################# - -######################################################### + +######################################################### # check that old and new answers are the same # foreach my $key (keys %$rh_ans_evaluation_result) { # next unless defined $key; @@ -1272,14 +1282,14 @@ sub process_answers{ # $rh_ans_evaluation_result->{$key}." new: ".$new_rh_ans_evaluation_result->{$key}) # unless $rh_ans_evaluation_result->{$key} eq $new_rh_ans_evaluation_result->{$key}; # } - -######################################################### + +######################################################### #$PG->debug_message("old $ans_name: $rf_fun -- ans: $temp_ans",pretty_print($rh_ans_evaluation_result)) if $printAnsHash==1; - $PG->debug_message("new $ans_name: $new_rf_fun -- ans: - $new_temp_ans",pretty_print($new_rh_ans_evaluation_result)) - if ($PG->{envir}->{inputs_ref}->{showAnsHashInfo})//'' + $PG->debug_message("new $ans_name: $new_rf_fun -- ans: + $new_temp_ans",pretty_print($new_rh_ans_evaluation_result)) + if ($PG->{envir}->{inputs_ref}->{showAnsHashInfo})//'' and ($PG->{envir}->{permissionLevel})//0 >=10; -######################################################### +######################################################### # decide whether to return the new or old answer evaluator hash $rh_ans_evaluation_result = $new_rh_ans_evaluation_result; @@ -1287,7 +1297,7 @@ sub process_answers{ # This warning may not be needed. # unless ( ( ref($rh_ans_evaluation_result) eq 'HASH') or ( ref($rh_ans_evaluation_result) eq 'AnswerHash') ) { # warn "Error in Translator.pm::process_answers: Answer $ans_name:
\n -# Answer evaluators must return a hash or an AnswerHash type, not type |", +# Answer evaluators must return a hash or an AnswerHash type, not type |", # ref($rh_ans_evaluation_result), "|"; # } $rh_ans_evaluation_result ->{ans_message} .= "$errors \n" if $errors; @@ -1519,8 +1529,8 @@ sub filter_answer { my $errors=''; if (ref($ans) eq 'ARRAY') { #handle the case where the answer comes from several inputs with the same name # In many cases this will be passed as a reference to an array - # if it is passed as a single string (separated by \0 characters) as - # some early versions of CGI behave, then + # if it is passed as a single string (separated by \0 characters) as + # some early versions of CGI behave, then # it is unclear what will happen when the answer is filtered. foreach my $item (@{$ans}) { my ($filtered_ans, $error) = &{ $self->{rf_safety_filter} } ($item); @@ -1528,11 +1538,11 @@ sub filter_answer { $errors .= " ". $error if $error; # add error message if error is non-zero. } (\@filtered_answers,$errors); - + } else { &{ $self->{rf_safety_filter} } ($ans); } - + } sub rf_safety_filter { @@ -1592,16 +1602,16 @@ have special significance. C$b} @list> C -sorts the list numerically and lexically respectively. +sorts the list numerically and lexically respectively. If C is used in a problem, before the sort routine is defined in a macro, then -things get badly confused. To correct this the macro PGsort is defined below. It is +things get badly confused. To correct this the macro PGsort is defined below. It is evaluated before the problem template is read. In PGbasicmacros.pl, the two subroutines PGsort sub { $_[0] < $_[1] }, @list; PGsort sub { $_[0] lt $_[1] }, @list; -(called num_sort and lex_sort) provide slightly slower, but safer, routines for the PG language. +(called num_sort and lex_sort) provide slightly slower, but safer, routines for the PG language. (The subroutines for ordering are B. Note the commas!) =cut @@ -1690,10 +1700,10 @@ sub PG_restricted_eval_helper { sub PG_macro_file_eval { # would like to modify this so that it requires use strict on the files that it evaluates. my $string = shift; my ($pck,$file,$line) = caller; - + local $SIG{__WARN__} = "DEFAULT"; local $SIG{__DIE__} = "DEFAULT"; - + no strict; my $out = eval ("package main; be_strict();\n" . $string ); my $errors =$@; @@ -1701,7 +1711,7 @@ sub PG_macro_file_eval { # would like to modify this so that it requires us . $errors . "The calling package is $pck\n" if defined($errors) && $errors =~/\S/; use strict; - + return (wantarray) ? ($out, $errors,$full_error_report) : $out; } =head2 PG_answer_eval @@ -1725,20 +1735,20 @@ sub PG_answer_eval { my($string) = shift; # I made this local just in case -- see PG_restricted_eval my $errors = ''; my $full_error_report = ''; - my ($pck,$file,$line) = caller; + my ($pck,$file,$line) = caller; # Because of the global variable $PG::compartment_name and $PG::safe_cmpt # only one problem safe compartment can be active at a time. # This might cause problems at some point. In that case a cleverer way # of insuring that the package stays in scope until the answer is evaluated # will be required. - + # This is pretty tricky and doesn't always work right. # We seem to need PG_priv instead of main when PG_answer_eval is called within a completion # 'package PG_priv; ' - + local $SIG{__WARN__} = sub {die(@_)}; # make warn die, so all errors are reported. local $SIG{__DIE__} = "DEFAULT"; - + no strict; my $out = eval('package main;'.$string); $out = '' unless defined($out); @@ -1747,7 +1757,7 @@ sub PG_answer_eval { $errors The calling package is $pck\n" if defined($errors) && $errors =~/\S/; use strict; - + return (wantarray) ? ($out, $errors,$full_error_report) : $out; @@ -1759,7 +1769,7 @@ sub PG_answer_eval { # $evalString =~ s/\n\s*END_TEXT[\s;]*\n/\nEND_TEXT\n/g; # $evalString =~ s/\n\s*BEGIN_TEXT[\s;]*\n/\nTEXT\(EV3\(<<'END_TEXT'\)\);\n/g; # $evalString =~ s/ENDDOCUMENT.*/ENDDOCUMENT();/s; # remove text after ENDDOCUMENT -# +# # $evalString =~ s/\\/\\\\/g; # \ can't be used for escapes because of TeX conflict # $evalString =~ s/~~/\\/g; # use ~~ as escape instead, use # for comments # $evalString; @@ -1794,33 +1804,33 @@ sub default_postprocess_code { $evalString_ref; } -no strict; +no strict; sub dumpvar { my ($packageName) = @_; local(*alias); - + sub emit { print @_; } - + *stash = *{"${packageName}::"}; $, = " "; - + emit "Content-type: text/html; charset=UTF-8\n\n
\n";
-    
-    
+
+
     while ( ($varName, $globValue) = each %stash) {
         emit "$varName\n";
-        
+
 	*alias = $globValue;
 	next if $varName=~/main/;
-	
+
 	#if (defined($alias) ) {  # get rid of defined since this is deprecated
 	if ($alias ) {
 	    emit "  \$$varName $alias \n";
 	}
-	
+
 	if ( @alias) {
 	    emit "  \@$varName @alias \n";
 	}
diff --git a/macros/LinearProgramming.pl b/macros/LinearProgramming.pl
index ea7c8f7070..d1edca911a 100644
--- a/macros/LinearProgramming.pl
+++ b/macros/LinearProgramming.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/MathObjects.pl b/macros/MathObjects.pl
index b1a17b7fe4..6c8392ecdf 100644
--- a/macros/MathObjects.pl
+++ b/macros/MathObjects.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/PG.pl b/macros/PG.pl
index 4228bb852f..c3d5f33a8d 100644
--- a/macros/PG.pl
+++ b/macros/PG.pl
@@ -475,8 +475,11 @@ sub ENDDOCUMENT {
 				my $data_mq_opts = scalar(keys %$mq_part_opts)
 					? qq!data-mq-opts="@{[encode_pg_and_html(JSON->new->encode($mq_part_opts))]}"!
 					: "";
-				TEXT(MODES(TeX => "",
-						HTML => qq!!));
+				TEXT(MODES(
+					TeX => "",
+					PTX => "",
+					HTML => qq!!
+				));
 			}
 		}
 	}
@@ -913,8 +916,7 @@ sub includePGproblem {
 
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/PG.pl,v 1.46 2010/05/27 02:22:51 gage Exp $
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
diff --git a/macros/PGanswermacros.pl b/macros/PGanswermacros.pl
index cf3e3ef005..f2d23cbf95 100644
--- a/macros/PGanswermacros.pl
+++ b/macros/PGanswermacros.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/PGanswermacros.pl,v 1.72 2010/02/01 01:33:05 apizer Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/PGauxiliaryFunctions.pl b/macros/PGauxiliaryFunctions.pl
index b813e81524..53042ec4a6 100644
--- a/macros/PGauxiliaryFunctions.pl
+++ b/macros/PGauxiliaryFunctions.pl
@@ -1,12 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2021 The WeBWorK Project, http://openwebwork.sf.net/
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/PGbasicmacros.pl b/macros/PGbasicmacros.pl
index 94ccfe0c81..14599bf583 100644
--- a/macros/PGbasicmacros.pl
+++ b/macros/PGbasicmacros.pl
@@ -1,6 +1,6 @@
 ################################################################################
-# WeBWorK Program Generation Language
-# Copyright © 2000-2020 The WeBWorK Project, http://openwebwork.sf.net/
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
diff --git a/macros/PGchoicemacros.pl b/macros/PGchoicemacros.pl
index 997d9837f6..1ed09e5550 100644
--- a/macros/PGchoicemacros.pl
+++ b/macros/PGchoicemacros.pl
@@ -1,7 +1,6 @@
 ################################################################################
-# WeBWorK Program Generation Language
-# Copyright � 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
diff --git a/macros/PGcomplexmacros.pl b/macros/PGcomplexmacros.pl
index 7ad47e505e..16e89ca03e 100644
--- a/macros/PGcomplexmacros.pl
+++ b/macros/PGcomplexmacros.pl
@@ -1,13 +1,20 @@
 # This file     is PGcomplexmacros.pl
 # This includes the subroutines for the ANS macros, that
 # is, macros allowing a more flexible answer checking
-####################################################################
-# Copyright @ 1995-2002 The WeBWorK Team
-# All Rights Reserved
-####################################################################
-#$Id$
-
-
+################################################################################
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of either: (a) the GNU General Public License as published by the
+# Free Software Foundation; either version 2, or (at your option) any later
+# version, or (b) the "Artistic License" which comes with this package.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
+# Artistic License for more details.
+################################################################################
 
 =head1 NAME
 
diff --git a/macros/PGcomplexmacros2.pl b/macros/PGcomplexmacros2.pl
index e307d004e5..d42cf9d242 100644
--- a/macros/PGcomplexmacros2.pl
+++ b/macros/PGcomplexmacros2.pl
@@ -2,10 +2,20 @@
 # This file     is PGcomplexmacros2.pl
 # This includes the subroutines for the ANS macros, that
 # is, macros allowing a more flexible answer checking
-####################################################################
-# Copyright @ 2006-2007 The WeBWorK Team
-# All Rights Reserved
-####################################################################
+################################################################################
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of either: (a) the GNU General Public License as published by the
+# Free Software Foundation; either version 2, or (at your option) any later
+# version, or (b) the "Artistic License" which comes with this package.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
+# Artistic License for more details.
+################################################################################
 
 
 =head1 NAME
diff --git a/macros/PGessaymacros.pl b/macros/PGessaymacros.pl
index 164409ab63..6abfab559c 100644
--- a/macros/PGessaymacros.pl
+++ b/macros/PGessaymacros.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright � 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/PGanswermacros.pl,v 1.72 2010/02/01 01:33:05 apizer Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/PGfunctionevaluators.pl b/macros/PGfunctionevaluators.pl
index ae91f4faca..bfcecdf704 100644
--- a/macros/PGfunctionevaluators.pl
+++ b/macros/PGfunctionevaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/PGinfo.pl b/macros/PGinfo.pl
index ad0a0e0291..d72c19be67 100644
--- a/macros/PGinfo.pl
+++ b/macros/PGinfo.pl
@@ -1,8 +1,18 @@
 
-####################################################################
-# Copyright @ 1995-2007 University of Rochester
-# All Rights Reserved
-####################################################################
+################################################################################
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of either: (a) the GNU General Public License as published by the
+# Free Software Foundation; either version 2, or (at your option) any later
+# version, or (b) the "Artistic License" which comes with this package.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
+# Artistic License for more details.
+################################################################################
 
 =head1 NAME
 
@@ -110,4 +120,4 @@ sub pp {
     my $hash = shift;
     "printing |". ref($hash)."|$BR". pretty_print($hash);
 }
-1;
\ No newline at end of file
+1;
diff --git a/macros/PGlateximage.pl b/macros/PGlateximage.pl
index 59df97a77e..660b9c1082 100644
--- a/macros/PGlateximage.pl
+++ b/macros/PGlateximage.pl
@@ -1,12 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/PGmiscevaluators.pl b/macros/PGmiscevaluators.pl
index ef9c8e0a9d..d23c140b19 100644
--- a/macros/PGmiscevaluators.pl
+++ b/macros/PGmiscevaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/PGnumericevaluators.pl b/macros/PGnumericevaluators.pl
index 684806eab7..12caad6bbb 100644
--- a/macros/PGnumericevaluators.pl
+++ b/macros/PGnumericevaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/PGstringevaluators.pl b/macros/PGstringevaluators.pl
index af4ebb1b3f..34f690cb06 100644
--- a/macros/PGstringevaluators.pl
+++ b/macros/PGstringevaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/PGtextevaluators.pl b/macros/PGtextevaluators.pl
index c3ac67728c..91648dfd77 100644
--- a/macros/PGtextevaluators.pl
+++ b/macros/PGtextevaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/PGtikz.pl b/macros/PGtikz.pl
index 851fd87f77..a6583a638d 100644
--- a/macros/PGtikz.pl
+++ b/macros/PGtikz.pl
@@ -1,12 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/Parser.pl b/macros/Parser.pl
index 6c94f98e54..e2b60d6b11 100644
--- a/macros/Parser.pl
+++ b/macros/Parser.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/answerComposition.pl b/macros/answerComposition.pl
index 84930396da..f63fbcb1fc 100644
--- a/macros/answerComposition.pl
+++ b/macros/answerComposition.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/answerComposition.pl,v 1.8 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/answerCustom.pl b/macros/answerCustom.pl
index 2b166f9ef9..46ebaee69d 100644
--- a/macros/answerCustom.pl
+++ b/macros/answerCustom.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/answerHints.pl b/macros/answerHints.pl
index ad7faaf820..9ad34f7346 100644
--- a/macros/answerHints.pl
+++ b/macros/answerHints.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
@@ -114,59 +113,81 @@ =head1 AnswerHints()
 
 =cut
 
-sub _answerHints_init {}
+sub _answerHints_init { }
 
 sub AnswerHints {
-  return (sub {
-    my $ans = shift; $ans->{_filter_name} = "Answer Hints Post Filter";
-    my $correct = $ans->{correct_value};
-    my $student = $ans->{student_value};
-    Value::Error("AnswerHints can only be used with MathObjects answer checkers") unless ref($correct);
-    return $ans unless ref($student);
-    my $context = $correct->context;
-    my $hash = $context->{answerHash};
-    $context->{answerHash} = $ans;
-    my $processPreview = $correct->getFlag('answerHintsProcessPreview', 0);
-    $context->{answerHash} = $hash;
-
-    while (@_) {
-      my $wrongList = shift; my $message = shift; my @options;
-      ($message,@options) = @{$message} if ref($message) eq 'ARRAY';
-      my %options = (
-        checkCorrect => 0,
-        replaceMessage => 0,
-	checkTypes => 1,
-        processPreview => $processPreview,
-        score => undef,
-        cmp_options => [],
-        @options,
-      );
-      next if $options{checkTypes} && $correct->type ne $student->type;
-      next if !$options{processPreview} && $ans->{isPreview};
-      $wrongList = [$wrongList] unless ref($wrongList) eq 'ARRAY';
-      foreach my $wrong (@{$wrongList}) {
-	if (ref($wrong) eq 'CODE') {
-	  if (($ans->{score} < 1 || $options{checkCorrect}) &&
-	      ($ans->{ans_message} eq "" || $options{replaceMessage}) &&
-	      &$wrong($correct,$student,$ans)) {
-	    $ans->{ans_message} = $ans->{error_message} = $message;
-	    $ans->{score} = $options{score} if defined $options{score};
-	    last;
-	  }
-	} else {
-	  $wrong = Value::makeValue($wrong);
-	  if (($ans->{score} < 1 || $options{checkCorrect} || AnswerHints::Compare($correct,$wrong,$ans)) &&
-	      ($ans->{ans_message} eq "" || $options{replaceMessage}) &&
-	      AnswerHints::Compare($wrong,$student,$ans,@{$options{cmp_options}})) {
-	    $ans->{ans_message} = $ans->{error_message} = $message;
-	    $ans->{score} = $options{score} if defined $options{score};
-	    last;
-	  }
-	}
-      }
-    }
-    return $ans;
-  },@_);
+	return (
+		sub {
+			my $ans = shift;
+			$ans->{_filter_name} = "Answer Hints Post Filter";
+			my $correct = $ans->{correct_value};
+			my $student = $ans->{student_value};
+			Value::Error("AnswerHints can only be used with MathObjects answer checkers") unless ref($correct);
+			return $ans unless ref($student);
+			my $context = $correct->context;
+			my $hash    = $context->{answerHash};
+			$context->{answerHash} = $ans;
+			my $processPreview = $correct->getFlag('answerHintsProcessPreview', 0);
+			$context->{answerHash} = $hash;
+
+			while (@_) {
+				my $wrongList = shift;
+				my $message   = shift;
+				my @options;
+				($message, @options) = @{$message} if ref($message) eq 'ARRAY';
+				my %options = (
+					checkCorrect   => 0,
+					replaceMessage => 0,
+					checkTypes     => 1,
+					processPreview => $processPreview,
+					score          => undef,
+					cmp_options    => [],
+					@options,
+				);
+				next if $options{checkTypes}      && $correct->type ne $student->type;
+				next if !$options{processPreview} && $ans->{isPreview};
+				$wrongList = [$wrongList] unless ref($wrongList) eq 'ARRAY';
+
+				foreach my $wrong (@{$wrongList}) {
+					if (ref($wrong) eq 'CODE') {
+						if (   ($ans->{score} < 1 || $options{checkCorrect})
+							&& ($ans->{ans_message} eq "" || $options{replaceMessage}))
+						{
+							# Make the call to run the function inside an eval to trap errors
+							my $myResult = 0;
+							eval { $myResult = &$wrong($correct, $student, $ans); 1; } or do {
+								warn "An error occurred in this problem.";
+								last;
+							};
+							if ($myResult) {
+								$ans->{ans_message} = $ans->{error_message} = $message;
+								$ans->{score}       = $options{score} if defined $options{score};
+								last;
+							}
+						}
+					} else {
+						$wrong = Value::makeValue($wrong);
+						if (
+							(
+								   $ans->{score} < 1
+								|| $options{checkCorrect}
+								|| AnswerHints::Compare($correct, $wrong, $ans)
+							)
+							&& ($ans->{ans_message} eq "" || $options{replaceMessage})
+							&& AnswerHints::Compare($wrong, $student, $ans, @{ $options{cmp_options} })
+							)
+						{
+							$ans->{ans_message} = $ans->{error_message} = $message;
+							$ans->{score}       = $options{score} if defined $options{score};
+							last;
+						}
+					}
+				}
+			}
+			return $ans;
+		},
+		@_
+	);
 }
 
 package AnswerHints;
@@ -176,23 +197,27 @@ package AnswerHints;
 #  and returns true if the two values match and false otherwise.
 #
 sub Compare {
-  my $self = shift; my $other = shift; my $ans = shift;
-  $ans = bless {%{$ans},@_}, ref($ans);  # make a copy
-  $ans->{typeError} = 0; $ans->{ans_message} = $ans->{error_message} = ""; $ans->{score} = 0;
-  if (sprintf("%p",$self) ne sprintf("%p",$ans->{correct_value})) {
-    $ans->{correct_ans} = $self->string;
-    $ans->{correct_value} = $self;
-    $ans->{correct_formula} = Value->Package("Formula")->new($self);
-  }
-  if (sprintf("%p",$other) ne sprintf("%p",$ans->{student_value})) {
-    $ans->{student_ans} = $other->string;
-    $ans->{student_value} = $other;
-    $ans->{student_formula} = Value->Package("Formula")->new($other);
-  }
-  $self->cmp_preprocess($ans);
-  $self->cmp_equal($ans);
-  $self->cmp_postprocess($ans) if !$ans->{error_message} && !$ans->{typeError};
-  return $ans->{score} >= 1;
+	my $self  = shift;
+	my $other = shift;
+	my $ans   = shift;
+	$ans = bless { %{$ans}, @_ }, ref($ans);    # make a copy
+	$ans->{typeError}   = 0;
+	$ans->{ans_message} = $ans->{error_message} = "";
+	$ans->{score}       = 0;
+	if (sprintf("%p", $self) ne sprintf("%p", $ans->{correct_value})) {
+		$ans->{correct_ans}     = $self->string;
+		$ans->{correct_value}   = $self;
+		$ans->{correct_formula} = Value->Package("Formula")->new($self);
+	}
+	if (sprintf("%p", $other) ne sprintf("%p", $ans->{student_value})) {
+		$ans->{student_ans}     = $other->string;
+		$ans->{student_value}   = $other;
+		$ans->{student_formula} = Value->Package("Formula")->new($other);
+	}
+	$self->cmp_preprocess($ans);
+	$self->cmp_equal($ans);
+	$self->cmp_postprocess($ans) if !$ans->{error_message} && !$ans->{typeError};
+	return $ans->{score} >= 1;
 }
 
 1;
diff --git a/macros/answerVariableList.pl b/macros/answerVariableList.pl
index 8b0c7a1a5e..32b3ea58b9 100644
--- a/macros/answerVariableList.pl
+++ b/macros/answerVariableList.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/compoundProblem5.pl b/macros/compoundProblem5.pl
index cf814cef4e..2523e16dbd 100644
--- a/macros/compoundProblem5.pl
+++ b/macros/compoundProblem5.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextABCD.pl b/macros/contextABCD.pl
index 774daa5be8..23e34303d8 100644
--- a/macros/contextABCD.pl
+++ b/macros/contextABCD.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextAlternateDecimal.pl b/macros/contextAlternateDecimal.pl
index c5b9b07d37..cfa4eb213e 100644
--- a/macros/contextAlternateDecimal.pl
+++ b/macros/contextAlternateDecimal.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextAlternateIntervals.pl b/macros/contextAlternateIntervals.pl
index 8500cd2766..fca902678f 100644
--- a/macros/contextAlternateIntervals.pl
+++ b/macros/contextAlternateIntervals.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextComplexExtras.pl b/macros/contextComplexExtras.pl
index dc5a468c31..c2441deb5b 100644
--- a/macros/contextComplexExtras.pl
+++ b/macros/contextComplexExtras.pl
@@ -1,7 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextComplexExtras.pl,v 1.0 2012/08/01 11:33:50 dpvc Exp $
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
diff --git a/macros/contextComplexJ.pl b/macros/contextComplexJ.pl
index c386ccd39e..80bcdfcf0a 100644
--- a/macros/contextComplexJ.pl
+++ b/macros/contextComplexJ.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextCongruence.pl b/macros/contextCongruence.pl
index 8e1b9937e2..f7cea379cb 100644
--- a/macros/contextCongruence.pl
+++ b/macros/contextCongruence.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2017 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextCurrency.pl b/macros/contextCurrency.pl
index 0b8111efb8..68bac416a8 100644
--- a/macros/contextCurrency.pl
+++ b/macros/contextCurrency.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextCurrency.pl,v 1.17 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextFraction.pl b/macros/contextFraction.pl
index 2a5afdc1de..b85cfcbed3 100644
--- a/macros/contextFraction.pl
+++ b/macros/contextFraction.pl
@@ -907,11 +907,16 @@ sub string {
 sub TeX {
   my $self = shift; my $equation = shift; shift; shift; my $prec = shift;
   my ($a,$b) = @{$self->{data}}; my $n = "";
+  my $textstyle = '';
   return "$a" if $b == 1;
-  if ($self->getFlagWithAlias("showMixedNumbers","showProperFractions") && CORE::abs($a) > $b)
-    {$n = int($a/$b); $a = CORE::abs($a) % $b; $n .= " " unless $a == 0}
+  if ($self->getFlagWithAlias("showMixedNumbers","showProperFractions") && CORE::abs($a) > $b) {
+    $n = int($a/$b);
+    $a = CORE::abs($a) % $b;
+    $n .= ' ' unless $a == 0;
+    $textstyle = '\\textstyle';
+  }
   my $s = ""; ($a,$s) = (-$a,"-") if $a < 0;
-  $n .= ($self->{isHorizontal} ? "$s$a/$b" : "${s}{\\textstyle\\frac{$a}{$b}}")
+  $n .= ($self->{isHorizontal} ? "$s$a/$b" : "${s}{$textstyle\\frac{$a}{$b}}")
     unless $a == 0 && $n ne '';
   return "$n";
 }
diff --git a/macros/contextInequalities.pl b/macros/contextInequalities.pl
index 6e209ce3f9..70e5a72efe 100644
--- a/macros/contextInequalities.pl
+++ b/macros/contextInequalities.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextInequalities.pl,v 1.23 2010/03/22 11:01:55 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextInequalitySetBuilder.pl b/macros/contextInequalitySetBuilder.pl
index f3bc52fdba..c68e625e6d 100644
--- a/macros/contextInequalitySetBuilder.pl
+++ b/macros/contextInequalitySetBuilder.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2013 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextInteger.pl b/macros/contextInteger.pl
index 15878aaef8..47c943e9f9 100644
--- a/macros/contextInteger.pl
+++ b/macros/contextInteger.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2017 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextIntegerFunctions.pl b/macros/contextIntegerFunctions.pl
index 49c6ae9808..38488c5ca3 100644
--- a/macros/contextIntegerFunctions.pl
+++ b/macros/contextIntegerFunctions.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextLimitedComplex.pl b/macros/contextLimitedComplex.pl
index f58cb1bf47..39214b480a 100644
--- a/macros/contextLimitedComplex.pl
+++ b/macros/contextLimitedComplex.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextLimitedNumeric.pl b/macros/contextLimitedNumeric.pl
index adce6bdef0..c678fbbdc8 100644
--- a/macros/contextLimitedNumeric.pl
+++ b/macros/contextLimitedNumeric.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextLimitedPoint.pl b/macros/contextLimitedPoint.pl
index 72d93d8083..2d1ba73828 100644
--- a/macros/contextLimitedPoint.pl
+++ b/macros/contextLimitedPoint.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextLimitedPoint.pl,v 1.14 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextLimitedPolynomial.pl b/macros/contextLimitedPolynomial.pl
index 017dcc0bea..e4f8c94665 100644
--- a/macros/contextLimitedPolynomial.pl
+++ b/macros/contextLimitedPolynomial.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextLimitedPowers.pl b/macros/contextLimitedPowers.pl
index adb18de1b2..0a9de2deb2 100644
--- a/macros/contextLimitedPowers.pl
+++ b/macros/contextLimitedPowers.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextLimitedVector.pl b/macros/contextLimitedVector.pl
index abe12ed044..1c5314b4bb 100644
--- a/macros/contextLimitedVector.pl
+++ b/macros/contextLimitedVector.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextMatrixExtras.pl b/macros/contextMatrixExtras.pl
index d42e2483b3..d3e835c000 100644
--- a/macros/contextMatrixExtras.pl
+++ b/macros/contextMatrixExtras.pl
@@ -1,7 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextMatrixExtras.pl,v 1.0 2012/08/01 11:21:45 dpvc Exp $
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
diff --git a/macros/contextPartition.pl b/macros/contextPartition.pl
index 2e0e4cbcae..d5d74c7b6a 100644
--- a/macros/contextPartition.pl
+++ b/macros/contextPartition.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextPercent.pl b/macros/contextPercent.pl
index fb7c788380..f4dd56bb2f 100644
--- a/macros/contextPercent.pl
+++ b/macros/contextPercent.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2013 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextCurrency.pl,v 1.17 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextPeriodic.pl b/macros/contextPeriodic.pl
index b8186c881b..0a3706326e 100644
--- a/macros/contextPeriodic.pl
+++ b/macros/contextPeriodic.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextPermutation.pl b/macros/contextPermutation.pl
index 4db42ab4c3..60fb1f61a0 100644
--- a/macros/contextPermutation.pl
+++ b/macros/contextPermutation.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextPermutationUBC.pl b/macros/contextPermutationUBC.pl
index b5972b7732..8b9215d176 100644
--- a/macros/contextPermutationUBC.pl
+++ b/macros/contextPermutationUBC.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2014 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader:$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextPiecewiseFunction.pl b/macros/contextPiecewiseFunction.pl
index 724574caa1..1bf49cfda8 100644
--- a/macros/contextPiecewiseFunction.pl
+++ b/macros/contextPiecewiseFunction.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextPolynomialFactors.pl b/macros/contextPolynomialFactors.pl
index 95686be6eb..40d342b844 100644
--- a/macros/contextPolynomialFactors.pl
+++ b/macros/contextPolynomialFactors.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2010 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextPolynomialFactors.pl,v 1.2 2010/03/31 21:45:42 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextRationalFunction.pl b/macros/contextRationalFunction.pl
index 46f07c24ee..84f6f63d46 100644
--- a/macros/contextRationalFunction.pl
+++ b/macros/contextRationalFunction.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2010 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/contextRationalFunction.pl,v 1.1 2010/03/31 21:01:14 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextScientificNotation.pl b/macros/contextScientificNotation.pl
index c098724e6c..dfe52e114e 100644
--- a/macros/contextScientificNotation.pl
+++ b/macros/contextScientificNotation.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextString.pl b/macros/contextString.pl
index 6a323c3b5b..1e418d12bd 100644
--- a/macros/contextString.pl
+++ b/macros/contextString.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextTF.pl b/macros/contextTF.pl
index 22fe43c053..29933658f5 100644
--- a/macros/contextTF.pl
+++ b/macros/contextTF.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/contextTrigDegrees.pl b/macros/contextTrigDegrees.pl
index a20aa7e7d9..3e8754859a 100644
--- a/macros/contextTrigDegrees.pl
+++ b/macros/contextTrigDegrees.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright � 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/draggableProof.pl b/macros/draggableProof.pl
index a9cdde8fdd..aba3b584c7 100644
--- a/macros/draggableProof.pl
+++ b/macros/draggableProof.pl
@@ -1,6 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2021 The WeBWorK Project, https://github.com/openwebwork
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
diff --git a/macros/draggableSubsets.pl b/macros/draggableSubsets.pl
index d72b636987..4a82bcc69e 100644
--- a/macros/draggableSubsets.pl
+++ b/macros/draggableSubsets.pl
@@ -1,6 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2021 The WeBWorK Project, https://github.com/openwebwork
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
diff --git a/macros/extraAnswerEvaluators.pl b/macros/extraAnswerEvaluators.pl
index 3c0c257d18..36a9329c5f 100644
--- a/macros/extraAnswerEvaluators.pl
+++ b/macros/extraAnswerEvaluators.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserAssignment.pl b/macros/parserAssignment.pl
index 763f09e840..a0ef1089d2 100644
--- a/macros/parserAssignment.pl
+++ b/macros/parserAssignment.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserAutoStrings.pl b/macros/parserAutoStrings.pl
index e260701d73..b5bd26b4e7 100644
--- a/macros/parserAutoStrings.pl
+++ b/macros/parserAutoStrings.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserCustomization.pl b/macros/parserCustomization.pl
index e57172b3f3..f28e64528a 100644
--- a/macros/parserCustomization.pl
+++ b/macros/parserCustomization.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserDifferenceQuotient.pl b/macros/parserDifferenceQuotient.pl
index 048c5f1371..7f0d4b4c48 100644
--- a/macros/parserDifferenceQuotient.pl
+++ b/macros/parserDifferenceQuotient.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserFormulaAnyVar.pl b/macros/parserFormulaAnyVar.pl
index 049661be60..2ae71ded43 100644
--- a/macros/parserFormulaAnyVar.pl
+++ b/macros/parserFormulaAnyVar.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserFormulaUpToConstant.pl b/macros/parserFormulaUpToConstant.pl
index f54e138c67..30db29374e 100644
--- a/macros/parserFormulaUpToConstant.pl
+++ b/macros/parserFormulaUpToConstant.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserFormulaUpToConstant.pl,v 1.23 2010/02/08 13:56:09 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserFormulaWithUnits.pl b/macros/parserFormulaWithUnits.pl
index 556648c743..4eeba49335 100644
--- a/macros/parserFormulaWithUnits.pl
+++ b/macros/parserFormulaWithUnits.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserFunction.pl b/macros/parserFunction.pl
index b407c11d4d..13458982a1 100644
--- a/macros/parserFunction.pl
+++ b/macros/parserFunction.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserFunctionPrime.pl b/macros/parserFunctionPrime.pl
index 2382d183d6..a02183a4dc 100644
--- a/macros/parserFunctionPrime.pl
+++ b/macros/parserFunctionPrime.pl
@@ -1,7 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
diff --git a/macros/parserGraphTool.pl b/macros/parserGraphTool.pl
index 4ffeda96f1..14d879eb66 100644
--- a/macros/parserGraphTool.pl
+++ b/macros/parserGraphTool.pl
@@ -1,6 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2020 The WeBWorK Project, http://openwebwork.sf.net/
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
diff --git a/macros/parserImplicitEquation.pl b/macros/parserImplicitEquation.pl
index 269d8edad1..7e66a5c70a 100644
--- a/macros/parserImplicitEquation.pl
+++ b/macros/parserImplicitEquation.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserImplicitEquation.pl,v 1.14 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserImplicitPlane.pl b/macros/parserImplicitPlane.pl
index 50a8111c5f..f482f23b76 100644
--- a/macros/parserImplicitPlane.pl
+++ b/macros/parserImplicitPlane.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserLinearInequality.pl b/macros/parserLinearInequality.pl
index 943edcf6dd..6bcd34b3e4 100644
--- a/macros/parserLinearInequality.pl
+++ b/macros/parserLinearInequality.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserMultiAnswer.pl b/macros/parserMultiAnswer.pl
index 7d8670177a..b37a547ae0 100644
--- a/macros/parserMultiAnswer.pl
+++ b/macros/parserMultiAnswer.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserMultiAnswer.pl,v 1.11 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserMultiPart.pl b/macros/parserMultiPart.pl
index f5a1937e12..77b2196a76 100644
--- a/macros/parserMultiPart.pl
+++ b/macros/parserMultiPart.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserNumberWithUnits.pl b/macros/parserNumberWithUnits.pl
index 3d58bae5b1..6c8169bbea 100644
--- a/macros/parserNumberWithUnits.pl
+++ b/macros/parserNumberWithUnits.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserOneOf.pl b/macros/parserOneOf.pl
index 424d496d09..5033cce410 100644
--- a/macros/parserOneOf.pl
+++ b/macros/parserOneOf.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2012 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserParametricLine.pl b/macros/parserParametricLine.pl
index 9cda736bc5..b903df6242 100644
--- a/macros/parserParametricLine.pl
+++ b/macros/parserParametricLine.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserParametricLine.pl,v 1.17 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserParametricPlane.pl b/macros/parserParametricPlane.pl
index 8c64772d31..ec6447221e 100644
--- a/macros/parserParametricPlane.pl
+++ b/macros/parserParametricPlane.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2013 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserParametricLine.pl,v 1.17 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserPopUp.pl b/macros/parserPopUp.pl
index f8781a6861..25aa271d9a 100644
--- a/macros/parserPopUp.pl
+++ b/macros/parserPopUp.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserPopUp.pl,v 1.10 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserPrime.pl b/macros/parserPrime.pl
index 089debb573..81deebfe05 100644
--- a/macros/parserPrime.pl
+++ b/macros/parserPrime.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2009 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserPrime.pl,v 1.2 2009/10/03 15:58:49 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserRadioButtons.pl b/macros/parserRadioButtons.pl
index 751eb618b6..f3a9e90dad 100644
--- a/macros/parserRadioButtons.pl
+++ b/macros/parserRadioButtons.pl
@@ -1,7 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
diff --git a/macros/parserRoot.pl b/macros/parserRoot.pl
index 122beb9f01..30a7563e41 100644
--- a/macros/parserRoot.pl
+++ b/macros/parserRoot.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2013 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserSolutionFor.pl b/macros/parserSolutionFor.pl
index 894058f09e..6cf8778831 100644
--- a/macros/parserSolutionFor.pl
+++ b/macros/parserSolutionFor.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserVectorUtils.pl b/macros/parserVectorUtils.pl
index f43061561f..c554c81518 100644
--- a/macros/parserVectorUtils.pl
+++ b/macros/parserVectorUtils.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/parserWordCompletion.pl b/macros/parserWordCompletion.pl
index d55ce6dbc0..5c556cbbc7 100644
--- a/macros/parserWordCompletion.pl
+++ b/macros/parserWordCompletion.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright � 2000-2015 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/parserWordCompletion.pl,v 1.0 2015/11/25 23:28:44 paultpearson Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/problemPanic.pl b/macros/problemPanic.pl
index 4a398a63ac..fe11f3544a 100644
--- a/macros/problemPanic.pl
+++ b/macros/problemPanic.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2009 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/problemPanic.pl,v 1.6 2010/04/27 02:00:37 dpvc Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/problemPreserveAnswers.pl b/macros/problemPreserveAnswers.pl
index ad72195a2d..9ba7752c1a 100644
--- a/macros/problemPreserveAnswers.pl
+++ b/macros/problemPreserveAnswers.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader$
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/problemRandomize.pl b/macros/problemRandomize.pl
index 092e391949..e996990d4b 100644
--- a/macros/problemRandomize.pl
+++ b/macros/problemRandomize.pl
@@ -1,13 +1,12 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: pg/macros/problemRandomize.pl,v 1.12 2009/06/25 23:28:44 gage Exp $
-# 
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
 # Free Software Foundation; either version 2, or (at your option) any later
 # version, or (b) the "Artistic License" which comes with this package.
-# 
+#
 # This program is distributed in the hope that it will be useful, but WITHOUT
 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
diff --git a/macros/scaffold.pl b/macros/scaffold.pl
index 1b7d2b5cc7..54aba86773 100644
--- a/macros/scaffold.pl
+++ b/macros/scaffold.pl
@@ -1,6 +1,6 @@
 ################################################################################
 # WeBWorK Online Homework Delivery System
-# Copyright © 2000-2021 The WeBWorK Project, https://github.com/openwebwork
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of either: (a) the GNU General Public License as published by the
diff --git a/t/README b/t/README
deleted file mode 100644
index 77ca89b829..0000000000
--- a/t/README
+++ /dev/null
@@ -1,14 +0,0 @@
-These files, contributed by Boyd Duffe, are an example of writing unit tests 
-for PG modules so that we can determine (a) that they work, and (b) that they continue
-to work when some seemingly innocuous change elsewhere in the system causes troubles
-in this module.  
-
-Of course this is not supposed to happen when modular programming practices are followed
-but it happens anyway. :-)
-
-We need more "unit tests" like this.  (The unit does not refer to Unit.pm but to testing
-a small piece or unit of the WeBWorK/PG code.)  I would also like to wire these
-in so that administrators can perform these tests from the admin page on the web
-to reassure themselves that the new version of PG they are using is functioning correctly.
-
- 
diff --git a/t/README.md b/t/README.md
index 93f8ef8c8b..29f3053262 100644
--- a/t/README.md
+++ b/t/README.md
@@ -1,14 +1,45 @@
-# Unit Tests for PG
+# Testing PG
+
+This directory houses the resources for testing PG. It includes a mix
+of strategies for testing at different scales. It helps to catch errors
+before they are found in production and prevent regressions from being
+re-introduced.
+
+The philosophy of
+[Test Driven Design](https://en.wikipedia.org/wiki/Test-driven_development)
+is that when a bug is found, a test is written to show it failing
+and when it is fixed, the test will pass.
+The unit tests are easy to run and amenable to automation.  Some services
+can be "mocked" so that behaviour can be tested in their absence.
+All of this is to provide confidence that the code does what is intended
+and a working test can be better than documentation because it shows how
+the code currently works in practice.
+
+Old references can be found on the WebWork wiki page
+[Unit Testing](https://webwork.maa.org/wiki/Unit_Testing)
+
+
+# Unit Tests
+
+[Unit tests](https://en.wikipedia.org/wiki/Unit_testing) look at small chunks
+of self-coherent code to verify the behaviour of a subroutine or module.
+This is the test you write to catch corner cases or to explore code branches.
+In this repository, all files with the `.t` extension are unit tests which
+are found by Perl's [prove](https://perldoc.perl.org/prove) command.
 
 The individual unit tests are located in each of the directories.
+Best practice is to create a directory for each module being tested and
+group similar tests together in separate files with a descriptive name,
+such as **t/units/** for testing the **Units.pm** module.
 
-Formal unit tests are located in the the `macros` and `contexts` directories that are designed to test the pg macros and contexts respectively.
+Formal unit tests are located in the the `macros` and `contexts` directories
+that are designed to test the pg macros and contexts respectively.
 
 ## Running the tests
 
 ```bash
 cd $PG_ROOT
-prove -r .
+prove -lr t/
 ```
 
 will run all of the tests in `.t` files within subdirectories of `t`.
@@ -23,6 +54,7 @@ prove -v pgaux.t
 ```
 
 which will be verbose (`-v`).
+Or you could use `prove -lv t/macros/pgaux.t` from the root directory. 
 
 ## Writing a Unit Test
 
@@ -70,3 +102,34 @@ is(check_score($f->eval(x=>2),"4"),1,"math objects: eval x^2 at x=2");
 ```
 
 The `check_score` subroutine evaluates and compares a MathObject with a string representation of the answer.  If the score is 1, then the two are equal.
+
+
+# Integration tests
+
+[Integration testing](https://en.wikipedia.org/wiki/Integration_testing)
+tests components working together as a group.  The files with the `.pg`
+extension are used to demonstrate the output of the rendering engine.
+
+**TODO:** add an explanation of how to run these integration tests
+and their requirements.
+
+
+# Test Dependencies
+
+The tests for **Units.pm** have brought in a new module dependency,
+[Test2](https://metacpan.org/pod/Test2::V0) which is the state of the art in
+testing Perl modules.  It can compare data structures, examine warnings and
+catch fatal errors thrown under expected conditions.  It provides many tools
+for testing and randomly executes its subtests to avoid the programmer
+depending on stateful data.
+
+To make these easier to install with
+[cpanm](https://metacpan.org/dist/App-cpanminus/view/bin/cpanm), there is a
+[cpanfile](https://metacpan.org/dist/Module-CPANfile/view/lib/cpanfile.pod)
+in the root directory.  Use
+
+  cpanm --installdeps .
+
+which will install the runtime and test dependencies.
+To use the cpanfile for a minimal install skipping the test requirements,
+use the `--notest` option with cpanm.
diff --git a/t/contexts/fraction.t b/t/contexts/fraction.t
index ecebcd848f..ec10c3deaf 100644
--- a/t/contexts/fraction.t
+++ b/t/contexts/fraction.t
@@ -1,23 +1,15 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+# should I "use" Parser Value Parser::Legacy here instead?
 
-use Test::More;
-use Test::Exception;
+use lib 't/lib';
+use Test::PG;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+=head2 Fraction context
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
-
-use lib "$main::pg_dir/lib";
-
-require("$main::pg_dir/t/build_PG_envir.pl");
+To test the reduction of fractions
 
-## END OF TOP_MATERIAL
+=cut
 
 loadMacros("PGstandard.pl", "MathObjects.pl", "contextFraction.pl");
 
@@ -39,9 +31,9 @@ Context("Fraction");
 # require("Parser::Legacy::LimitedNumeric::Number");
 # require("Parser::Legacy");
 
-my $a1 = Compute("1/2");
-my $a2 = Compute("2/4");
+ok my $a1 = Compute("1/2");
+ok my $a2 = Compute("2/4");
 
-is($a1->value, $a2->value, "contextFraction: reduce fractions");
+is $a1->value, $a2->value, 'contextFraction: reduce fractions';
 
 done_testing();
diff --git a/t/contexts/integer.t b/t/contexts/integer.t
index 7981c240dd..d5f495e84a 100644
--- a/t/contexts/integer.t
+++ b/t/contexts/integer.t
@@ -1,23 +1,15 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+# should I "use" Parser Value Parser::Legacy here instead?
 
-use Test::More;
-use Test::Exception;
+use lib 't/lib';
+use Test::PG;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+=head2 Integer context
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
-
-use lib "$main::pg_dir/lib";
+To test for greatest common denomenators and such like.
 
-require("$main::pg_dir/t/build_PG_envir.pl");
-
-## END OF TOP_MATERIAL
+=cut
 
 loadMacros("MathObjects.pl", "contextInteger.pl");
 
@@ -33,4 +25,3 @@ ANS($b->cmp);
 ok(1, "integer test: dummy test");
 
 done_testing();
-
diff --git a/t/contexts/toltype_digits.t b/t/contexts/toltype_digits.t
index f878cea032..4b3f741f85 100644
--- a/t/contexts/toltype_digits.t
+++ b/t/contexts/toltype_digits.t
@@ -1,64 +1,56 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+use lib 't/lib';
+use Test::PG;
 
-use Test::More;
-use Test::Exception;
+=head2 TolType context
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+To test for tolerances
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
-
-use lib "$main::pg_dir/lib";
-
-require("$main::pg_dir/t/build_PG_envir.pl");
-
-## END OF TOP_MATERIAL
+=cut
 
 loadMacros("PGstandard.pl", "MathObjects.pl");
 
 my $ctx = Context("Numeric");
-$ctx->flags->set(tolType => 'digits', tolerance => 3, tolTruncation => 1);
-
 my $pi = Real("pi");
 
-is(check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14");
-is(check_score($pi, Compute("3.141")), 1, "toltype digits: pi is 3.141");
-is(check_score($pi, Compute("3.142")), 1, "toltype digits: pi is 3.142");
-is(check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143");
-is(check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15");
-
-note("");
-note("change tolTrunction to 0");
-
-$ctx->flags->set(tolType => 'digits', tolerance => 3, tolTruncation => 0);
-is(check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14");
-is(check_score($pi, Compute("3.141")), 0, "toltype digits: pi is not 3.141");
-is(check_score($pi, Compute("3.142")), 1, "toltype digits: pi is not 3.142");
-is(check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143");
-is(check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15");
-
-note("");
-note("set tolExtraDigits to 2");
-
-$ctx->flags->set(
-	tolType        => 'digits',
-	tolerance      => 3,
-	tolTruncation  => 0,
-	tolExtraDigits => 2
-);
-is(check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14");
-is(check_score($pi, Compute("3.141")), 0, "toltype digits: pi is not 3.141");
-is(check_score($pi, Compute("3.142")), 1, "toltype digits: pi is not 3.142");
-is(check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143");
-is(check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15");
-
-is(check_score($pi, Compute("3.1416")),    1, "toltype digits: pi is 3.1416");
-is(check_score($pi, Compute("3.1415888")), 1, "toltype digits: pi is 3.1415888");
-is(check_score($pi, Compute("3.1415")),    0, "toltype digits: pi is not 3.1415");
+subtest 'set tolTrunction to 1' => sub {
+	$ctx->flags->set(tolType => 'digits', tolerance => 3, tolTruncation => 1);
+
+	is check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14";
+	is check_score($pi, Compute("3.141")), 1, "toltype digits: pi is 3.141";
+	is check_score($pi, Compute("3.142")), 1, "toltype digits: pi is 3.142";
+	is check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143";
+	is check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15";
+};
+
+subtest 'set tolTrunction to 0' => sub {
+	$ctx->flags->set(tolType => 'digits', tolerance => 3, tolTruncation => 0);
+
+	is check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14";
+	is check_score($pi, Compute("3.141")), 0, "toltype digits: pi is not 3.141";
+	is check_score($pi, Compute("3.142")), 1, "toltype digits: pi is not 3.142";
+	is check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143";
+	is check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15";
+};
+
+subtest 'set tolExtraDigits to 2' => sub {
+	$ctx->flags->set(
+		tolType        => 'digits',
+		tolerance      => 3,
+		tolTruncation  => 0,
+		tolExtraDigits => 2
+	);
+
+	is check_score($pi, Compute("3.14")),  1, "toltype digits: pi is 3.14";
+	is check_score($pi, Compute("3.141")), 0, "toltype digits: pi is not 3.141";
+	is check_score($pi, Compute("3.142")), 1, "toltype digits: pi is not 3.142";
+	is check_score($pi, Compute("3.143")), 0, "toltype digits: pi is not 3.143";
+	is check_score($pi, Compute("3.15")),  0, "toltype digits: pi is not 3.15";
+
+	is check_score($pi, Compute("3.1416")),    1, "toltype digits: pi is 3.1416";
+	is check_score($pi, Compute("3.1415888")), 1, "toltype digits: pi is 3.1415888";
+	is check_score($pi, Compute("3.1415")),    0, "toltype digits: pi is not 3.1415";
+};
 
 done_testing();
diff --git a/t/contexts/trig_degrees.t b/t/contexts/trig_degrees.t
index 3d2230ad75..ba8481721f 100644
--- a/t/contexts/trig_degrees.t
+++ b/t/contexts/trig_degrees.t
@@ -1,23 +1,26 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+use lib 't/lib';
+use Test::PG;
 
-use Test::More;
-use Test::Exception;
+# remove warnings about redefining trig functions
+delete $main::{sin};
+delete $main::{cos};
+delete $main::{atan2};
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
+=head2 Errors to be fixed
 
-use lib "$main::pg_dir/lib";
+There is something wrong with either contextTrigDegrees.pl or how this test
+sets up the context.  It looks like it still calculates in radians.
+Maybe the problem is how it imports symbols?
 
-require("$main::pg_dir/t/build_PG_envir.pl");
+When you fix it, the test output will report the test numbers after C
+
+These are the same results as the original Test::More version with build_PG_env.pl
+
+=cut
 
-## END OF TOP_MATERIAL
 
 loadMacros("contextTrigDegrees.pl");
 
@@ -25,9 +28,21 @@ my $ctx = Context("TrigDegrees");
 
 ok(Value::isContext($ctx), "trig degrees: check context");
 
-my $cos60 = Compute("cos(60)");
+ok my $cos60 = Compute("cos(60)"), 'Call Compute';
+ok my $eval_cos60 = $cos60->cmp->evaluate("1/2"), 'evalute an answer to cos(60)';
+
+is $eval_cos60,
+    hash {
+        field type          => 'Value (Real)';
+        field score         => 0;
+        field correct_ans   => "cos(60)";
+        field student_ans   => 0.5;
+        field error_flag    => U();
+        field error_message => DF();
+        etc();
+    }, 'What does the Compute("cos(60)") object look like?';
+
 
-Compute("cos(60)")->cmp->evaluate("1/2");
 # dd Compute("1/2")->value;
 # is (check_score($cos60,"1/2"),1,"trig degrees: cos(60) = 1/2");
 
@@ -38,4 +53,19 @@ Compute("cos(60)")->cmp->evaluate("1/2");
 
 # is (check_score(Compute("cos(60)"),"sin(30)"),1,"trig degrees: cos(60) = 1/2");
 
+# simple sanity checking
+is check_score( Compute('sin(0)'), '0'), 1, 'trig degrees: sin(0) = 0';
+is check_score( Compute('sin(90)'), '1'), 1, 'trig degrees: sin(90) = 1';
+is check_score( Compute('cos(0)'), '1'), 1, 'trig degrees: cos(0) = 1';
+
+todo 'why is cos(90) not equal to 0' => sub {
+	# are we still computing in radians?
+	is check_score( Compute('cos(90)'), '0'), 1, 'trig degrees: cos(90) = 0';
+	is check_score( Compute('cos(90)'), '1.6155E-15'), 1, 'trig degrees: cos(90) ~ 0';
+
+	my $pi = 4 * atan2(1,1);
+	is check_score( Compute("cos($pi/3)"), "sin($pi/6)"), 1, 'trig degrees: cos(60) = sin(30)';
+};
+
+
 done_testing();
diff --git a/t/lib/Test/PG.pm b/t/lib/Test/PG.pm
new file mode 100644
index 0000000000..c382a303b8
--- /dev/null
+++ b/t/lib/Test/PG.pm
@@ -0,0 +1,108 @@
+package Test::PG;
+
+BEGIN {
+    die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
+    $main::pg_dir = $ENV{PG_ROOT};
+}
+
+use warnings;
+use strict;
+
+use lib "$main::pg_dir/lib"; # only needed if not using prove with -l
+
+
+=head1 Test::PG
+
+This module provides the environment and some generic functions for
+writing tests for PG macros.  Mostly copied from F,
+it also redefines Test2's "exists" function from C to C
+to avoid the conflict with WebWork's exponential function.
+Its final action is to load C.
+
+The reason for the module is that There Is More Than One Way To Do It
+and people develop different styles.  This is my coding style.
+Also we learn the underlying structures better by re-inventing the wheel.
+We just try to make a better wheel, but success is not guaranteed.
+
+This module strives for elegance in reducing boiler plate in test files,
+a minimum of duplicated code and adheres to the principle of least surprise
+by locating modules below the C directory.
+It does not make a decent cup of tea.
+
+=head2 Usage
+
+To test a macro that needs to be loaded, include this preamble
+at the top of your test file and customize.
+
+  use Test2::V0;
+
+  use MyPGLib;   # does your macro need a module?
+
+  use lib 't/lib';
+  use Test::PG;    # setup a minimal WW environment
+
+  loadMacros("parserMyPGMacro.pl");
+
+  Context("Numeric");
+
+And run your test from the $PG_ROOT directory with
+
+  prove -l t/my_macro/my_test.t
+
+
+=head2 TODO
+
+=head3 Quiet warnings in F
+
+The functions sin, cos and atan2 are redefined and generate warnings.
+C them from the symbol table before loading the macro.
+
+F declares C<$deg> twice, it being
+a required file, the package scope isn't heeded by the second C.
+I wonder if this happens every time this macro is loaded.
+
+=cut
+
+
+package main;
+
+$main::{EXISTS} = delete $main::{E}; # redefine Test2's E() function as EXISTS()
+
+my $macros_dir = "$main::pg_dir/macros";
+
+# use WeBWorK::Localize;
+use PGcore;
+use Parser;
+
+# build up enough of a PG environment to get things running
+our %envir = (
+    htmlDirectory       => '/opt/webwork/courses/daemon_course/html',
+    htmlURL             => 'http://localhost/webwork2/daemon_course/html',
+    tempURL             => 'http://localhost/webwork2/daemon_course/tmp',
+    pgDirectories       => { macrosPath => ["$macros_dir"] },
+    macrosPath          => ["$macros_dir"],
+    displayMode         => 'HTML_MathJax',
+    language            => 'en-us',
+    language_subroutine => sub { return @_; },    # return the string passed in instead going to maketext
+);
+
+sub be_strict {
+        require 'ww_strict.pm';
+        strict::import();
+}
+
+sub PG_restricted_eval {
+        WeBWorK::PG::Translator::PG_restricted_eval(@_);
+}
+
+sub check_score {
+        my ($correct_answer, $ans) = @_;
+        return $correct_answer->cmp->evaluate($ans)->{score};
+}
+
+require "$macros_dir/PG.pl";
+DOCUMENT();
+
+loadMacros('PGbasicmacros.pl');
+
+1;
diff --git a/t/macros/basicmacros.t b/t/macros/basicmacros.t
index eab341352f..fd69c3d5e7 100644
--- a/t/macros/basicmacros.t
+++ b/t/macros/basicmacros.t
@@ -1,29 +1,19 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
-
-use Test::More;
-use Test::Exception;
+use HTML::Entities;
+use HTML::TagParser;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+use lib 't/lib';
+use Test::PG;
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
 
-use lib "$main::pg_dir/lib";
+=head1 MathObjects
 
-require("$main::pg_dir/t/build_PG_envir.pl");
+=cut
 
-## END OF TOP_MATERIAL
 
 loadMacros("PGbasicmacros.pl");
 
-use HTML::Entities;
-use HTML::TagParser;
-
 my $name      = "myansrule";
 my $named_box = NAMED_ANS_RULE($name);
 
diff --git a/t/macros/math_objects_basics.t b/t/macros/math_objects_basics.t
index a0b5a18d72..933199f73a 100644
--- a/t/macros/math_objects_basics.t
+++ b/t/macros/math_objects_basics.t
@@ -1,25 +1,18 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
-
-use Test::More;
-use Test::Exception;
+use Parser;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+use lib 't/lib';
+use Test::PG;
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
 
-use lib "$main::pg_dir/lib";
+=head1 MathObjects
 
-require("$main::pg_dir/t/build_PG_envir.pl");
+Test MathObject properties and operations.
+Try out operations with Infinity.
 
-## END OF TOP_MATERIAL
+=cut
 
-use Parser;
 
 loadMacros("MathObjects.pl");
 
@@ -28,71 +21,82 @@ Context("Numeric");
 my ($val1, $val2) = (10, 5);
 my $obj1 = Compute($val1);
 my $obj2 = Compute($val2);
-my $one  = Compute("1");
 my $zero = Compute("0");
-
-is($obj1->class, "Real",   "math objects: check class of object");
-is($obj2->type,  "Number", "math objects: check type of object");
-ok($one->isOne,   "math objects: check if a number is 1");
-ok(!$zero->isOne, "math objects: check if a number is not 1");
-ok($zero->isZero, "math objects: check if a number is 0");
-ok(!$one->isZero, "math objects: check if a number is not 0");
-
-ok(Value::isValue($obj1),    "math objects: check if an object is a value");
-ok(Value::isNumber($obj1),   "math objects: check if an object is a number");
-ok(Value::isReal($obj1),     "math objects: check if a number is a real number");
-ok(!Value::isComplex($obj1), "math objects: check if an integer is complex");
-
-ok(!Value::isFormula($obj1), "math objects: check if a number is not a formula");
-
-# check infinite values
-note("Tests for infinite values");
-
-my $inf = Compute("inf");
-is($inf->value, "infinity", "math objects: check for infinity via a string");
-is($inf->class, "Infinity", "math objects: check that the class is Infinity");
-is($inf->type,  "Infinity", "math objects: check that the type is Infinity");
-ok(!Value::isNumber($inf), "math objects: check if inf is a number");
-
-# check that operations with infinity are not allowed
-
-throws_ok {
-	Compute("$obj1+$inf");
-}
-qr/can't be infinities/, "math objects: addition with infinity";
-throws_ok {
-	Compute("$obj1-$inf");
-}
-qr/can't be infinities/, "math objects: subtraction with infinity";
-throws_ok {
-	Compute("$obj1*$inf");
-}
-qr/can't be infinities/, "math objects: multiplication with infinity";
-throws_ok {
-	Compute("$obj1/$inf");
-}
-qr/can't be infinities/, "math objects: division with infinity";
-
-# is($result1->value,"infinity","math objects: check that the sum of a finite and infinite value is infinite");
+ok my $one  = Compute("1"), 'Create a MathObject with Compute';
+
+subtest 'Basic properties of MathObjects' => sub {
+	is $obj1->class,  'Real',   'math objects: check class of object';
+	is $obj2->type,   'Number', 'math objects: check type of object';
+	is $one->isOne,   T(), 'math objects: check if a number is 1';
+	is $zero->isOne,  F(), 'math objects: check if a number is not 1';
+	is $zero->isZero, T(), 'math objects: check if a number is 0';
+	is $one->isZero,  F(), 'math objects: check if a number is not 0';
+};
+
+subtest 'Class methods of Value to determine type' => sub {
+	is Value::isValue($obj1),   T(), 'math objects: check if an object is a value';
+	is Value::isNumber($obj1),  T(), 'math objects: check if an object is a number';
+	is Value::isReal($obj1),    T(), 'math objects: check if a number is a real number';
+	is Value::isComplex($obj1), F(), 'math objects: check if an integer is complex';
+
+	is Value::isFormula($obj1), F(), 'math objects: check if a number is not a formula';
+};
+
+ok my $inf = Compute("inf"), 'Can create Infinity';
+
+subtest 'Tests for infinite values' => sub {
+	is $inf->value, 'infinity', 'math objects: check for infinity via a string';
+	is $inf->class, 'Infinity', 'math objects: check that the class is Infinity';
+	is $inf->type,  'Infinity', 'math objects: check that the type is Infinity';
+	ok !Value::isNumber($inf),  'math objects: check if inf is a number';
+};
+
+subtest 'check that operations with infinity are not allowed' => sub {
+	like(
+		dies { Compute("$obj1+$inf") },
+		qr/can't be infinities/,
+		"math objects: addition with infinity"
+	);
+	like(
+		dies { Compute("$obj1-$inf") },
+		qr/can't be infinities/,
+		"math objects: subtraction with infinity"
+	);
+	like(
+		dies { Compute("$obj1*$inf") },
+		qr/can't be infinities/,
+		"math objects: multiplication with infinity"
+	);
+	like(
+		dies { Compute("$obj1/$inf") },
+		qr/can't be infinities/,
+		"math objects: division with infinity"
+	);
+
+	# is($result1->value,'infinity','math objects: check that the sum of a finite and infinite value is infinite');
+};
 
 my $sum  = $obj1 + $obj2;
 my $diff = $obj1 - $obj2;
-my $prod = $obj1 * $obj2;
-
-is($sum->value,  $val1 + $val2, "math objects: test sum");
-is($diff->value, $val1 - $val2, "math objects: test difference");
-is($prod->value, $val1 * $val2, "math objects: test product");
-
-## check scores using the cmp method
-
-is(check_score($sum,  Compute($sum)),  1, "math object: use cmp to check sum");
-is(check_score($diff, Compute($diff)), 1, "math object: use cmp to check diff");
-is(check_score($prod, Compute($prod)), 1, "math object: use cmp to check prod");
-
-## check some wrong answers;
+ok my $prod = $obj1 * $obj2, 'Operate on two MathObjects';
+
+subtest 'check object operations' => sub {
+	is $sum->value,  $val1 + $val2, 'math objects: test sum';
+	is $diff->value, $val1 - $val2, 'math objects: test difference';
+	is $prod->value, $val1 * $val2, 'math objects: test product';
+};
+
+subtest 'check scores using the cmp method' => sub {
+	is check_score($sum,  Compute($sum)),  1, 'math object: use cmp to check sum';
+	is check_score($diff, Compute($diff)), 1, 'math object: use cmp to check diff';
+	is check_score($prod, Compute($prod)), 1, 'math object: use cmp to check prod';
+};
+
+subtest 'check some wrong answers' => sub {
+	is check_score($sum,  Compute($sum + 1)),  0, 'math object: use cmp to check sum';
+	is check_score($diff, Compute($diff + 1)), 0, 'math object: use cmp to check diff';
+	is check_score($prod, Compute($prod + 1)), 0, 'math object: use cmp to check prod';
+};
 
-is(check_score($sum,  Compute($sum + 1)),  0, "math object: use cmp to check sum");
-is(check_score($diff, Compute($diff + 1)), 0, "math object: use cmp to check diff");
-is(check_score($prod, Compute($prod + 1)), 0, "math object: use cmp to check prod");
 
 done_testing();
diff --git a/t/macros/math_objects_more.t b/t/macros/math_objects_more.t
index e54bb409e8..37c0ec29d7 100644
--- a/t/macros/math_objects_more.t
+++ b/t/macros/math_objects_more.t
@@ -1,23 +1,15 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+use lib 't/lib';
+use Test::PG;
 
-use Test::More;
-use Test::Exception;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+=head1 MathObjects
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
+Tests pass
 
-use lib "$main::pg_dir/lib";
+=cut
 
-require("$main::pg_dir/t/build_PG_envir.pl");
-
-## END OF TOP_MATERIAL
 
 loadMacros("MathObjects.pl");
 
diff --git a/t/macros/pgaux.t b/t/macros/pgaux.t
index 73bc3031de..34bec6aeb7 100644
--- a/t/macros/pgaux.t
+++ b/t/macros/pgaux.t
@@ -1,21 +1,15 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+use lib 't/lib';
+use Test::PG;
 
-use Test::More;
 
-## the following needs to include at the top of any testing  down to TOP_MATERIAL
+=head1 PGauxiliaryFunctions
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
+Tests pass
 
-use lib "$main::pg_dir/lib";
-require("$main::pg_dir/t/build_PG_envir.pl");
+=cut
 
-## END OF TOP_MATERIAL
 
 loadMacros("PGauxiliaryFunctions.pl");
 
diff --git a/t/macros/tableau.t b/t/macros/tableau.t
index a059ab4557..b09da818f4 100755
--- a/t/macros/tableau.t
+++ b/t/macros/tableau.t
@@ -1,40 +1,44 @@
-use warnings;
-use strict;
+use Test2::V0;
 
-package main;
+use Class::Accessor;
+use Parser;
+use PGcore;
+use Value;
 
-use Test::More;
-use Test::Exception;
+use lib 't/lib';
+use Test::PG;
 
-# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
 
-BEGIN {
-	die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT};
-	$main::pg_dir = $ENV{PG_ROOT};
-}
+=head1 Tableau
 
-use lib "$main::pg_dir/lib";
+Deep recursion error possibly due to "context" being a reference
+to the original context variable.
+I can't find a way to stop the recursion.
+As a result Test::More::is_deeply( $A, $B )
+becomes Test2::Tools::Compare::is( $A->string, $B->string )
+I wrote a validator for the cases where you are trying to compare
+a Value::Matrix with an array ref of values which doesn't have
+a "string" method.  If someone can clean it up, future coders
+will thank you.
 
-require("$main::pg_dir/t/build_PG_envir.pl");
+Tests many of the functions provided by F
 
-## END OF TOP_MATERIAL
+Removed the redefined WARN and DEBUG MESSAGES
 
-use Parser;
-use Value;
-use Class::Accessor;
-use PGcore;
+Error Messages:
 
-loadMacros("tableau.pl", "Value.pl");    #gives us Real() etc.
+A context appears to have been destroyed without first calling release().
+Based on $@ it does not look like an exception was thrown (this is not always
+a reliable test)
+
+=cut
 
-note(
-	"THIS FILE TESTS MANY OF THE FUNCTIONS PROVIDED BY tableau.pl
-#
-#
-"
-);
+
+loadMacros('tableau.pl', 'Value.pl');    #gives us Real() etc.
 
 my %context = ();
 
+
 sub Context { Parser::Context->current(\%context, @_) }
 unless (%context && $context{current}) {
 	# ^variable our %context
@@ -43,13 +47,6 @@ unless (%context && $context{current}) {
 	Context();        # Initialize context (for persistent mod_perl)
 }
 
-sub WARN_MESSAGE {
-	warn("WARN MESSAGE: ", @_);
-}
-
-sub DEBUG_MESSAGE {
-	warn("DEBUG MESSAGE: ", @_);
-}
 Context("Matrix");
 
 Context()->flags->set(
@@ -57,11 +54,27 @@ Context()->flags->set(
 	zeroLevelTol => 1E-5
 );
 
+
 my $A = Real(.0000005);
 my $B = Real(0);
 
-is($A, $B, "test zeroLevel tolerance");
-ok($A == $B, "test zeroLevel tolerance with ok");
+subtest 'test zeroLevel tolerance' => sub {
+	is($A->value, within($B->value, $B->getFlag('zeroLevel')), 'test zeroLevel tolerance');
+	ok($A == $B, 'test zeroLevel tolerance with ok');
+
+	my $real_object = object {
+		prop isa => 'Value::Real';
+
+		field data    => array { item 0 => match qr/\d/; };
+		field context => D();
+
+		call string => match qr/\d/;
+		call TeX    => match qr/\d/;
+	};
+
+	is $A, $real_object, 'Zero is a Real';
+	is $B, $real_object, 'Near-Zero is a Real';
+};
 
 my $money_total = 6000;
 my $time_total  = 600;
@@ -93,192 +106,305 @@ my $b = Value::Vector->new([ -$bill_profit, -$steve_profit ]);    # need vertica
 my $c = Value::Vector->new([ $money_total, $time_total, 1, 1 ]);
 
 my $tableau1 = Tableau->new(A => $a, b => $b, c => $c);
-###############################################################
-# Check mutators
-#
-#
+
 ###############################################################
 
-ok(1 == 1,             "trivial first test");
-ok(defined($tableau1), 'tableau has been defined and loaded');
-is(ref($tableau1), "Tableau", 'has type Tableau');
-
-# test "close_enough_to_zero" subroutine
-is $tableau1->close_enough_to_zero(0),     1, "checking_close_enough to zero";
-is $tableau1->close_enough_to_zero(1e-9),  0, "checking_close_enough to zero";
-is $tableau1->close_enough_to_zero(1e-5),  0, "checking_close_enough to zero";
-is $tableau1->close_enough_to_zero(1e-10), 1, "checking_NOT_close_enough to zero for 1e-10 ";
-note("sanity check 1e-10 vs 10**(-10):  ", 1e-10, " ", 10**(-10));
-note(1.e-10);
-note(0.9999e-10);
-note(-0.9999e-10);
-is $tableau1->close_enough_to_zero(0.9999e-10), 1, "checking_close_enough to zero for 0.9999e-10";
-
-is $tableau1->close_enough_to_zero(-0.9999e-10), 1, "checking_close_enough to zero for -0.9999e-10";
-note("display stringified  \$tableau1: ", $tableau1, "\n");
-is ref($tableau1), "Tableau", "checking data type is Tableau";
-ok $tableau1 eq "[[-5000,-400,-1,0,1,0,0,-4700],[-3000,-500,0,-1,0,1,0,-4500]]", "checking_stringification of tableau";
-
-is($tableau1->{m}, 2, 'number of constraints is 2');
-is($tableau1->{n}, 4, 'number of variables is 4');
-is_deeply([ $tableau1->{m}, $tableau1->{n} ], [ $tableau1->{A}->dimensions ], '{m},{n} match dimensions of A');
-is_deeply($tableau1->{A},                     $a,                             'constraint matrix');
-is_deeply($tableau1->{b},                     Matrix([$b])->transpose,        'constraint constants is m by 1 matrix');
-is_deeply($tableau1->{c},                     $c,                             'objective function constants');
-is_deeply($tableau1->{A},                     $tableau1->A, '{A} original constraint matrix accessor');
-is_deeply($tableau1->{b},                     $tableau1->b, '{b} orginal constraint constants accessor');
-is_deeply($tableau1->{c},                     $tableau1->c, '{c} original objective function constants accessor');
+subtest 'Check mutators' => sub {
+	is $tableau1, D(), 'tableau has been defined and loaded';
+	is $tableau1, object { prop isa => 'Tableau' }, 'has type Tableau';
+};
+
+subtest 'test "close_enough_to_zero" subroutine' => sub {
+	is $tableau1->close_enough_to_zero(0),     1, 'checking_close_enough to zero';
+	is $tableau1->close_enough_to_zero(1e-9),  0, 'checking_close_enough to zero';
+	is $tableau1->close_enough_to_zero(1e-5),  0, 'checking_close_enough to zero';
+	is $tableau1->close_enough_to_zero(1e-10), 1, 'checking_NOT_close_enough to zero for 1e-10 ';
+	note('sanity check 1e-10 vs 10**(-10):  ', 1e-10, ' ', 10**(-10));
+	note(1.e-10);
+	note(0.9999e-10);
+	note(-0.9999e-10);
+	is $tableau1->close_enough_to_zero(0.9999e-10), 1, 'checking_close_enough to zero for 0.9999e-10';
+
+	is $tableau1->close_enough_to_zero(-0.9999e-10), 1, 'checking_close_enough to zero for -0.9999e-10';
+};
+
+subtest 'Basic test warmup' => sub {
+	note("display stringified  \$tableau1: ", $tableau1, "\n");
+	is ref($tableau1), "Tableau", "checking data type is Tableau";
+	is $tableau1,
+		'[[-5000,-400,-1,0,1,0,0,-4700],[-3000,-500,0,-1,0,1,0,-4500]]',
+		'checking_stringification of tableau';
+
+	is $tableau1->{m}, 2, 'number of constraints is 2';
+};
+
+subtest 'check data structure of tableau object' => sub {
+	is($tableau1->{m}, 2, 'number of constraints is 2');
+	is($tableau1->{n}, 4, 'number of variables is 4');
+	is [ $tableau1->{m}, $tableau1->{n} ], [ $tableau1->{A}->dimensions ],
+		'{m},{n} match dimensions of A';
+
+	is $tableau1,
+		object {
+			field A => object {
+				prop isa => 'Value::Matrix';
+				call string => "$a";
+				field context => D();
+				field data => D();
+			};
+			field b => D();
+			field c => D();
+			etc();
+		}, 'tableau attributes';
+
+	# call the string method to avoid the circular refs
+	is $tableau1->{A}->string, $a->string, 'Constraint matrix';
+	is $tableau1->{b}->string, Matrix([$b])->transpose->string, 'Constraint constants is m by 1 matrix';
+	is $tableau1->{c}->string, $c->string, 'Objective function constants';
+
+	is $tableau1->{A}->string, $tableau1->A->string, '{A} original constraint matrix accessor';
+	is $tableau1->{b}->string, $tableau1->b->string, '{b} original constraint constants accessor';
+	is $tableau1->{c}->string, $tableau1->c->string, '{c} original objective function constants accessor';
+};
 
 my $test_constraint_matrix = Matrix($ra_matrix->[0], $ra_matrix->[1]);
-is_deeply($tableau1->{current_constraint_matrix},
-	$test_constraint_matrix, 'initialization of current_constraint_matrix');
-is_deeply(
-	$tableau1->{current_constraint_matrix},
-	$tableau1->current_constraint_matrix,
-	'current_constraint_matrix accessor'
-);
-is_deeply($tableau1->{current_b},               $tableau1->{b},       'initialization of current_b');
-is_deeply($tableau1->{current_b},               $tableau1->current_b, 'current_b accessor');
-is_deeply([ $tableau1->current_b->dimensions ], [ 2, 1 ],             'dimensions of current_b');
-my $obj_row_test = [ ((-$c)->value, 0, 0, 1, 0) ];
-is_deeply($tableau1->objective_row, $obj_row_test, 'initialization of $tableau->{obj_row}');
-
-is(ref($tableau1->{obj_row}), 'Value::Matrix', '->{obj_row} has type Value::Matrix');
-is(ref($tableau1->obj_row),   'Value::Matrix', '->obj_row has type Value::Matrix');
-is_deeply($tableau1->obj_row,            $tableau1->{obj_row},            'verify mutator for {obj_row}');
-is_deeply(ref($tableau1->objective_row), 'ARRAY',                         '->objective_row has type ARRAY');
-is_deeply($tableau1->objective_row,      [ $tableau1->{obj_row}->value ], 'access to {obj_row}');
-is_deeply($tableau1->objective_row,      [ $tableau1->obj_row->value ],   'objective_row is obj_row->value = ARRAY');
-
-is(ref($tableau1->current_tableau), 'Value::Matrix', '-> current_tableau is Value::Matrix');
-is_deeply($tableau1->current_tableau, Matrix($ra_matrix), 'entire tableau including obj coeff row');
-
-is(ref($tableau1->S), "Value::Matrix", 'slack variables are a Value::Matrix');
-is_deeply($tableau1->S, $tableau1->I($tableau1->m), 'slack variables are identity matrix');
-
-# test basis
-is_deeply(ref($tableau1->basis_columns), "ARRAY",  "{basis_column} has type ARRAY");
-is_deeply($tableau1->basis_columns,      [ 5, 6 ], "initialization of basis");
-is(
-	ref($tableau1->current_basis_matrix),
-	ref(Value::Matrix->I($tableau1->m)),
-	"current_basis_matrix type is MathObjectMatrix"
-);
-is_deeply($tableau1->current_basis_matrix, Value::Matrix->I($tableau1->m), "initialization of basis");
-
-# change basis and test again
-$tableau1->basis(2, 3);
-is_deeply(ref($tableau1->basis_columns), "ARRAY",  "{basis_column} has type ARRAY");
-is_deeply($tableau1->basis_columns,      [ 2, 3 ], " basis columns set to {2,3}");
-is(
-	ref($tableau1->current_basis_matrix),
-	ref($test_constraint_matrix->column_slice(2, 3)),
-	"current_basis_matrix type is MathObjectMatrix"
-);
-is_deeply(
-	$tableau1->current_basis_matrix,
-	$test_constraint_matrix->column_slice(2, 3),
-	"basis_matrix for columns {2,3} is correct"
-);
-is_deeply($tableau1->basis(Set(2, 3)),  List([ 2, 3 ]), "->basis(Set(2,3))");
-is_deeply($tableau1->basis(List(2, 3)), List([ 2, 3 ]), "->basis(List(2,3))");
-is_deeply($tableau1->basis([ 2, 3 ]),   List([ 2, 3 ]), "->basis([2,3])");
-
-# find basis column index corresponding to row index (and value of the basis coefficient)
-
-$tableau1->basis(5, 6);
-note("\nbasis is",                     $tableau1->basis(5, 6));
-note(print $tableau1->current_tableau, "\n");
-is_deeply([ $tableau1->find_leaving_column(1) ], [ 5, 1 ], "find_leaving_column returns [col_index, pivot_value] ");
-is_deeply([ $tableau1->find_leaving_column(2) ], [ 6, 1 ], "find_leaving_column returns [col_index, pivot_value] ");
-
-is_deeply($tableau1->find_next_basis_from_pivot(1, 2), Set(2, 6), "find next basis from pivot (1,2)");
-is_deeply($tableau1->find_next_basis_from_pivot(1, 3), Set(3, 6), "find next basis from pivot (1,3)");
-is_deeply($tableau1->find_next_basis_from_pivot(2, 1), Set(1, 5), "find next basis from pivot (2,1)");
-is_deeply($tableau1->find_next_basis_from_pivot(1, 1), Set(1, 6), "find next basis from pivot (1,1)");
-
-throws_ok(
-	sub { $tableau1->find_next_basis_from_pivot(2, 5) },
-	qr/pivot point should not be in a basis column/,
-	"can't pivot in basis column (2,5)"
-);    # probably shouldn't be doing this.
-throws_ok(
-	sub { $tableau1->find_next_basis_from_pivot(1, 6) },
-	qr/pivot point should not be in a basis column/,
-	"can't pivot in basis column (2,6)"
-);    # probably shouldn't be doing this.
-is_deeply($tableau1->find_next_basis_from_pivot(2, 1), Set(1, 5), "find next basis from pivot (2,1)");
-throws_ok(
-	sub { $tableau1->find_next_basis_from_pivot(2, 6) },
-	qr/pivot point should not be in a basis column/,
-	"can't pivot in basis column (2,6)"
-);    # probably shouldn't be doing this.
-
-$tableau1->basis(2, 3);
-note("\nbasis is",                     $tableau1->basis());
-note(print $tableau1->current_tableau, "\n");
-is_deeply([ $tableau1->find_leaving_column(1) ], [ 2, 500 ], "find_leaving_column returns [col_index, pivot_value] ");
-is_deeply([ $tableau1->find_leaving_column(2) ], [ 3, 500 ], "find_leaving_column returns [col_index, pivot_value] ");
-
-throws_ok(
-	sub { $tableau1->find_next_basis_from_pivot(1, 2) },
-	qr/pivot point should not be in a basis column/,
-	"can't pivot in basis column (1,2)"
-);    # probably shouldn't be doing this either.
-throws_ok(
-	sub { $tableau1->find_next_basis_from_pivot(1, 3) },
-	qr/pivot point should not be in a basis column.*/,
-	"can't pivot in basis column (1,3)"
-);    # probably shouldn't be doing this.
-is_deeply($tableau1->find_next_basis_from_pivot(2, 1), Set(1, 2), "find next basis from pivot (2,1)");
-is_deeply($tableau1->find_next_basis_from_pivot(1, 1), Set(1, 3), "find next basis from pivot (1,1)");
-
-$tableau1->basis(5, 6);
-note("\nbasis is ",              $tableau1->basis());
-note($tableau1->current_tableau, "\n");
-note("find next short cut pivots");
-# ($row_index, $value, $feasible_point) = $self->find_short_cut_row()
-is_deeply([ $tableau1->find_short_cut_row() ],     [ 1, -4700, 0 ], "row 1");
-is_deeply([ $tableau1->find_short_cut_column(1) ], [ 1, -5000, 0 ], "column 1 ");
-is_deeply([ $tableau1->next_short_cut_pivot() ],   [ 1, 1, 0, 0 ],  "pivot (1,1)");
-is_deeply([ $tableau1->next_short_cut_basis() ],   [ 1, 6, undef ], "new basis {1,6} continue");
-$tableau1->current_tableau(1, 6);
-note($tableau1->current_tableau);
-
-is_deeply([ $tableau1->find_short_cut_row ],       [ 2, Value::Real->new(-8.4E+06), 0 ], "find short cut row");
-is_deeply([ $tableau1->find_short_cut_column(2) ], [ 2, Value::Real->new(-1.3E+06), 0 ], "find short cut col 2 ");
-is_deeply([ $tableau1->next_short_cut_pivot() ],   [ 2, 2, 0, 0 ],                       "pivot (2,2)");
-is_deeply([ $tableau1->next_short_cut_basis() ],   [ 1, 2, undef ],                      "new basis {1,2} continue");
-
-$tableau1->current_tableau(1, 2);
-note($tableau1->current_tableau);
-
-is_deeply([ $tableau1->next_short_cut_pivot() ], [ undef, undef, 1, 0 ], "feasible point found");
-is_deeply(
-	[ $tableau1->next_short_cut_basis() ],
-	[ 1, 2, 'feasible_point' ],
-	"all constraints positive at basis {1,2} --start phase2"
-);
-is_deeply([ $tableau1->find_pivot_column('max') ], [ 3, Value::Real->new(-100000), 0 ],      "col 3");
-is_deeply([ $tableau1->find_pivot_row(3) ],        [ 1, Value::Real->new(550000 / 500), 0 ], "row 1 ");
-is_deeply([ $tableau1->find_next_pivot('max') ],   [ 1, 3, 0, 0 ],                           "pivot (1,3)");
-is_deeply([ $tableau1->find_next_basis('max') ],   [ 2, 3, undef ], "new basis {2,3} continue");
-
-$tableau1->current_tableau(2, 3);
-note($tableau1->current_tableau);
-is_deeply([ $tableau1->find_pivot_column('max') ], [ 4, Value::Real->new(-300), 0 ], "col 4");
-is_deeply([ $tableau1->find_pivot_row(4) ], [ 1, 4500, 0 ], "row 2) ");
-
-is_deeply([ $tableau1->find_next_pivot('max') ], [ 1, 4, 0, 0 ], "pivot 1,4");
-is_deeply([ $tableau1->find_next_basis('max') ], [ 3, 4, undef ], "new basis {3,4} continue");
-
-$tableau1->current_tableau(3, 4);
-note($tableau1->current_tableau);
-is_deeply([ $tableau1->find_pivot_column('max') ], [ 5, Value::Real->new(-1), 0 ], "col 5");
-is_deeply([ $tableau1->find_pivot_row(5) ], [ undef, undef, 1 ], "row 2) ");
-
-is_deeply([ $tableau1->find_next_pivot('max') ], [ undef, 5, 0, 1 ], "unbounded -- no pivot");
-is_deeply([ $tableau1->find_next_basis('max') ], [ 3, 4, 'unbounded' ], "basis 3,4 unbounded");
+
+subtest 'Current constraint matrix' => sub {
+	is $tableau1->{current_constraint_matrix}->string,
+		$test_constraint_matrix->string,
+		'initialization of current_constraint_matrix';
+	is
+		$tableau1->{current_constraint_matrix}->string,
+		$tableau1->current_constraint_matrix->string,
+		'current_constraint_matrix accessor';
+	is $tableau1->{current_b}->string, $tableau1->{b}->string,       'initialization of current_b';
+	is $tableau1->{current_b}->string, $tableau1->current_b->string, 'current_b accessor';
+
+	is [ $tableau1->current_b->dimensions ], [ 2, 1 ],               'dimensions of current_b';
+};
+
+subtest 'Objective row properties' => sub {
+	my $obj_row_test = [ ((-$c)->value, 0, 0, 1, 0) ];
+
+	for (my $i = 0; $i < 4; $i++) {
+		is $tableau1->objective_row->[$i]->string, $obj_row_test->[$i]->string,
+			'initialization of $tableau->{obj_row} (first half)';
+	}
+	is @{$tableau1->objective_row}[4..7],
+		@{$obj_row_test}[4..7],
+		'initialization of $tableau->{obj_row} (second half)';
+
+	is $tableau1->{obj_row}, object { prop isa => 'Value::Matrix' }, '->{obj_row} has type Value::Matrix';
+	is $tableau1->obj_row,   object { prop isa => 'Value::Matrix' }, '->obj_row has type Value::Matrix';
+	is $tableau1->obj_row->string,   $tableau1->{obj_row}->string,   'verify mutator for {obj_row}';
+	is ref $tableau1->objective_row, 'ARRAY',                        '->objective_row has type ARRAY';
+
+    # the first 4 elements are Value::Real's and the remainder are perl scalars (numbers)
+    # these are all mapped to array refs of scalars
+    # should these use the validator( $compare_data ) pattern below?
+    is  [ map { ref $_ ? $_->{data} : [$_] } $tableau1->objective_row->@* ],
+        [ map { ref $_ ? $_->{data} : $_ } $tableau1->{obj_row}->value ],
+        'access to {obj_row}';
+    is  [ map { ref $_ ? $_->{data} : [$_] } $tableau1->objective_row->@* ],
+        [ map { ref $_ ? $_->{data} : $_ } $tableau1->obj_row->value ],
+        'objective_row is obj_row->value = ARRAY';
+};
+
+subtest 'Current tableau' => sub {
+	is $tableau1->current_tableau,
+		object { prop isa => 'Value::Matrix' },
+		'-> current_tableau is Value::Matrix';
+	is $tableau1->current_tableau,
+		Matrix($ra_matrix)->string,
+		'entire tableau including obj coeff row';
+
+	is $tableau1->S, object { prop isa => 'Value::Matrix' }, 'slack variables are a Value::Matrix';
+	is $tableau1->S, $tableau1->I($tableau1->m)->string,     'slack variables are identity matrix';
+};
+
+subtest 'Verify stringify subroutine' => sub {
+	my $aref = [ [qw/1 2/], 7, [3, 0.4], [ (5, -.6, [8, 9])], 0, -1, [qw/-2 -3/]];
+	my $expected_string = '[[1,2],7,[3,0.4],[5,-0.6,[8,9]],0,-1,[-2,-3]]';
+	is stringify($aref), $expected_string, 'Local stringify recursively descends the refs';
+};
+
+# try out validator for mixed data types
+my $compare_data = sub {
+	my %params = @_;
+
+	# postfix dereferencing stable in perl 5.24
+	my ($g, $e) = map { ref $_ =~ /Value/ ? $_->copy : $_ } @{$params{got}};
+
+	my ($got, $exp);
+	$got = ref $g eq 'Value::Matrix' ? $g->string : stringify($g);
+	$exp = ref $e eq 'Value::Matrix' ? $e->string : stringify($e);
+
+	return is $got, $exp, 'Compare datastructures of MathObjects';
+};
+
+subtest 'Verify objective_row methods and properties' => sub {
+	is [ $tableau1->obj_row, $tableau1->{obj_row} ],
+		validator( $compare_data ),
+		'verify mutator for {obj_row}';
+
+	is [ $tableau1->objective_row, [$tableau1->obj_row->value] ],
+		validator( $compare_data ),
+		'objective_row is obj_row->value = ARRAY';
+};
+
+subtest 'test basis' => sub {
+	is ref $tableau1->basis_columns, 'ARRAY',  '{basis_column} has type ARRAY';
+	is [$tableau1->basis_columns,  [ 5, 6 ]], validator( $compare_data ), 'initialization of basis';
+	is(
+		ref($tableau1->current_basis_matrix),
+		ref(Value::Matrix->I($tableau1->m)),
+		'current_basis_matrix type is MathObjectMatrix'
+	);
+	is $tableau1->current_basis_matrix->string,
+		Value::Matrix->I($tableau1->m)->string,
+		'initialization of basis';
+};
+
+
+subtest 'change basis and test again' => sub {
+	$tableau1->basis(2, 3);
+
+	is ref $tableau1->basis_columns, 'ARRAY',  '{basis_column} has type ARRAY';
+	is [$tableau1->basis_columns, [ 2, 3 ]], validator( $compare_data ), ' basis columns set to {2,3}';
+	is(
+		ref($tableau1->current_basis_matrix),
+		ref($test_constraint_matrix->column_slice(2, 3)),
+		'current_basis_matrix type is MathObjectMatrix'
+	);
+	is(
+		$tableau1->current_basis_matrix->string,
+		$test_constraint_matrix->column_slice(2, 3)->string,
+		'basis_matrix for columns {2,3} is correct'
+	);
+	is $tableau1->basis(Set(2, 3))->string,  List([ 2, 3 ])->string, '->basis(Set(2,3))';
+	is $tableau1->basis(List(2, 3))->string, List([ 2, 3 ])->string, '->basis(List(2,3))';
+	is $tableau1->basis([ 2, 3 ])->string,   List([ 2, 3 ])->string, '->basis([2,3])';
+};
+
+subtest 'find basis column index corresponding to row index' => sub {
+	# and value of the basis coefficient
+
+	$tableau1->basis(5, 6);
+	note("\nbasis is",                     $tableau1->basis(5, 6));
+	note(print $tableau1->current_tableau, "\n");
+	is [ $tableau1->find_leaving_column(1) ], [ 5, 1 ],
+		'find_leaving_column returns [col_index, pivot_value] ';
+	is [ $tableau1->find_leaving_column(2) ], [ 6, 1 ],
+		'find_leaving_column returns [col_index, pivot_value] ';
+
+	is $tableau1->find_next_basis_from_pivot(1, 2)->string, Set(2, 6)->string,
+		'find next basis from pivot (1,2)';
+	is $tableau1->find_next_basis_from_pivot(1, 3)->string, Set(3, 6)->string,
+		'find next basis from pivot (1,3)';
+	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 5)->string,
+		'find next basis from pivot (2,1)';
+	is $tableau1->find_next_basis_from_pivot(1, 1)->string, Set(1, 6)->string,
+		'find next basis from pivot (1,1)';
+
+	like(
+		dies { $tableau1->find_next_basis_from_pivot(2, 5)  },
+		qr/pivot point should not be in a basis column/,
+		"can't pivot in basis column (2,5)"
+	);    # probably shouldn't be doing this.
+	like(
+		dies { $tableau1->find_next_basis_from_pivot(1, 6) },
+		qr/pivot point should not be in a basis column/,
+		"can't pivot in basis column (2,6)"
+	);    # probably shouldn't be doing this.
+
+	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 5)->string,
+		'find next basis from pivot (2,1)';
+	like(
+		dies { $tableau1->find_next_basis_from_pivot(2, 6) },
+		qr/pivot point should not be in a basis column/,
+		"can't pivot in basis column (2,6)"
+	);    # probably shouldn't be doing this.
+};
+
+subtest 'find another basis (2,3)' => sub {
+	$tableau1->basis(2, 3);
+	note("\nbasis is",                     $tableau1->basis());
+	note(print $tableau1->current_tableau, "\n");
+
+	is [ $tableau1->find_leaving_column(1) ], [ 2, 500 ],
+		'find_leaving_column returns [col_index, pivot_value] ';
+	is [ $tableau1->find_leaving_column(2) ], [ 3, 500 ],
+		'find_leaving_column returns [col_index, pivot_value] ';
+
+	like(
+		dies { $tableau1->find_next_basis_from_pivot(1, 2) },
+		qr/pivot point should not be in a basis column/,
+		"can't pivot in basis column (1,2)"
+	);    # probably shouldn't be doing this either.
+	like(
+		dies { $tableau1->find_next_basis_from_pivot(1, 3) },
+		qr/pivot point should not be in a basis column/,
+		"can't pivot in basis column (1,3)"
+	);    # probably shouldn't be doing this.
+
+	is $tableau1->find_next_basis_from_pivot(2, 1)->string, Set(1, 2)->string,
+		'find next basis from pivot (2,1)';
+	is $tableau1->find_next_basis_from_pivot(1, 1)->string, Set(1, 3)->string,
+		'find next basis from pivot (1,1)';
+};
+
+subtest 'find next short cut pivots' => sub {
+	$tableau1->basis(5, 6);
+	note("\nbasis is ",              $tableau1->basis());
+	note($tableau1->current_tableau, "\n");
+
+	# ($row_index, $value, $feasible_point) = $self->find_short_cut_row()
+
+	is [ $tableau1->find_short_cut_row() ],     [ 1, -4700, 0 ], 'row 1';
+	is [ $tableau1->find_short_cut_column(1) ], [ 1, -5000, 0 ], 'column 1 ';
+	is [ $tableau1->next_short_cut_pivot() ],   [ 1, 1, 0, 0 ],  'pivot (1,1)';
+	is [ $tableau1->next_short_cut_basis() ],   [ 1, 6, undef ], 'new basis {1,6} continue';
+
+	$tableau1->current_tableau(1, 6);
+	note($tableau1->current_tableau);
+
+	is [ $tableau1->find_short_cut_row ],
+		[ 2, Value::Real->new(-8.4E+06)->string, 0 ], 'find short cut row';
+	is [ $tableau1->find_short_cut_column(2) ],
+		[ 2, Value::Real->new(-1.3E+06)->string, 0 ], 'find short cut col 2 ';
+	is [ $tableau1->next_short_cut_pivot() ], [ 2, 2, 0, 0 ],  'pivot (2,2)';
+	is [ $tableau1->next_short_cut_basis() ], [ 1, 2, undef ], 'new basis {1,2} continue';
+
+	$tableau1->current_tableau(1, 2);
+	note($tableau1->current_tableau);
+
+	is [ $tableau1->next_short_cut_pivot() ], [ undef, undef, 1, 0 ], 'feasible point found';
+	is(
+		[ $tableau1->next_short_cut_basis() ],
+		[ 1, 2, 'feasible_point' ],
+		'all constraints positive at basis {1,2} --start phase2'
+	);
+	is [ $tableau1->find_pivot_column('max') ], [ 3, Value::Real->new(-100000)->string, 0 ],      'col 3';
+	is [ $tableau1->find_pivot_row(3) ],        [ 1, Value::Real->new(550000 / 500)->string, 0 ], 'row 1';
+	is [ $tableau1->find_next_pivot('max') ],   [ 1, 3, 0, 0 ],  'pivot (1,3)';
+	is [ $tableau1->find_next_basis('max') ],   [ 2, 3, undef ], 'new basis {2,3} continue';
+
+	$tableau1->current_tableau(2, 3);
+	note($tableau1->current_tableau);
+	is [ $tableau1->find_pivot_column('max') ], [ 4, Value::Real->new(-300)->string, 0 ], 'col 4';
+	is [ $tableau1->find_pivot_row(4) ], [ 1, 4500, 0 ], 'row 2';
+
+	is [ $tableau1->find_next_pivot('max') ], [ 1, 4, 0, 0 ], 'pivot 1,4';
+	is [ $tableau1->find_next_basis('max') ], [ 3, 4, undef ], 'new basis {3,4} continue';
+
+	$tableau1->current_tableau(3, 4);
+	note($tableau1->current_tableau);
+	is [ $tableau1->find_pivot_column('max') ], [ 5, Value::Real->new(-1)->string, 0 ], 'col 5';
+	is [ $tableau1->find_pivot_row(5) ], [ undef, undef, 1 ], 'row 2';
+
+	is [ $tableau1->find_next_pivot('max') ], [ undef, 5, 0, 1 ],    'unbounded -- no pivot';
+	is [ $tableau1->find_next_basis('max') ], [ 3, 4, 'unbounded' ], 'basis 3,4 unbounded';
+};
 # note that the column is returned from find_next_pivot so one can find a certificate
 # of unboundedness (can return a line going off to infinity)
 
@@ -292,31 +418,44 @@ is_deeply([ $tableau1->find_next_basis('max') ], [ 3, 4, 'unbounded' ], "basis 3
 # # "unbounded, feasible_point, infeasible_tableau, optimal"?
 # # it might be easier to remember.
 #
-note("reset tableau to feasible point and try to minimize it for phase2");
-$tableau1->current_tableau(1, 2);
-note($tableau1->current_tableau);
-is_deeply([ $tableau1->next_short_cut_pivot() ], [ undef, undef, 1, 0 ], "feasible point found");
-is_deeply(
-	[ $tableau1->next_short_cut_basis() ],
-	[ 1, 2, 'feasible_point' ],
-	"all constraints positive at basis {1,2} --start phase2"
-);
-
-is_deeply([ $tableau1->find_pivot_column('min') ], [ undef, undef, 1 ],                          "all neg coeff");
-is_deeply([ $tableau1->find_pivot_row(1) ],        [ 1, Value::Real->new(550000 / 1300000), 0 ], "row 1 ");
-is_deeply([ $tableau1->find_next_pivot('min') ],   [ undef, undef, 1, 0 ],                       "optimum");
-is_deeply([ $tableau1->find_next_basis('min') ],   [ 1, 2, 'optimum' ],                          "optimum");
-#
-#
 
-is_deeply(
-	$tableau1->statevars,    # round off errors
-	[ 550000 / 1300000, 8400000 / 1300000, 0, 0, 0, 0, 8.339999999999999E9 / 1300000 ], "state variables"
-);
+subtest 'reset tableau to feasible point and try to minimize it for phase2' => sub {
+	$tableau1->current_tableau(1, 2);
+	note($tableau1->current_tableau);
+	is [ $tableau1->next_short_cut_pivot() ], [ undef, undef, 1, 0 ], 'feasible point found';
+	is(
+		[ $tableau1->next_short_cut_basis() ],
+		[ 1, 2, 'feasible_point' ],
+		'all constraints positive at basis {1,2} --start phase2'
+	);
+
+	is [ $tableau1->find_pivot_column('min') ], [ undef, undef, 1 ], 'all neg coeff';
+	is [ $tableau1->find_pivot_row(1) ],
+		[ 1, Value::Real->new(550000 / 1300000)->string, 0 ],
+		'row 1';
+	is [ $tableau1->find_next_pivot('min') ], [ undef, undef, 1, 0 ], 'optimum';
+	is [ $tableau1->find_next_basis('min') ], [ 1, 2, 'optimum' ],    'optimum';
+
+	is(
+		$tableau1->statevars,    # round off errors
+		[ 550000 / 1300000, 8400000 / 1300000, 0, 0, 0, 0, 8.339999999999999E9 / 1300000 ],
+		'state variables'
+	);
+
+	is $tableau1->align,    'cccc|cc|c|c',               'check align';
+	is $tableau1->toplevel, [qw(x1 x2 x3 x4 x5 x6 z b)], 'check toplevel';
+
+	# diag($tableau1->align);
+	# diag(join(q{ } , @{$tableau1->toplevel}));
+};
 
-is($tableau1->align, 'cccc|cc|c|c', "check align");
-is_deeply($tableau1->toplevel, [qw(x1 x2 x3 x4 x5 x6 z b)], "check toplevel");
 
-# diag($tableau1->align);
-# diag(join(" " , @{$tableau1->toplevel}));
 done_testing();
+
+sub stringify {
+	my $arrayref = shift;
+	warn "Not an array ref [$arrayref]" unless ref $arrayref eq 'ARRAY';
+	return sprintf("[%s]",
+		join(',', map { my $s = $_; ref $s eq 'ARRAY' ? stringify($s) : $s } @{$arrayref})
+	);
+}
diff --git a/t/math_objects/factorial.t b/t/math_objects/factorial.t
new file mode 100644
index 0000000000..5342054ad5
--- /dev/null
+++ b/t/math_objects/factorial.t
@@ -0,0 +1,78 @@
+use warnings;
+use strict;
+
+package main;
+
+use Test::More;
+use Test::Exception;
+
+# The following needs to include at the top of any testing down to END OF TOP_MATERIAL.
+
+BEGIN {
+	die 'PG_ROOT not found in environment.\n' unless $ENV{PG_ROOT};
+	$main::pg_dir = $ENV{PG_ROOT};
+}
+
+use lib "$main::pg_dir/lib";
+
+require("$main::pg_dir/t/build_PG_envir.pl");
+
+## END OF TOP_MATERIAL
+
+use Parser;
+
+loadMacros('MathObjects.pl');
+
+Context('Numeric');
+Context()->variables->add(y => "Real");
+Context()->variables->add(n => "Real");
+
+my $five_fact = Compute('5!');
+
+use Data::Dumper;
+print Dumper $five_fact->class;
+
+is($five_fact->class, 'Real',   'factorial: check class of object');
+is($five_fact->type,  'Number', 'factorial: check type of object');
+
+ok(Value::isValue($five_fact),    'factorial: check if an object is a value');
+ok(Value::isNumber($five_fact),   'factorial: check if an object is a number');
+ok(Value::isReal($five_fact),     'factorial: check if a number is a real number');
+ok(!Value::isComplex($five_fact), 'factorial: check if an integer is complex');
+ok(!Value::isFormula($five_fact), 'factorial: check if a number is not a formula');
+
+is($five_fact->value,120, 'factorial: 5! is 120');
+is(Compute("0!")->value, 1, 'factorial: 0! is 1');
+
+note('The double factorial is not defined here.');
+my $four_double_fact = Compute("4!!")->value;
+ok(6.2e+23 < $four_double_fact && $four_double_fact < 6.3e+23, 'factorial: 4!! is defined as (4!)!=24!' );
+
+ok(Compute("170!") > 1e+306, 'factorial: 170! is large but not infinite.');
+
+note('Tests for throwing exceptions.');
+
+throws_ok {
+	Compute("(-1)!");
+}
+qr/Factorial can only be taken of \(non-negative\) integers/, 'factorial: can\'t take factorial of negative integers.';
+
+throws_ok {
+	Compute("1.5!");
+}
+qr/Factorial can only be taken of \(non-negative\) integers/, 'factorial: can\'t take factorial of non-integer reals.';
+
+note('Try taking factorials of variables');
+my $n_fact = Compute("n!");
+is($n_fact->class, "Formula", "factorial: n! is a Formula");
+is($n_fact->type,  "Number",  "factorial: n! has type is Number");
+is($n_fact->eval(n=>5), 120, 'factorial: n! evaluated at n=5 is correct.');
+
+# check infinite values
+note('Tests for infinite values');
+
+my $large_fact = Compute('171!');
+my $inf = Compute('inf');
+is($large_fact->value, $inf, '171! is infinite.');
+
+done_testing();
diff --git a/t/units/basic_module.t b/t/units/basic_module.t
new file mode 100644
index 0000000000..0ad70fe095
--- /dev/null
+++ b/t/units/basic_module.t
@@ -0,0 +1,24 @@
+use Test2::V0;
+
+use Units;
+
+# get unit hashes
+my %joule = evaluate_units('J');
+my %newton_metre = evaluate_units('N*m');
+my %energy_base_units = evaluate_units('kg*m^2/s^2');
+
+# basic definitions of energy equivalence
+is \%joule, \%newton_metre,
+    'A joule is a newton-metre';
+is \%joule, \%energy_base_units,
+    'A joule is a kg metre squared per second squared';
+
+
+# test the error handling
+my $fake = 'bleurg';
+ok my %error = evaluate_units($fake);
+like $error{ERROR}, qr/UNIT ERROR Unrecognizable unit: \|$fake\|/,
+    "No unit '$fake' defined in Units file";
+
+
+done_testing;
diff --git a/t/units/basic_parser.t b/t/units/basic_parser.t
new file mode 100644
index 0000000000..81e2be0f9e
--- /dev/null
+++ b/t/units/basic_parser.t
@@ -0,0 +1,202 @@
+use Test2::V0;
+
+use Parser::Legacy::NumberWithUnits; # load this before the parser macro
+use Units;
+
+use lib 't/lib';
+use Test::PG;
+
+loadMacros("parserNumberWithUnits.pl");
+
+Context("Numeric");
+
+
+=head1 NumberWithUnits
+
+We test the basic functionality of the NumberWithUnits parser,
+F, to give us faith
+that the parser and its methods are working.
+Other test files will probe deeper into specific use cases of
+the NumberWithUnits macro and the L module.
+
+=head2 Testing Strategy
+
+Test all the methods of an object, check the attributes, verify the errors
+are thrown correctly, display strings look the way they should and
+all the ways that a student could answer produce the appropriate results.
+Check that the objects we create belong to their expected class.
+
+Demonstrate some of the flavour of Test2, with hash, bag, dies, todo, etc.
+Group similar tests into subtests.
+
+=head3 Setup
+
+All the boilerplate code is loaded with Test::PG and assume that people run
+it from the root directory with C.
+Load your base modules before loading the macros which depend on them
+and set the Context, if appropriate.
+
+See the example in the documentation of L
+
+  perldoc t/lib/Test/PG.pm
+
+=head2 TODO list
+
+=over 4
+
+=item Fix display of temperature units
+
+=item Test adding new units
+
+=item Look up how to get the value of the object instead of reaching into the hashref
+
+=item Test messages from wrong student answer submissions
+
+=back
+
+=cut
+
+
+# define some basic objects
+ok my $joule = NumberWithUnits(1, 'J');
+ok my $Nm    = NumberWithUnits(1, 'N*m');
+ok my $energy_base_units = NumberWithUnits(1, 'kg*m^2/s^2');
+
+
+subtest 'Verify classes and methods' => sub {
+    isa_ok $joule, 'Parser::Legacy::NumberWithUnits';
+    can_ok $joule,
+        [ qw/cmp splitUnits getUnitNames getUnits TeXunits cmp_parse adjustCorrectValue
+             add_fundamental_unit add_unit string TeX / ],
+        'Can we NumberWithUnits';
+
+    ok my $evaluator = $joule->cmp($Nm), 'Get an AnswerEvaluator';
+    isa_ok $evaluator, 'AnswerEvaluator';
+    can_ok $evaluator, [ qw/ evaluate / ], 'We Can Evaluate';
+};
+
+
+subtest 'Check attributes' => sub {
+    is(
+        $joule,
+        {
+            data      => [ 1 ],
+            units     => 'J',
+            units_ref => { kg => 1, m => 2, s => -2, factor => 1,
+                           amp => 0, cd => 0, mol => 0, rad => 0,
+                           degC => 0, degF => 0, degK => 0,
+            },
+            isValue => T(),
+            context => check_isa 'Parser::Context',
+        },
+        'This looks like a joule'
+    );
+};
+
+subtest 'Basic definitions of energy equivalence' => sub {
+    is $joule->{data}->[0], $Nm->{data}->[0], 'One joule is one newton-metre';
+    is $joule->getUnits, $Nm->getUnits, 'A joule has the same dimensions as a newton-metre';
+
+    is (check_score($joule, $Nm), 1, 'A Joule is a Newton-metre');
+    is (check_score($joule, $energy_base_units), 1, 'A Joule can be expressed in SI base units');
+};
+
+subtest 'Test error handling' => sub {
+    my $fake = 'bleurg';
+
+    like(
+        dies { NumberWithUnits(1, "$fake") },
+        qr/Unrecognizable unit: \|$fake\|/,
+        "No unit '$fake' defined in Units file"
+    );
+    like(
+        dies { NumberWithUnits(1) },
+        qr/You must provide units for your number/,
+        "No unit given"
+    );
+    like(
+        dies { NumberWithUnits('J') },
+        qr/You must provide units for your number/,
+        "No value given, wants 2 arguments"
+    );
+};
+
+subtest 'Check parsing of arguments' => sub {
+    ok my $three_args = NumberWithUnits(1, 'N', 'm'), 'Ignores extra argument';
+    is $three_args->string, '1 N', 'Only sees the first unit';
+
+    ok my $string_arg = NumberWithUnits('1J'), 'Parses string argument';
+    is $string_arg->string, '1 J', 'Parses string correctly';
+};
+
+subtest 'Check some known units' => sub {
+    ok my @unit_names = (split /\|/, $joule->getUnitNames), 'Can getUnitNames';
+
+    is \@unit_names,
+        bag {
+            all_items( match qr/^[-%\w]+$/ );
+            item 'J'; item 'N';
+            item 'm'; item 'kg'; item 's';
+            etc;
+        },
+        'Basic units loaded, sanity check';
+};
+
+subtest 'Check other methods' => sub {
+    is [ $joule->splitUnits ], ['1', 'J'], 'splitUnits creates an array';
+
+    is $joule->adjustCorrectValue, 0, 'What is adjustCorrectValue?';
+};
+
+subtest 'Check display methods' => sub {
+    is $joule->string,   '1 J',           'Displays string - Joule';
+    is $joule->TeX,      '1\ {\rm J}',    'Displays LaTeX string - Joule';
+    is $joule->TeXunits, '{\rm 1 J}',     'Displays LaTeX string - Joule';
+    is $Nm->TeX,         '1\ {\rm N\,m}', 'Displays LaTeX string - Newton metre';
+    like $energy_base_units->TeX,
+        qr/ 1\\ \s \{ \S* \\frac\{ \\rm\S* \s kg \\, m\^\{2\}\} \{\\rm\S* \s s\^\{2\}\}\} /x,
+        'Displays LaTeX string - energy in SI base units';
+
+    ok my $celsius = NumberWithUnits(1, 'degC');
+    ok my $kelvin  = NumberWithUnits(1, 'degK');
+    todo 'Fix the display of temperatures' => sub {
+        is $celsius->TeX, '1\ {\rm ^{\circ}C}', 'Displays LaTeX string for degrees (finally)';
+        is $kelvin->TeX,  '1\ {\rm K}',         'Displays LaTeX string for kelvin, no degree sign';
+    };
+};
+
+subtest 'Check possible answer format branches' => sub {
+    # re-write without check_score so we can get the messages to students
+
+    is check_score($joule, '1 J'),       1, 'one Joule plain';
+    is check_score($joule, '1.00 J'),    1, 'one Joule float';
+    is check_score($joule, '1E0 J'),     1, 'one Joule exponential notation';
+    is check_score($joule, '7/7 J'),     1, 'one Joule value calculated';
+    is check_score($joule, '1 J^1'),     1, 'one Joule to the power of one';
+    is check_score($joule, 'J 1'),       0, 'one Joule wrong order';
+    is check_score($joule, '2 J'),       0, 'one Joule wrong value';
+    is check_score($joule, '1 j'),       0, 'one Joule wrong case';
+    is check_score($joule, '1'),         0, 'one Joule missing unit';
+    is check_score($joule, 'J'),         0, 'one Joule missing value';
+    is check_score($joule, '1J'),        1, 'one Joule missing space between value and unit is valid';
+    is check_score($joule, '1 N'),       0, 'one Joule wrong unit force not energy';
+    is check_score($joule, '1 Nm'),      0, 'one Joule Nm missing *';
+    is check_score($joule, '1 N*m'),     1, 'one Joule as Newton metre';
+    is check_score($joule, '1 Joule'),   0, 'one Joule in words';
+    is check_score($joule, '1E-3 kJ'),   1, 'one Joule value as exponential';
+};
+
+todo 'check_score is stateful.  Cannot handle repeated calls' => sub { 
+    is check_score($joule, '1E-3 kJ'),   1, 'one Joule value as exponential second call';
+    is check_score($joule, '1E-3 kJ'),   1, 'one Joule value as exponential third call';
+
+    # the other tests I'd like to run
+    is check_score($joule, '0.001 kJ'),     1, 'one Joule decimal kJ';
+    is check_score($joule, '1/1000 kJ'),    1, 'one Joule fractional kJ';
+    is check_score($joule, '10^-3 kJ'),     1, 'one Joule latex power kJ';
+    is check_score($joule, '1 x 10^-3 kJ'), 1, 'one Joule scientific notation';
+    is check_score($joule, '10**-3 kJ'),    1, 'one Joule power of 10 kJ';
+};
+
+
+done_testing;
diff --git a/t/units/electron_volts.t b/t/units/electron_volts.t
new file mode 100644
index 0000000000..2a86920c6f
--- /dev/null
+++ b/t/units/electron_volts.t
@@ -0,0 +1,44 @@
+use Test2::V0;
+
+use Units;
+
+my %joule = evaluate_units('J');
+my %newton_metre = evaluate_units('N*m');
+my %base_units = evaluate_units('kg*m^2/s^2');
+
+my %electron_volt = evaluate_units('eV');
+my %kev = evaluate_units('keV');
+my %mev = evaluate_units('MeV');
+my %gev = evaluate_units('GeV');
+
+SKIP: {
+	skip('New eV units not available until PG-2.17')
+		if $kev{ERROR} =~ /^UNIT ERROR Unrecognizable unit/;
+
+	is \%electron_volt, by_factor( 1.6022E-19, \%joule ),
+		'eV and joules differ by a factor of 1.6022 x 10^19';
+
+	is \%kev, by_factor( 1000, \%electron_volt ),  'kilo is factor 1000';
+	is \%mev, by_factor( 10**6, \%electron_volt ), 'mega is factor 10^6';
+	is \%gev, by_factor( 10**9, \%electron_volt ), 'giga is factor 10^9';
+}
+
+subtest 'electron volt has units of energy' => sub {
+	my ($ev, $J) = ( { %electron_volt }, { %joule } );
+	delete $ev->{factor};
+	delete $J->{factor};
+
+	is $ev, $J, 'electron volt has units of energy';
+};
+
+
+done_testing;
+
+sub by_factor {
+    my ($value, $unit) = @_;
+    my $new_unit = { %$unit }; # shallow copy hash values
+
+    $new_unit->{factor} *= $value;
+
+    return $new_unit;
+}