From 4652a1f231e3383c069e7101d77d8683f487eb6a Mon Sep 17 00:00:00 2001 From: Glenn Rice <47527406+drgrice1@users.noreply.github.com> Date: Wed, 14 Jul 2021 15:39:59 -0500 Subject: [PATCH 001/134] Update the develop version. (#590) --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index d59de1c529..e9d9c39ee9 100644 --- a/VERSION +++ b/VERSION @@ -1,4 +1,4 @@ -$PG_VERSION ='2.16'; +$PG_VERSION ='2.16+develop'; $PG_COPYRIGHT_YEARS = '1996-2021'; 1; From 8addfac0d8113468521eb056c245e8ae351b8f64 Mon Sep 17 00:00:00 2001 From: Michael Gage <1203580+mgage@users.noreply.github.com> Date: Tue, 20 Jul 2021 06:29:52 -0400 Subject: [PATCH 002/134] fix type replace assemble_matrix with assemble_tableau (#592) --- macros/tableau.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/tableau.pl b/macros/tableau.pl index 95d42f8725..1eaab6cd87 100755 --- a/macros/tableau.pl +++ b/macros/tableau.pl @@ -515,7 +515,7 @@ sub initialize { $self->{align} = ($self->{align})//$myAlignString; $self->{S} = Value::Matrix->I($m); $self->{basis_columns} = [($n+1)...($n+$m)] unless ref($self->{basis_columns})=~/ARRAY/; - my @rows = $self->assemble_matrix; + my @rows = $self->assemble_tableau; $self->M( _Matrix([@rows]) ); #original matrix $self->{data}= $self->M->data; my $new_obj_row = $self->objective_row; From 1e910a72666e422b4c4c8c6b353e36b5e2dcc843 Mon Sep 17 00:00:00 2001 From: Michael Gage Date: Thu, 22 Jul 2021 20:13:57 -0400 Subject: [PATCH 003/134] Fixed a \1 by replacing by in a regex expression, qualified length() with CORE:: --- lib/Value/Real.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Value/Real.pm b/lib/Value/Real.pm index 5817dd84ed..608b8a47b1 100644 --- a/lib/Value/Real.pm +++ b/lib/Value/Real.pm @@ -144,14 +144,14 @@ sub compare { $b += ($b <=> 0) * "1E$exp"; # Same for $b my $bd = sprintf("%.${tdigits}E", $b); # Round $b to the number of tdigits $bd =~ s/^.*\.(.*?)0*E.*$/$1/; # Get the decimal part without trailing zeros - my $bn = length($bd); # Number of those decimal digits + my $bn = CORE::length($bd); # Number of those decimal digits $bn = $digits if ($bn < $digits); # (with a minimum of $digits); my $aE = sprintf("%.${bn}E", $a); # Round $a to $bn digits my $bE = sprintf("%.${bn}E", $b); # Round $b to $bn digits return 0 if $aE eq $bE; # Return equal if they are if ($self->getFlag('tolTruncation')) { # If truncation is allowed $aE = sprintf("%.15E", $a); # Get $a to full resolution - $aE =~ s/\.(\d{$bn}).*E/.\1E/; $aE =~ s/\.E/E/; # Truncate it to the required number of digits + $aE =~ s/\.(\d{$bn}).*E/.$1E/; $aE =~ s/\.E/E/; # Truncate it to the required number of digits return 0 if $aE eq $bE; # Return equal if they are } # return $a <=> $b; # Otherwise compare numbers as perl reals From d9cc7e555cb2b16b4c9448f0ecef9e38e26367e0 Mon Sep 17 00:00:00 2001 From: Michael Gage Date: Thu, 22 Jul 2021 20:15:33 -0400 Subject: [PATCH 004/134] tan() is not a native perl function so there is no need to override it --- lib/Complex1.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Complex1.pm b/lib/Complex1.pm index 23f086332f..5f9a3b3066 100644 --- a/lib/Complex1.pm +++ b/lib/Complex1.pm @@ -14,7 +14,7 @@ use vars qw($VERSION @ISA @EXPORT %EXPORT_TAGS); my ( $i, $ip2, %logn ); -$VERSION = sprintf("%s", q$Id$ =~ /(\d+\.\d+)/); +# $VERSION = sprintf("%s", q$Id$ =~ /(\d+\.\d+)/); # not finding $Id ??? @ISA = qw(Exporter); @@ -57,7 +57,7 @@ use overload 'log' => \&log, 'sin' => \&sin, 'cos' => \&cos, - 'tan' => \&tan, +# 'tan' => \&tan, # perl doesn't have a native tan() function so you can't override it 'atan2' => \&atan2, qw("" stringify); From 1b69d7a2212391ff64ad6c07fe795657ca978957 Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Tue, 27 Jul 2021 10:38:45 -0400 Subject: [PATCH 005/134] move wwsafe --- lib/WWSafe.pm | 628 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 628 insertions(+) create mode 100644 lib/WWSafe.pm diff --git a/lib/WWSafe.pm b/lib/WWSafe.pm new file mode 100644 index 0000000000..516d0e038f --- /dev/null +++ b/lib/WWSafe.pm @@ -0,0 +1,628 @@ +package WWSafe; + +#use 5.003_11; +use 5.12.0; +use strict; +use utf8; +$Safe::VERSION = "2.16"; + +# *** Don't declare any lexicals above this point *** +# +# This function should return a closure which contains an eval that can't +# see any lexicals in scope (apart from __ExPr__ which is unavoidable) + +sub lexless_anon_sub { + # $_[0] is package; + # $_[1] is strict flag; + my $__ExPr__ = $_[2]; # must be a lexical to create the closure that + # can be used to pass the value into the safe + # world + + # Create anon sub ref in root of compartment. + # Uses a closure (on $__ExPr__) to pass in the code to be executed. + # (eval on one line to keep line numbers as expected by caller) + eval sprintf + 'package %s; %s strict; sub { @_=(); eval q[my $__ExPr__;] . $__ExPr__; }', + $_[0], $_[1] ? 'use' : 'no'; +} + +use Carp; +BEGIN { eval q{ + use Carp::Heavy; +} } + +use Opcode 1.01, qw( + opset opset_to_ops opmask_add + empty_opset full_opset invert_opset verify_opset + opdesc opcodes opmask define_optag opset_to_hex +); + +*ops_to_opset = \&opset; # Temporary alias for old Penguins + + +my $default_root = 0; +# share *_ and functions defined in universal.c +# Don't share stuff like *UNIVERSAL:: otherwise code from the +# compartment can 0wn functions in UNIVERSAL +my $default_share = [qw[ + *_ + &PerlIO::get_layers + &UNIVERSAL::isa + &UNIVERSAL::can + &UNIVERSAL::VERSION + &utf8::is_utf8 + &utf8::valid + &utf8::encode + &utf8::decode + &utf8::upgrade + &utf8::downgrade + &utf8::native_to_unicode + &utf8::unicode_to_native +], ($] >= 5.008001 && qw[ + &Regexp::DESTROY +]), ($] >= 5.010 && qw[ + &re::is_regexp + &re::regname + &re::regnames + &re::regnames_count + &Tie::Hash::NamedCapture::FETCH + &Tie::Hash::NamedCapture::STORE + &Tie::Hash::NamedCapture::DELETE + &Tie::Hash::NamedCapture::CLEAR + &Tie::Hash::NamedCapture::EXISTS + &Tie::Hash::NamedCapture::FIRSTKEY + &Tie::Hash::NamedCapture::NEXTKEY + &Tie::Hash::NamedCapture::SCALAR + &Tie::Hash::NamedCapture::flags + &UNIVERSAL::DOES + &version::() + &version::new + &version::("" + &version::stringify + &version::(0+ + &version::numify + &version::normal + &version::(cmp + &version::(<=> + &version::vcmp + &version::(bool + &version::boolean + &version::(nomethod + &version::noop + &version::is_alpha + &version::qv +]), ($] >= 5.011 && qw[ + &re::regexp_pattern +])]; + +sub new { + my($class, $root, $mask) = @_; + my $obj = {}; + bless $obj, $class; + + if (defined($root)) { + croak "Can't use \"$root\" as root name" + if $root =~ /^main\b/ or $root !~ /^\w[:\w]*$/; + $obj->{Root} = $root; + $obj->{Erase} = 0; + } + else { + $obj->{Root} = "Safe::Root".$default_root++; + $obj->{Erase} = 1; + } + + # use permit/deny methods instead till interface issues resolved + # XXX perhaps new Safe 'Root', mask => $mask, foo => bar, ...; + croak "Mask parameter to new no longer supported" if defined $mask; + $obj->permit_only(':default'); + + # We must share $_ and @_ with the compartment or else ops such + # as split, length and so on won't default to $_ properly, nor + # will passing argument to subroutines work (via @_). In fact, + # for reasons I don't completely understand, we need to share + # the whole glob *_ rather than $_ and @_ separately, otherwise + # @_ in non default packages within the compartment don't work. + $obj->share_from('main', $default_share); + Opcode::_safe_pkg_prep($obj->{Root}) if($Opcode::VERSION > 1.04); + return $obj; +} + +sub DESTROY { + my $obj = shift; + $obj->erase('DESTROY') if $obj->{Erase}; +} + +sub erase { + my ($obj, $action) = @_; + my $pkg = $obj->root(); + my ($stem, $leaf); + + no strict 'refs'; + $pkg = "main::$pkg\::"; # expand to full symbol table name + ($stem, $leaf) = $pkg =~ m/(.*::)(\w+::)$/; + + # The 'my $foo' is needed! Without it you get an + # 'Attempt to free unreferenced scalar' warning! + my $stem_symtab = *{$stem}{HASH}; + + #warn "erase($pkg) stem=$stem, leaf=$leaf"; + #warn " stem_symtab hash ".scalar(%$stem_symtab)."\n"; + # ", join(', ', %$stem_symtab),"\n"; + +# delete $stem_symtab->{$leaf}; + + my $leaf_glob = $stem_symtab->{$leaf}; + my $leaf_symtab = *{$leaf_glob}{HASH}; +# warn " leaf_symtab ", join(', ', %$leaf_symtab),"\n"; + %$leaf_symtab = (); + #delete $leaf_symtab->{'__ANON__'}; + #delete $leaf_symtab->{'foo'}; + #delete $leaf_symtab->{'main::'}; +# my $foo = undef ${"$stem\::"}{"$leaf\::"}; + + if ($action and $action eq 'DESTROY') { + delete $stem_symtab->{$leaf}; + } else { + $obj->share_from('main', $default_share); + } + 1; +} + + +sub reinit { + my $obj= shift; + $obj->erase; + $obj->share_redo; +} + +sub root { + my $obj = shift; + croak("Safe root method now read-only") if @_; + return $obj->{Root}; +} + + +sub mask { + my $obj = shift; + return $obj->{Mask} unless @_; + $obj->deny_only(@_); +} + +# v1 compatibility methods +sub trap { shift->deny(@_) } +sub untrap { shift->permit(@_) } + +sub deny { + my $obj = shift; + $obj->{Mask} |= opset(@_); +} +sub deny_only { + my $obj = shift; + $obj->{Mask} = opset(@_); +} + +sub permit { + my $obj = shift; + # XXX needs testing + $obj->{Mask} &= invert_opset opset(@_); +} +sub permit_only { + my $obj = shift; + $obj->{Mask} = invert_opset opset(@_); +} + + +sub dump_mask { + my $obj = shift; + print opset_to_hex($obj->{Mask}),"\n"; +} + + + +sub share { + my($obj, @vars) = @_; + $obj->share_from(scalar(caller), \@vars); +} + +sub share_from { + my $obj = shift; + my $pkg = shift; + my $vars = shift; + my $no_record = shift || 0; + my $root = $obj->root(); + croak("vars not an array ref") unless ref $vars eq 'ARRAY'; + no strict 'refs'; + # Check that 'from' package actually exists + croak("Package \"$pkg\" does not exist") + unless keys %{"$pkg\::"}; + my $arg; + foreach $arg (@$vars) { + # catch some $safe->share($var) errors: + my ($var, $type); + $type = $1 if ($var = $arg) =~ s/^(\W)//; + # warn "share_from $pkg $type $var"; + *{$root."::$var"} = (!$type) ? \&{$pkg."::$var"} + : ($type eq '&') ? \&{$pkg."::$var"} + : ($type eq '$') ? \${$pkg."::$var"} + : ($type eq '@') ? \@{$pkg."::$var"} + : ($type eq '%') ? \%{$pkg."::$var"} + : ($type eq '*') ? *{$pkg."::$var"} + : croak(qq(Can't share "$type$var" of unknown type)); + } + $obj->share_record($pkg, $vars) unless $no_record or !$vars; +} + +sub share_record { + my $obj = shift; + my $pkg = shift; + my $vars = shift; + my $shares = \%{$obj->{Shares} ||= {}}; + # Record shares using keys of $obj->{Shares}. See reinit. + @{$shares}{@$vars} = ($pkg) x @$vars if @$vars; +} +sub share_redo { + my $obj = shift; + my $shares = \%{$obj->{Shares} ||= {}}; + my($var, $pkg); + while(($var, $pkg) = each %$shares) { + # warn "share_redo $pkg\:: $var"; + $obj->share_from($pkg, [ $var ], 1); + } +} +sub share_forget { + delete shift->{Shares}; +} + +sub varglob { + my ($obj, $var) = @_; + no strict 'refs'; + return *{$obj->root()."::$var"}; +} + + +sub reval { + my ($obj, $expr, $strict) = @_; + my $root = $obj->{Root}; + + my $evalsub = lexless_anon_sub($root,$strict, $expr); + return Opcode::_safe_call_sv($root, $obj->{Mask}, $evalsub); +} + +sub rdo { + my ($obj, $file) = @_; + my $root = $obj->{Root}; + + my $evalsub = eval + sprintf('package %s; sub { @_ = (); do $file }', $root); + return Opcode::_safe_call_sv($root, $obj->{Mask}, $evalsub); +} + + +1; + +__END__ + +=head1 NAME + +Safe - Compile and execute code in restricted compartments + +=head1 SYNOPSIS + + use WWSafe; + + $compartment = new Safe; + + $compartment->permit(qw(time sort :browse)); + + $result = $compartment->reval($unsafe_code); + +=head1 DESCRIPTION + +The Safe extension module allows the creation of compartments +in which perl code can be evaluated. Each compartment has + +=over 8 + +=item a new namespace + +The "root" of the namespace (i.e. "main::") is changed to a +different package and code evaluated in the compartment cannot +refer to variables outside this namespace, even with run-time +glob lookups and other tricks. + +Code which is compiled outside the compartment can choose to place +variables into (or I variables with) the compartment's namespace +and only that data will be visible to code evaluated in the +compartment. + +By default, the only variables shared with compartments are the +"underscore" variables $_ and @_ (and, technically, the less frequently +used %_, the _ filehandle and so on). This is because otherwise perl +operators which default to $_ will not work and neither will the +assignment of arguments to @_ on subroutine entry. + +=item an operator mask + +Each compartment has an associated "operator mask". Recall that +perl code is compiled into an internal format before execution. +Evaluating perl code (e.g. via "eval" or "do 'file'") causes +the code to be compiled into an internal format and then, +provided there was no error in the compilation, executed. +Code evaluated in a compartment compiles subject to the +compartment's operator mask. Attempting to evaluate code in a +compartment which contains a masked operator will cause the +compilation to fail with an error. The code will not be executed. + +The default operator mask for a newly created compartment is +the ':default' optag. + +It is important that you read the L module documentation +for more information, especially for detailed definitions of opnames, +optags and opsets. + +Since it is only at the compilation stage that the operator mask +applies, controlled access to potentially unsafe operations can +be achieved by having a handle to a wrapper subroutine (written +outside the compartment) placed into the compartment. For example, + + $cpt = new Safe; + sub wrapper { + # vet arguments and perform potentially unsafe operations + } + $cpt->share('&wrapper'); + +=back + + +=head1 WARNING + +The authors make B, implied or otherwise, about the +suitability of this software for safety or security purposes. + +The authors shall not in any case be liable for special, incidental, +consequential, indirect or other similar damages arising from the use +of this software. + +Your mileage will vary. If in any doubt B. + + +=head2 RECENT CHANGES + +The interface to the Safe module has changed quite dramatically since +version 1 (as supplied with Perl5.002). Study these pages carefully if +you have code written to use Safe version 1 because you will need to +makes changes. + + +=head2 Methods in class Safe + +To create a new compartment, use + + $cpt = new Safe; + +Optional argument is (NAMESPACE), where NAMESPACE is the root namespace +to use for the compartment (defaults to "Safe::Root0", incremented for +each new compartment). + +Note that version 1.00 of the Safe module supported a second optional +parameter, MASK. That functionality has been withdrawn pending deeper +consideration. Use the permit and deny methods described below. + +The following methods can then be used on the compartment +object returned by the above constructor. The object argument +is implicit in each case. + + +=over 8 + +=item permit (OP, ...) + +Permit the listed operators to be used when compiling code in the +compartment (in I to any operators already permitted). + +You can list opcodes by names, or use a tag name; see +L. + +=item permit_only (OP, ...) + +Permit I the listed operators to be used when compiling code in +the compartment (I other operators are permitted). + +=item deny (OP, ...) + +Deny the listed operators from being used when compiling code in the +compartment (other operators may still be permitted). + +=item deny_only (OP, ...) + +Deny I the listed operators from being used when compiling code +in the compartment (I other operators will be permitted). + +=item trap (OP, ...) + +=item untrap (OP, ...) + +The trap and untrap methods are synonyms for deny and permit +respectfully. + +=item share (NAME, ...) + +This shares the variable(s) in the argument list with the compartment. +This is almost identical to exporting variables using the L +module. + +Each NAME must be the B of a non-lexical variable, typically +with the leading type identifier included. A bareword is treated as a +function name. + +Examples of legal names are '$foo' for a scalar, '@foo' for an +array, '%foo' for a hash, '&foo' or 'foo' for a subroutine and '*foo' +for a glob (i.e. all symbol table entries associated with "foo", +including scalar, array, hash, sub and filehandle). + +Each NAME is assumed to be in the calling package. See share_from +for an alternative method (which share uses). + +=item share_from (PACKAGE, ARRAYREF) + +This method is similar to share() but allows you to explicitly name the +package that symbols should be shared from. The symbol names (including +type characters) are supplied as an array reference. + + $safe->share_from('main', [ '$foo', '%bar', 'func' ]); + + +=item varglob (VARNAME) + +This returns a glob reference for the symbol table entry of VARNAME in +the package of the compartment. VARNAME must be the B of a +variable without any leading type marker. For example, + + $cpt = new Safe 'Root'; + $Root::foo = "Hello world"; + # Equivalent version which doesn't need to know $cpt's package name: + ${$cpt->varglob('foo')} = "Hello world"; + + +=item reval (STRING) + +This evaluates STRING as perl code inside the compartment. + +The code can only see the compartment's namespace (as returned by the +B method). The compartment's root package appears to be the +C package to the code inside the compartment. + +Any attempt by the code in STRING to use an operator which is not permitted +by the compartment will cause an error (at run-time of the main program +but at compile-time for the code in STRING). The error is of the form +"'%s' trapped by operation mask...". + +If an operation is trapped in this way, then the code in STRING will +not be executed. If such a trapped operation occurs or any other +compile-time or return error, then $@ is set to the error message, just +as with an eval(). + +If there is no error, then the method returns the value of the last +expression evaluated, or a return statement may be used, just as with +subroutines and B. The context (list or scalar) is determined +by the caller as usual. + +This behaviour differs from the beta distribution of the Safe extension +where earlier versions of perl made it hard to mimic the return +behaviour of the eval() command and the context was always scalar. + +Some points to note: + +If the entereval op is permitted then the code can use eval "..." to +'hide' code which might use denied ops. This is not a major problem +since when the code tries to execute the eval it will fail because the +opmask is still in effect. However this technique would allow clever, +and possibly harmful, code to 'probe' the boundaries of what is +possible. + +Any string eval which is executed by code executing in a compartment, +or by code called from code executing in a compartment, will be eval'd +in the namespace of the compartment. This is potentially a serious +problem. + +Consider a function foo() in package pkg compiled outside a compartment +but shared with it. Assume the compartment has a root package called +'Root'. If foo() contains an eval statement like eval '$foo = 1' then, +normally, $pkg::foo will be set to 1. If foo() is called from the +compartment (by whatever means) then instead of setting $pkg::foo, the +eval will actually set $Root::pkg::foo. + +This can easily be demonstrated by using a module, such as the Socket +module, which uses eval "..." as part of an AUTOLOAD function. You can +'use' the module outside the compartment and share an (autoloaded) +function with the compartment. If an autoload is triggered by code in +the compartment, or by any code anywhere that is called by any means +from the compartment, then the eval in the Socket module's AUTOLOAD +function happens in the namespace of the compartment. Any variables +created or used by the eval'd code are now under the control of +the code in the compartment. + +A similar effect applies to I runtime symbol lookups in code +called from a compartment but not compiled within it. + + + +=item rdo (FILENAME) + +This evaluates the contents of file FILENAME inside the compartment. +See above documentation on the B method for further details. + +=item root (NAMESPACE) + +This method returns the name of the package that is the root of the +compartment's namespace. + +Note that this behaviour differs from version 1.00 of the Safe module +where the root module could be used to change the namespace. That +functionality has been withdrawn pending deeper consideration. + +=item mask (MASK) + +This is a get-or-set method for the compartment's operator mask. + +With no MASK argument present, it returns the current operator mask of +the compartment. + +With the MASK argument present, it sets the operator mask for the +compartment (equivalent to calling the deny_only method). + +=back + + +=head2 Some Safety Issues + +This section is currently just an outline of some of the things code in +a compartment might do (intentionally or unintentionally) which can +have an effect outside the compartment. + +=over 8 + +=item Memory + +Consuming all (or nearly all) available memory. + +=item CPU + +Causing infinite loops etc. + +=item Snooping + +Copying private information out of your system. Even something as +simple as your user name is of value to others. Much useful information +could be gleaned from your environment variables for example. + +=item Signals + +Causing signals (especially SIGFPE and SIGALARM) to affect your process. + +Setting up a signal handler will need to be carefully considered +and controlled. What mask is in effect when a signal handler +gets called? If a user can get an imported function to get an +exception and call the user's signal handler, does that user's +restricted mask get re-instated before the handler is called? +Does an imported handler get called with its original mask or +the user's one? + +=item State Changes + +Ops such as chdir obviously effect the process as a whole and not just +the code in the compartment. Ops such as rand and srand have a similar +but more subtle effect. + +=back + +=head2 AUTHOR + +Originally designed and implemented by Malcolm Beattie. + +Reworked to use the Opcode module and other changes added by Tim Bunce. + +Currently maintained by the Perl 5 Porters, . + +=cut + From 6bbdbe732fe33273da639a8db418b9537c0e83b8 Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Tue, 27 Jul 2021 13:10:25 -0400 Subject: [PATCH 006/134] Remove CourseEnvironment and added defaults file. --- lib/PGEnvironment.pm | 58 +++++++++++++++++++++++ lib/WeBWorK/PG/IO.pm | 106 +++++++++++++++++++++++++++---------------- 2 files changed, 126 insertions(+), 38 deletions(-) create mode 100644 lib/PGEnvironment.pm diff --git a/lib/PGEnvironment.pm b/lib/PGEnvironment.pm new file mode 100644 index 0000000000..efa7a57712 --- /dev/null +++ b/lib/PGEnvironment.pm @@ -0,0 +1,58 @@ + + +use strict; +use warnings; + +package PGEnvironment; + +my $ce; +my $pg_dir; + +use Data::Dump qw/dd/; +use YAML::XS qw/LoadFile/; + + +BEGIN { + eval { + require WeBWorK::CourseEnvironment; + # WeBWorK::CourseEnvironment->import(); + $ce = WeBWorK::CourseEnvironment->new({webwork_dir=>$ENV{WEBWORK_ROOT}}); + 1; + } or do { + my $error = $@; + + $pg_dir = $ENV{PG_ROOT}; + die "The environmental variable PG_ROOT must be a directory" unless -d $pg_dir; + + # Module load failed. You could recover, try loading + # an alternate module, die with $error... + # whatever's appropriate + }; +} + +sub new { + my ($invocant, @rest) = @_; + my $class = ref($invocant) || $invocant; + + my $self = { + }; + + if (defined($ce)){ + $self->{webworkDirs} = $ce->{webworkDirs}; + $self->{externalPrograms} = $ce->{externalPrograms}; + $self->{pg_dir} = $ce->{pg_dir}; + } else { + ## load from an conf file; + $self->{pg_dir} = $ENV{PG_ROOT}; + + dd LoadFile($self->{pg_dir} . "/conf/pg_defaults.yml"); + + } + + bless $self, $class; + + return $self; +} + + +1; \ No newline at end of file diff --git a/lib/WeBWorK/PG/IO.pm b/lib/WeBWorK/PG/IO.pm index f476e277ba..7167864581 100644 --- a/lib/WeBWorK/PG/IO.pm +++ b/lib/WeBWorK/PG/IO.pm @@ -9,15 +9,20 @@ use parent qw(Exporter); use Encode qw( encode decode); use JSON qw(decode_json); use PGUtil qw(not_null); -use WeBWorK::Utils qw(path_is_subdir); -use WeBWorK::CourseEnvironment; +# use WeBWorK::Utils qw(path_is_subdir); +# use WeBWorK::CourseEnvironment; +use PGEnvironment; use utf8; #binmode(STDOUT,":encoding(UTF-8)"); #binmode(STDIN,":encoding(UTF-8)"); #binmode(INPUT,":encoding(UTF-8)"); -my $CE = new WeBWorK::CourseEnvironment({ - webwork_dir => $ENV{WEBWORK_ROOT}, - }); +# my $pg_envir = new WeBWorK::CourseEnvironment({ +# webwork_dir => $ENV{WEBWORK_ROOT}, +# }); + +my $pg_envir = new PGEnvironment(); + +dd $pg_envir; =head1 NAME @@ -61,7 +66,7 @@ BEGIN { /^2\./ and $mod = "WeBWorK::PG::IO::WW2"; /^Daemon\s*2\./ and $mod = "WeBWorK::PG::IO::Daemon2"; } - + eval "package Main; require $mod; import $mod"; # this is runtime_use die $@ if $@; } else { @@ -92,8 +97,8 @@ contains the function. =item includePGtext($string_ref, $envir_ref) -This is used in processing some of the sample CAPA files and -in creating aliases to redirect calls to duplicate problems so that +This is used in processing some of the sample CAPA files and +in creating aliases to redirect calls to duplicate problems so that they go to the original problem instead. It is called by includePGproblem. It reads and evaluates the string in the same way that the Translator evaluates the string in a PG file. @@ -109,7 +114,7 @@ sub includePGtext { # $evalString =~ s/\\/\\\\/g; # \ can't be used for escapes because of TeX conflict # $evalString =~ s/~~/\\/g; # use ~~ as escape instead, use # for comments no strict; - $evalString = eval( q! &{$main::PREPROCESS_CODE}($evalString) !); + $evalString = eval( q! &{$main::PREPROCESS_CODE}($evalString) !); $evalString = $evalString||''; # current preprocessing code passed from Translator (see Translator::initialization) my $errors = $@; @@ -142,9 +147,9 @@ sub read_whole_file { my $filePath = shift; warn "Can't read file $filePath
" unless -r $filePath; return "" unless -r $filePath; - die "File path $filePath is unsafe." + die "File path $filePath is unsafe." unless path_is_course_subdir($filePath); - + local (*INPUT); open(INPUT, "<:raw", $filePath) || die "$0: read_whole_file subroutine:
Can't read file $filePath"; local($/)=undef; @@ -160,10 +165,10 @@ sub read_whole_file { close(INPUT); \$string; } -# <:utf8 is more relaxed on input, <:encoding(UTF-8) would be better, but +# <:utf8 is more relaxed on input, <:encoding(UTF-8) would be better, but # perhaps it's not so horrible to have lax input. encoding(UTF-8) tries to use require # to import Encode, Encode::Alias::find_encoding and Safe raises an exception. -# haven't figured a way around this yet. +# haven't figured a way around this yet. =item convertPath($path) Currently a no-op. Returns $path unmodified. @@ -199,7 +204,7 @@ Uses C<&getDirDelim> to determine the path delimiter. Returns the initial segments of the of the path (i.e. the text up to the last delimiter). =cut - + sub directoryFromPath { my $path = shift; my $delim = &getDirDelim(); @@ -223,7 +228,7 @@ sub createFile { or die "Can't open $fileName: $!"; my @stat = stat TEMPCREATEFILE; close(TEMPCREATEFILE); - + # if the owner of the file is running this script (e.g. when the file is # first created) set the permissions and group correctly if ($< == $stat[4]) { @@ -262,6 +267,31 @@ sub createDirectory { } } +# This is needed for the subroutine below. It is copied from WeBWorK::Utils. +# Note: if a place for common code is ever created this should go there. + +sub path_is_subdir($$;$) { + my ($path, $dir, $allow_relative) = @_; + + unless ($path =~ /^\//) { + if ($allow_relative) { + $path = "$dir/$path"; + } else { + return 0; + } + } + + $path = canonpath($path); + $path .= "/" unless $path =~ m|/$|; + return 0 if $path =~ m#(^\.\.$|^\.\./|/\.\./|/\.\.$)#; + + $dir = canonpath($dir); + $dir .= "/" unless $dir =~ m|/$|; + return 0 unless $path =~ m|^$dir|; + + return 1; +} + =item path_is_course_subdir($path) Checks to see if the given path is a sub directory of the courses directory @@ -269,11 +299,11 @@ Checks to see if the given path is a sub directory of the courses directory =cut sub path_is_course_subdir { - return path_is_subdir(shift,$CE->{webwork_courses_dir},1); + return path_is_subdir(shift, $pg_envir->{webwork_courses_dir},1); } sub ww_tmp_dir { - return $CE->{webworkDirs}{tmp}; + return $pg_envir->{webworkDirs}{tmp}; } @@ -284,7 +314,7 @@ sub ww_tmp_dir { =cut sub curlCommand { - return $CE->{externalPrograms}{curl}; + return $pg_envir->{externalPrograms}{curl}; } =item copyCommand @@ -294,7 +324,7 @@ sub curlCommand { =cut sub copyCommand { - return $CE->{externalPrograms}{cp}; + return $pg_envir->{externalPrograms}{cp}; } =item externalCommand @@ -304,7 +334,7 @@ sub copyCommand { =cut sub externalCommand { - return $CE->{externalPrograms}{$_[0]}; + return $pg_envir->{externalPrograms}{$_[0]}; } # @@ -323,7 +353,7 @@ sub query_sage_server { warn "debug is turned on in IO.pm. "; warn "\n\nIO::query_sage_server(): SAGE CALL: ", $sagecall, "\n\n"; warn "\n\nRETURN from sage call \n", $output, "\n\n"; - warn "\n\n END SAGE CALL"; + warn "\n\n END SAGE CALL"; } # has something been returned? # $continue: HTTP/1.1 100 (Continue) @@ -335,27 +365,27 @@ sub query_sage_server { # Access-Control-Allow-Origin: * # Content-Type: application/json; charset=UTF-8 # $content: Either error message about terms of service or output from sage - # find the header + # find the header # expecting something like # HTTP/1.1 100 Continue - + # HTTP/1.1 200 OK # Date: Wed, 20 Sep 2017 14:54:03 GMT # ...... - # two blank lines + # two blank lines # content - + # or (notice that here there is no continue response) # HTTP/2 200 # date: Wed, 20 Sep 2017 16:06:03 GMT # ...... - # two blank lines + # two blank lines # content my ($continue, $header, @content) = split("\r\n\r\n",$output); #my $content = join("\r\n\r\n",@content); # handle case where there were blank lines in the content my @lines = split("\r\n\r\n", $output); - $continue=0; + $continue=0; my $header_ok =0; while (@lines) { my $header_block = shift(@lines); @@ -367,18 +397,18 @@ sub query_sage_server { last; } } - my $content = join("|||\n|||",@lines) ; #headers have been removed. + my $content = join("|||\n|||",@lines) ; #headers have been removed. #warn "output list is ", $content; # join("|||\n|||",($continue, $header, $content)); - #warn "header_ok is $header_ok"; + #warn "header_ok is $header_ok"; my $result; if ($header_ok) { #success put any extraneous splits back together $result = join("\r\n\r\n",@lines); } else { - warn "ERROR in contacting sage server. Did you accept the terms of service by + warn "ERROR in contacting sage server. Did you accept the terms of service by setting {accepted_tos=>'true'} in the askSage options?\n $content\n"; $result = undef; } - $result; + $result; } sub AskSage { @@ -428,8 +458,8 @@ END my $output = query_sage_server($python, $url, $accepted_tos, $setSeed, $webworkfunc, $debug , $curlCommand); # has something been returned? - not_null($output) or die "Unable to make a sage call to $url."; - warn "IO::askSage: We have some kind of value |$output| returned from sage" if $output and $debug; + not_null($output) or die "Unable to make a sage call to $url."; + warn "IO::askSage: We have some kind of value |$output| returned from sage" if $output and $debug; if ($output =~ /"success":\s*true/ and $debug){ warn '"success": true is present in the output'; } @@ -458,20 +488,20 @@ END warn "sage_WEBWORK_data $sage_WEBWORK_data" if $debug; if (not_null($sage_WEBWORK_data) ) { $WEBWORK_variable_non_empty = #another hack because '{}' is sometimes returned - ($sage_WEBWORK_data ne "{}" and $sage_WEBWORK_data ne "'{}'") ? + ($sage_WEBWORK_data ne "{}" and $sage_WEBWORK_data ne "'{}'") ? 1:0; } # {} indicates that WEBWORK was not used to pass or return a variable from sage. - + warn "WEBWORK variable has content" if $debug and $WEBWORK_variable_non_empty; $sage_WEBWORK_data =~s/^'//; #FIXME -- for now strip off the surrounding single quotes '. $sage_WEBWORK_data =~s/'$//; warn "sage_WEBWORK_data: ", PGUtil::pretty_print($sage_WEBWORK_data) if $debug and $WEBWORK_variable_non_empty; - if ( $WEBWORK_variable_non_empty ) { + if ( $WEBWORK_variable_non_empty ) { # have specific WEBWORK variables been defined? $ret->{webwork} = decode_json($sage_WEBWORK_data); $ret->{success}=1; - $ret->{stdout} = $decoded->{stdout}; + $ret->{stdout} = $decoded->{stdout}; } elsif (not_null( $decoded->{stdout} ) ) { # no WEBWORK content, but stdout exists # old style text output via stdout (deprecated) $ret = $decoded->{stdout}; # only standard out is returned @@ -485,7 +515,7 @@ END } else { die "IO.pm: Unknown error in asking Sage to do something: success = $success output = \n$output\n"; } - + }; # end eval{} for trapping errors in sage call if ($@) { warn "IO.pm: ERROR trapped during JSON call to sage:\n $@ "; From ddad64382e80efa4bf68428be62e9e0e667ade6e Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Tue, 27 Jul 2021 13:11:12 -0400 Subject: [PATCH 007/134] removed CourseEnvironment and added defaults file --- conf/pg_defaults.yml | 19 ++++++ lib/Chromatic.pm | 17 +++-- lib/chromatic/color.c | 152 +++++++++++++++++++++--------------------- 3 files changed, 107 insertions(+), 81 deletions(-) create mode 100644 conf/pg_defaults.yml diff --git a/conf/pg_defaults.yml b/conf/pg_defaults.yml new file mode 100644 index 0000000000..9d225399d9 --- /dev/null +++ b/conf/pg_defaults.yml @@ -0,0 +1,19 @@ +# This file is loaded if PG is loaded without a webwork2 lib +# +# The following are configuration options that are needed by PG +# and are a slimmed down list from those of WEBWORK_ROOT/conf/defaults.config + +options: + webworkDirs: + tmp: /tmp + externalPrograms: + curl: /usr/bin/curl + cp: /bin/cp + mv: /bin/mv + rm: /bin/rm + latex: /Library/TeX/texbin/latex + pdflatex: /Library/TeX/texbin/pdflatex + dvisvgm: /Library/TeX/texbin/dvisvgm + pdf2svf: /usr/local/bin/pdf2svg + convert: /usr/local/bin/convert + diff --git a/lib/Chromatic.pm b/lib/Chromatic.pm index 988e3b6282..1d5c497dae 100644 --- a/lib/Chromatic.pm +++ b/lib/Chromatic.pm @@ -10,13 +10,18 @@ our $webwork_directory = $WeBWorK::Constants::WEBWORK_DIRECTORY; #'/opt/webwork/ # or a WEBWORK_PG_DIRECTORY variable? -- perhaps not since all of those are in the defaults.config file # we would have to read that at compile time. -our $seed_ce = new WeBWorK::CourseEnvironment({ webwork_dir => $webwork_directory, courseName =>'foobar'}); -die "Can't create seed course environment for webwork in $webwork_directory" unless ref($seed_ce); -our $PGdirectory = $seed_ce->{pg_dir}; +my $pg_envir = new PGEnvironment(); + +# our $seed_ce = new WeBWorK::CourseEnvironment({ webwork_dir => $webwork_directory, courseName =>'foobar'}); +# die "Can't create seed course environment for webwork in $webwork_directory" unless ref($seed_ce); +# our $PGdirectory = $seed_ce->{pg_dir}; +my $PGdirectory = $pg_envir->{pg_dir}; # now that we have the PGdirectory we can get to work compiling color our $command = "$PGdirectory/lib/chromatic/color"; +warn $command; our $compileCommand = "/usr/bin/gcc -O3 -o $PGdirectory/lib/chromatic/color $PGdirectory/lib/chromatic/color.c"; +warn $compileCommand; unless (-x $command) { if (-w "$PGdirectory/lib/chromatic" and -r "$PGdirectory/lib/chromatic/color.c" and -x "/usr/bin/gcc") { # compile color if it is not there @@ -31,7 +36,7 @@ unless (-x $command) { warn "Can't read C file $PGdirectory/lib/chromatic/color.c" unless -r "$PGdirectory/lib/chromatic/color.c"; } } -our $tempDirectory = $seed_ce->{webworkDirs}->{DATA}; +our $tempDirectory = $pg_envir->{webworkDirs}->{DATA}; use UUID::Tiny ':std'; sub matrix_graph { @@ -58,9 +63,9 @@ sub ChromNum { my $unique_id_stub = create_uuid_as_string(UUID_V3, UUID_NS_URL, $unique_id_seed); my $fileout = "$tempDirectory/$unique_id_stub"; unless (-x $command) { - + die "Can't execute $command to calculate chromatic color"; - } + } @adj = matrix_graph($graph); $count = 0; diff --git a/lib/chromatic/color.c b/lib/chromatic/color.c index 5b762b3b88..1a53aa21c6 100644 --- a/lib/chromatic/color.c +++ b/lib/chromatic/color.c @@ -1,5 +1,5 @@ /* - * The author of this software is Michael Trick. Copyright (c) 1994 by + * The author of this software is Michael Trick. Copyright (c) 1994 by * Michael Trick. * * Permission to use, copy, modify, and distribute this software for any @@ -20,7 +20,7 @@ Graph is input in a file. First line contains the number of nodes and -edges. All following contain the node numbers (from 1 to n) incident to +edges. All following contain the node numbers (from 1 to n) incident to each edge. Sample: 4 4 @@ -79,7 +79,7 @@ int *valid,*clique; int place,done; int *order; int weight[MAX_NODE]; - + for (i=0;i max_prob) return -1; @@ -185,7 +185,7 @@ int lower,target; /* printf("Clique of size %5d found.\n",best_clique);*/ } /* printf("Greedy gave %f\n",incumb);*/ - + place = 0; for (i=0;i=BestColoring) return(current_color); if (BestColoring <=lb) return(BestColoring); - + if (i >= num_node) return(current_color); /* printf("Node %d, num_color %d\n",i,current_color);*/ - + /* Find node with maximum color_adj */ max = -1; place = -1; - for(k=0;k 0) count++; if (count!=ColorCount[k]) printf("Trouble with color count\n"); -*/ +*/ /* printf("ColorCount[%3d] = %d\n",k,ColorCount[k]);*/ if ((ColorCount[k] > max) || ((ColorCount[k]==max)&&(ColorAdj[k][0]>ColorAdj[place][0]))) { @@ -411,19 +431,19 @@ int color(i,current_color) place = k; } } - if (place==-1) + if (place==-1) { printf("Graph is disconnected. This code needs to be updated for that case.\n"); exit(1); } - + Order[i] = place; Handled[place] = TRUE; /* printf("Using node %d at level %d\n",place,i);*/ - for (j=1;j<=current_color;j++) + for (j=1;j<=current_color;j++) { - if (!ColorAdj[place][j]) + if (!ColorAdj[place][j]) { ColorClass[place] = j; AssignColor(place,j); @@ -439,7 +459,7 @@ int color(i,current_color) } } } - if (current_color+1 < BestColoring) + if (current_color+1 < BestColoring) { ColorClass[place] = current_color+1; AssignColor(place,current_color+1); @@ -448,57 +468,39 @@ int color(i,current_color) BestColoring = new_val; print_colors(); } - + RemoveColor(place,current_color+1); } Handled[place] = FALSE; return(BestColoring); } -print_colors() -{ - int i,j; - times(&buffer); - current_time = buffer.tms_utime; - - printf("Best coloring is %d at time %7.1f\n",BestColoring,(current_time-start_time)/60.0); - -/* for (i=0;i=max_prob) printf(" (not confirmed)\n"); else printf("\n"); val = color(place,place); times(&buffer); current_time=buffer.tms_utime; - + printf("Best coloring has value %d, subproblems: %d time:%7.1f\n",val,prob_count,(current_time-start_time)/60.0); - + } - - + + From b997eb75b535f89f2c421eabc16e361f6d8a6027 Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Tue, 27 Jul 2021 14:06:56 -0400 Subject: [PATCH 008/134] Add some comment/documentation and other cleanup. --- .gitignore | 3 +- .../{pg_defaults.yml => pg_defaults.yml.dist} | 1 - lib/Chromatic.pm | 114 +++---- lib/PGEnvironment.pm | 45 ++- lib/PGcore.pm | 320 +++++++++--------- lib/WeBWorK/PG/IO.pm | 3 +- 6 files changed, 259 insertions(+), 227 deletions(-) rename conf/{pg_defaults.yml => pg_defaults.yml.dist} (99%) diff --git a/.gitignore b/.gitignore index 9efdf335e0..9b77860e70 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -lib/chromatic/color \ No newline at end of file +lib/chromatic/color +conf/*.yml \ No newline at end of file diff --git a/conf/pg_defaults.yml b/conf/pg_defaults.yml.dist similarity index 99% rename from conf/pg_defaults.yml rename to conf/pg_defaults.yml.dist index 9d225399d9..e8d66400e2 100644 --- a/conf/pg_defaults.yml +++ b/conf/pg_defaults.yml.dist @@ -16,4 +16,3 @@ options: dvisvgm: /Library/TeX/texbin/dvisvgm pdf2svf: /usr/local/bin/pdf2svg convert: /usr/local/bin/convert - diff --git a/lib/Chromatic.pm b/lib/Chromatic.pm index 1d5c497dae..14228779a5 100644 --- a/lib/Chromatic.pm +++ b/lib/Chromatic.pm @@ -19,89 +19,87 @@ my $PGdirectory = $pg_envir->{pg_dir}; # now that we have the PGdirectory we can get to work compiling color our $command = "$PGdirectory/lib/chromatic/color"; -warn $command; our $compileCommand = "/usr/bin/gcc -O3 -o $PGdirectory/lib/chromatic/color $PGdirectory/lib/chromatic/color.c"; -warn $compileCommand; unless (-x $command) { if (-w "$PGdirectory/lib/chromatic" and -r "$PGdirectory/lib/chromatic/color.c" and -x "/usr/bin/gcc") { - # compile color if it is not there - system $compileCommand; - } else { - warn "ERROR: Unable to compile $PGdirectory/lib/chromatic/color.c."; - warn "The command $compileCommand failed"; - warn "Chromatic.pm and a compiled version of color.c are required for this problem"; - warn "The file color.c will need to be compiled by a systems administrator."; - warn "Can't find compiler at /usr/bin/gcc" unless -x '/usr/bin/gcc'; - warn "Can't write into directory $PGdirectory/lib/chromatic" unless -w "$PGdirectory/lib/chromatic"; - warn "Can't read C file $PGdirectory/lib/chromatic/color.c" unless -r "$PGdirectory/lib/chromatic/color.c"; - } + # compile color if it is not there + system $compileCommand; + } else { + warn "ERROR: Unable to compile $PGdirectory/lib/chromatic/color.c."; + warn "The command $compileCommand failed"; + warn "Chromatic.pm and a compiled version of color.c are required for this problem"; + warn "The file color.c will need to be compiled by a systems administrator."; + warn "Can't find compiler at /usr/bin/gcc" unless -x '/usr/bin/gcc'; + warn "Can't write into directory $PGdirectory/lib/chromatic" unless -w "$PGdirectory/lib/chromatic"; + warn "Can't read C file $PGdirectory/lib/chromatic/color.c" unless -r "$PGdirectory/lib/chromatic/color.c"; + } } our $tempDirectory = $pg_envir->{webworkDirs}->{DATA}; use UUID::Tiny ':std'; sub matrix_graph { - my ($graph) = @_; - $graph =~ s/\A\s*//; - $graph =~ s/;\s*\Z//; - - my (@m, $size, $i, $j, @r, @matrix); - @m=split /\s*[;]\s*/ , $graph; - $size=scalar @m; - @matrix=(); - for ($i=0; $i<$size ; $i++) { - @r=split /\s+/, $m[$i]; - for ($j=0; $j<$size;$j++) { - $matrix[$i][$j]=$r[$j]; - } - } - @matrix; + my ($graph) = @_; + $graph =~ s/\A\s*//; + $graph =~ s/;\s*\Z//; + + my (@m, $size, $i, $j, @r, @matrix); + @m=split /\s*[;]\s*/ , $graph; + $size=scalar @m; + @matrix=(); + for ($i=0; $i<$size ; $i++) { + @r=split /\s+/, $m[$i]; + for ($j=0; $j<$size;$j++) { + $matrix[$i][$j]=$r[$j]; + } + } + @matrix; } sub ChromNum { - my ($graph) = @_; - my ($i, $j, @adj, $val, $size, $count, @edges, $ctime, $fh, $fname); - my $unique_id_seed = time; - my $unique_id_stub = create_uuid_as_string(UUID_V3, UUID_NS_URL, $unique_id_seed); - my $fileout = "$tempDirectory/$unique_id_stub"; + my ($graph) = @_; + my ($i, $j, @adj, $val, $size, $count, @edges, $ctime, $fh, $fname); + my $unique_id_seed = time; + my $unique_id_stub = create_uuid_as_string(UUID_V3, UUID_NS_URL, $unique_id_seed); + my $fileout = "$tempDirectory/$unique_id_stub"; unless (-x $command) { die "Can't execute $command to calculate chromatic color"; } - @adj = matrix_graph($graph); - $count = 0; - $size = scalar @adj; - - for ($i = 0; $i < $size; $i++){ - for ($j = $i + 1; $j < $size; $j++){ - if ($adj[$i][$j] != 0){ - $count++; - push @edges, $i + 1, $j + 1; - } - } - } + @adj = matrix_graph($graph); + $count = 0; + $size = scalar @adj; + + for ($i = 0; $i < $size; $i++){ + for ($j = $i + 1; $j < $size; $j++){ + if ($adj[$i][$j] != 0){ + $count++; + push @edges, $i + 1, $j + 1; + } + } + } # This is not quite good enough to avoid race conditions but it'll do. - while (-e "$fileout") { - sleep 1; - } - open OUT , ">$fileout"; - print OUT "$size $count\n"; + while (-e "$fileout") { + sleep 1; + } + open OUT , ">$fileout"; + print OUT "$size $count\n"; - for ($i = 0; $i < scalar @edges; $i+=2){ - print OUT "$edges[$i] $edges[$i+1]\n"; - } - close (OUT); + for ($i = 0; $i < scalar @edges; $i+=2){ + print OUT "$edges[$i] $edges[$i+1]\n"; + } + close (OUT); # This does not work, don't know why. It's probably unsecure anyway. # unless (-e '/opt/webwork/pg/lib/chromatic/color') { # `cd /opt/webwork/pg/lib/chromatic; gcc color.c -o color`; # } - $val = qx[$command $fileout]; + $val = qx[$command $fileout]; - $val =~ /value (\d+)/g; - qx[rm $fileout]; - $1; + $val =~ /value (\d+)/g; + qx[rm $fileout]; + $1; } sub chn{ diff --git a/lib/PGEnvironment.pm b/lib/PGEnvironment.pm index efa7a57712..0b3749f50a 100644 --- a/lib/PGEnvironment.pm +++ b/lib/PGEnvironment.pm @@ -1,14 +1,44 @@ - +################################################################################ +# 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 $ +# +# 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 PGEnvironment; use strict; use warnings; -package PGEnvironment; +=pod + +=head1 PGEnvironment + +This is a substitute for the CourseEnvironment module on the webwork2 side +however is much slimmed down. It loads only configuration options need for +PG when not run in full mode. + +If the the WeBWorK::CourseEnvironment module is found the lib path, then the +necessary configuration options are loaded. + +Otherwise, defaults are loaded from PG_ROOT/conf/pg_defaults.yml + +=cut + + + my $ce; my $pg_dir; -use Data::Dump qw/dd/; use YAML::XS qw/LoadFile/; @@ -37,7 +67,7 @@ sub new { my $self = { }; - if (defined($ce)){ + if (defined($ce)) { $self->{webworkDirs} = $ce->{webworkDirs}; $self->{externalPrograms} = $ce->{externalPrograms}; $self->{pg_dir} = $ce->{pg_dir}; @@ -45,7 +75,12 @@ sub new { ## load from an conf file; $self->{pg_dir} = $ENV{PG_ROOT}; - dd LoadFile($self->{pg_dir} . "/conf/pg_defaults.yml"); + my $defaults_file = $self->{pg_dir} . "/conf/pg_defaults.yml"; + die "Cannot read the configuration file found at $defaults_file" unless -r $defaults_file; + + my $options = LoadFile($defaults_file); + $self->{webworkDirs} = $options->{webworkDirs}; + $self->{externalPrograms} = $options->{externalPrograms}; } diff --git a/lib/PGcore.pm b/lib/PGcore.pm index a8d6b348d4..8061522c59 100755 --- a/lib/PGcore.pm +++ b/lib/PGcore.pm @@ -2,12 +2,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 $ -# +# # 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 @@ -17,12 +17,12 @@ package PGcore; use strict; BEGIN { - use File::Basename qw(dirname); - my $dir = dirname(__FILE__); - do "${dir}/../VERSION"; - warn "Error loading PG VERSION file: $!" if $!; - warn "Error processing PG VERSION file: $@" if $@; - $ENV{PG_VERSION} = $PGcore::PG_VERSION || 'unknown'; + use File::Basename qw(dirname); + my $dir = dirname(__FILE__); + do "${dir}/../VERSION"; + warn "Error loading PG VERSION file: $!" if $!; + warn "Error processing PG VERSION file: $@" if $@; + $ENV{PG_VERSION} = $PGcore::PG_VERSION || 'unknown'; } our $internal_debug_messages = []; @@ -35,7 +35,7 @@ use PGloadfiles; use AnswerHash; use WeBWorK::PG::IO(); # don't important any command directly use Tie::IxHash; -use WeBWorK::Debug; +# use WeBWorK::Debug; # removing webwork2 modules. This doesn't appear to be called at all. use MIME::Base64(); use PGUtil(); use Encode qw(encode_utf8 decode_utf8); @@ -46,25 +46,25 @@ binmode(STDOUT, ":utf8"); ################################## sub not_null { - my $self = shift; - PGUtil::not_null(@_); + my $self = shift; + PGUtil::not_null(@_); } sub pretty_print { - my $self = shift; - my $input = shift; - my $displayMode = shift; - my $level = shift; + my $self = shift; + my $input = shift; + my $displayMode = shift; + my $level = shift; - if (!PGUtil::not_null($displayMode) && ref($self) eq 'PGcore') { + if (!PGUtil::not_null($displayMode) && ref($self) eq 'PGcore') { $displayMode = $self->{displayMode}; - } - warn "displayMode not defined" unless $displayMode; - PGUtil::pretty_print($input, $displayMode, $level); + } + warn "displayMode not defined" unless $displayMode; + PGUtil::pretty_print($input, $displayMode, $level); } sub new { - my $class = shift; + my $class = shift; my $envir = shift; #pointer to environment hash warn "PGcore must be called with an environment" unless ref($envir) eq 'HASH'; #warn "creating a new PGcore object"; @@ -99,8 +99,8 @@ sub new { WARNING_messages => [], DEBUG_messages => [], names_created => 0, - external_refs => {}, # record of external references - %options, # allows overrides and initialization + external_refs => {}, # record of external references + %options, # allows overrides and initialization }; bless $self, $class; tie %{$self->{PG_ANSWERS_HASH}}, "Tie::IxHash"; # creates a Hash with order @@ -116,16 +116,16 @@ sub initialize { $self->{PG_original_problem_seed} = $self->{envir}->{problemSeed}; $self->{PG_random_generator} = new PGrandom( $self->{PG_original_problem_seed}); $self->{problemSeed} = $self->{PG_original_problem_seed}; - $self->{tempDirectory} = $self->{envir}->{tempDirectory}; + $self->{tempDirectory} = $self->{envir}->{tempDirectory}; $self->{PG_problem_grader} = $self->{envir}->{PROBLEM_GRADER_TO_USE}; - $self->{PG_alias} = PGalias->new($self->{envir}, + $self->{PG_alias} = PGalias->new($self->{envir}, WARNING_messages => $self->{WARNING_messages}, - DEBUG_messages => $self->{DEBUG_messages}, + DEBUG_messages => $self->{DEBUG_messages}, ); #$self->{maketext} = WeBWorK::Localize::getLoc($self->{envir}->{language}); $self->{maketext} = $self->{envir}->{language_subroutine}; #$self->debug_message("PG alias created", $self->{PG_alias} ); - $self->{PG_loadMacros} = new PGloadfiles($self->{envir}); + $self->{PG_loadMacros} = new PGloadfiles($self->{envir}); $self->{flags} = { showpartialCorrectAnswers => 1, showHint => 1, @@ -137,8 +137,8 @@ sub initialize { # ANSWER_ENTRY_ORDER => [], # may not be needed if we ue Tie:IxHash comment => '', # implement as array? - - + + }; } @@ -290,12 +290,12 @@ sub envir { my $self = shift; my $in_key = shift; if ( $self->not_null($in_key) ) { - if (defined ($self->{envir}->{$in_key} ) ) { - $self->{envir}->{$in_key}; - } else { - warn "\$envir{$in_key} is not defined\n"; - return ''; - } + if (defined ($self->{envir}->{$in_key} ) ) { + $self->{envir}->{$in_key}; + } else { + warn "\$envir{$in_key} is not defined\n"; + return ''; + } } else { warn "

Environment

".$self->pretty_print($self->{envir}); return ''; @@ -328,37 +328,37 @@ Old name for LABELED_ANS(). DEPRECATED. # ^function NAMED_ANS # ^uses $PG_STOP_FLAG -sub LABELED_ANS{ - my $self = shift; - my @in = @_; - while (@in ) { - my $label = shift @in; - my $ans_eval = shift @in; - $self->warning_message("
Error in LABELED_ANS:|$label| - -- inputs must be references to AnswerEvaluator objects or subroutines
") +sub LABELED_ANS{ + my $self = shift; + my @in = @_; + while (@in ) { + my $label = shift @in; + my $ans_eval = shift @in; + $self->warning_message("
Error in LABELED_ANS:|$label| + -- inputs must be references to AnswerEvaluator objects or subroutines
") unless ref($ans_eval) =~ /CODE/ or ref($ans_eval) =~ /AnswerEvaluator/ ; if (ref($ans_eval) =~ /CODE/) { - # - # Create an AnswerEvaluator that calls the given CODE reference and use that for $ans_eval. - # So we always have an AnswerEvaluator from here on. - # - my $cmp = new AnswerEvaluator; - $cmp->install_evaluator(sub { - my $ans = shift; my $checker = shift; - my @args = ($ans->{student_ans}); - push(@args,ans_label=>$ans->{ans_label}) if defined($ans->{ans_label}); - $checker->(@args); # Call the original checker with the arguments that PG::Translator would have used - },$ans_eval); - $ans_eval = $cmp; + # + # Create an AnswerEvaluator that calls the given CODE reference and use that for $ans_eval. + # So we always have an AnswerEvaluator from here on. + # + my $cmp = new AnswerEvaluator; + $cmp->install_evaluator(sub { + my $ans = shift; my $checker = shift; + my @args = ($ans->{student_ans}); + push(@args,ans_label=>$ans->{ans_label}) if defined($ans->{ans_label}); + $checker->(@args); # Call the original checker with the arguments that PG::Translator would have used + },$ans_eval); + $ans_eval = $cmp; } if (defined($self->{PG_ANSWERS_HASH}->{$label}) ){ $self->{PG_ANSWERS_HASH}->{$label}->insert(ans_label => $label, ans_eval => $ans_eval, active=>$self->{PG_ACTIVE}); } else { - $self->{PG_ANSWERS_HASH}->{$label} = PGanswergroup->new($label, ans_eval => $ans_eval, active=>$self->{PG_ACTIVE}); - } - $self->{answer_eval_count}++; - } - $self->{PG_ANSWERS_HASH}; + $self->{PG_ANSWERS_HASH}->{$label} = PGanswergroup->new($label, ans_eval => $ans_eval, active=>$self->{PG_ACTIVE}); + } + $self->{answer_eval_count}++; + } + $self->{PG_ANSWERS_HASH}; } @@ -384,16 +384,16 @@ macro in L. # ^uses @PG_ANSWERS sub ANS{ - my $self = shift; - my @in = @_; - while (@in ) { - # create new label - $self->{unlabeled_answer_eval_count}++; - my $label = $self->new_label($self->{unlabeled_answer_eval_count}); - my $evaluator = shift @in; + my $self = shift; + my @in = @_; + while (@in ) { + # create new label + $self->{unlabeled_answer_eval_count}++; + my $label = $self->new_label($self->{unlabeled_answer_eval_count}); + my $evaluator = shift @in; $self->LABELED_ANS($label, $evaluator); - } - $self->{PG_ANSWERS_HASH}; + } + $self->{PG_ANSWERS_HASH}; } @@ -467,15 +467,15 @@ sub record_ans_name { # the labels in the PGanswer group and response group my $response_group = new PGresponsegroup($label,$label,$value); #$self->debug_message("adding a response group $response_group"); if (ref($self->{PG_ANSWERS_HASH}->{$label})=~/PGanswergroup/ ) { - $self->{PG_ANSWERS_HASH}->{$label}->replace(ans_label => $label, - response => $response_group, - active => $self->{PG_ACTIVE}); + $self->{PG_ANSWERS_HASH}->{$label}->replace(ans_label => $label, + response => $response_group, + active => $self->{PG_ACTIVE}); } else { - $self->{PG_ANSWERS_HASH}->{$label} = PGanswergroup->new($label, - response => $response_group, - active => $self->{PG_ACTIVE}); - } - $self->{answer_blank_count}++; + $self->{PG_ANSWERS_HASH}->{$label} = PGanswergroup->new($label, + response => $response_group, + active => $self->{PG_ACTIVE}); + } + $self->{answer_blank_count}++; $label; } @@ -483,19 +483,19 @@ sub record_array_name { # currently the same as record ans name my $self = shift; my $label = shift; my $value = shift; - my $response_group = new PGresponsegroup($label,$label,$value); + my $response_group = new PGresponsegroup($label,$label,$value); #$self->debug_message("adding a response group $response_group"); if (ref($self->{PG_ANSWERS_HASH}->{$label})=~/PGanswergroup/ ) { - $self->{PG_ANSWERS_HASH}->{$label}->replace(ans_label => $label, - response => $response_group, - active => $self->{PG_ACTIVE}); + $self->{PG_ANSWERS_HASH}->{$label}->replace(ans_label => $label, + response => $response_group, + active => $self->{PG_ACTIVE}); } else { - $self->{PG_ANSWERS_HASH}->{$label} = PGanswergroup->new($label, - response => $response_group, - active => $self->{PG_ACTIVE}); - } - $self->{answer_blank_count}++; - #$self->{PG_ANSWERS_HASH}->{$label}->{response}->clear; #why is this ? + $self->{PG_ANSWERS_HASH}->{$label} = PGanswergroup->new($label, + response => $response_group, + active => $self->{PG_ACTIVE}); + } + $self->{answer_blank_count}++; + #$self->{PG_ANSWERS_HASH}->{$label}->{response}->clear; #why is this ? $label; } @@ -507,29 +507,29 @@ sub extend_ans_group { # modifies the group type my @response_list = @_; my $answer_group = $self->{PG_ANSWERS_HASH}->{$label}; if (ref($answer_group) =~/PGanswergroup/) { - $answer_group->append_responses(@response_list); - } else { - #$self->warning_message("The answer |$label| has not yet been defined, you cannot extend it.",caller() ); - # this error message is correct but misleading for the original way - # in which matrix blanks and their response evaluators are matched up - # we should restore the warning message once the new matrix evaluation method is in place - - } - $label; + $answer_group->append_responses(@response_list); + } else { + #$self->warning_message("The answer |$label| has not yet been defined, you cannot extend it.",caller() ); + # this error message is correct but misleading for the original way + # in which matrix blanks and their response evaluators are matched up + # we should restore the warning message once the new matrix evaluation method is in place + + } + $label; } sub record_unlabeled_ans_name { my $self = shift; - $self->{unlabeled_answer_blank_count}++; - my $label = $self->new_label($self->{unlabeled_answer_blank_count}); - $self->record_ans_name($label); - $label; + $self->{unlabeled_answer_blank_count}++; + my $label = $self->new_label($self->{unlabeled_answer_blank_count}); + $self->record_ans_name($label); + $label; } sub record_unlabeled_array_name { my $self = shift; - $self->{unlabeled_answer_blank_count}++; - my $ans_label = $self->new_array_label($self->{unlabeled_answer_blank_count}); - $self->record_array_name($ans_label); + $self->{unlabeled_answer_blank_count}++; + my $ans_label = $self->new_array_label($self->{unlabeled_answer_blank_count}); + $self->record_array_name($ans_label); } sub store_persistent_data { # will store strings only (so far) my $self = shift; @@ -538,14 +538,14 @@ sub store_persistent_data { # will store strings only (so far) if (defined($self->{PERSISTENCE_HASH}->{$label}) ) { warn "can' overwrite $label in persistent data"; } else { - $self->{PERSISTENCE_HASH}->{$label} = join("",@content); #need base64 encoding? - } + $self->{PERSISTENCE_HASH}->{$label} = join("",@content); #need base64 encoding? + } $label; } sub check_answer_hash { my $self = shift; foreach my $key (keys %{ $self->{PG_ANSWERS_HASH} }) { - my $ans_eval = $self->{PG_ANSWERS_HASH}->{$key}->{ans_eval}; + my $ans_eval = $self->{PG_ANSWERS_HASH}->{$key}->{ans_eval}; unless (ref($ans_eval) =~ /CODE/ or ref($ans_eval) =~ /AnswerEvaluator/ ) { warn "The answer group labeled $key is missing an answer evaluator"; } @@ -558,18 +558,18 @@ sub check_answer_hash { sub PG_restricted_eval { my $self = shift; WeBWorK::PG::Translator::PG_restricted_eval(@_); -} +} # =head2 base64 coding -# +# # $str = decode_base64($coded_str); # $coded_str = encode_base64($str); -# +# # # Sometimes a question author needs to code or decode base64 directly -# +# # =cut -# +# sub decode_base64 ($) { my $self = shift; my $str = shift; @@ -588,13 +588,13 @@ sub encode_base64 ($;$) { ##### # This macro encodes HTML, EV3, and PGML special caracters using html codes # This should be done for any variable which contains student input and is -# printed to a screen or interpreted by EV3. +# printed to a screen or interpreted by EV3. sub encode_pg_and_html { - my $input = shift; - $input = HTML::Entities::encode_entities($input, - '<>"&\'\$\@\\\\`\\[*_\x00-\x1F\x7F'); - return $input; + my $input = shift; + $input = HTML::Entities::encode_entities($input, + '<>"&\'\$\@\\\\`\\[*_\x00-\x1F\x7F'); + return $input; } =back @@ -602,11 +602,11 @@ sub encode_pg_and_html { =head2 Message channels There are three message channels - $PG->debug_message() or in PG: DEBUG_MESSAGE() + $PG->debug_message() or in PG: DEBUG_MESSAGE() $PG->warning_message() or in PG: WARN_MESSAGE() - + They behave the same way, it is simply convention as to how they are used. - + To report the messages use: $PG->get_debug_messages @@ -615,11 +615,11 @@ To report the messages use: These are used in Problem.pm for example to report any errors. There is also - - $PG->internal_debug_message() + + $PG->internal_debug_message() $PG->get_internal_debug_message $PG->clear_internal_debug_messages(); - + There were times when things were buggy enough that only the internal_debug_message which are not saved inside the PGcore object would report. @@ -627,7 +627,7 @@ inside the PGcore object would report. sub debug_message { - my $self = shift; + my $self = shift; my @str = @_; push @{$self->{DEBUG_messages}}, "
\n", @str; } @@ -636,7 +636,7 @@ sub get_debug_messages { $self->{DEBUG_messages}; } sub warning_message { - my $self = shift; + my $self = shift; my @str = @_; unshift @str, "
------"; # mark start of each message push @{$self->{WARNING_messages}}, @str; @@ -647,7 +647,7 @@ sub get_warning_messages { } sub internal_debug_message { - my $self = shift; + my $self = shift; my @str = @_; push @{$internal_debug_messages}, @str; } @@ -755,39 +755,39 @@ sub getUniqueName { =cut sub maketext { - my $self = shift; - # uncomment this to check to see if strings are run through - # maketext. - # return 'xXx'. &{ $self->{maketext}}(@_).'xXx'; - &{ $self->{maketext}}(@_); + my $self = shift; + # uncomment this to check to see if strings are run through + # maketext. + # return 'xXx'. &{ $self->{maketext}}(@_).'xXx'; + &{ $self->{maketext}}(@_); } -sub includePGtext { +sub includePGtext { my $self = shift; - WeBWorK::PG::IO::includePGtext(@_); + WeBWorK::PG::IO::includePGtext(@_); }; -sub read_whole_problem_file { +sub read_whole_problem_file { my $self = shift; - WeBWorK::PG::IO::read_whole_problem_file(@_); + WeBWorK::PG::IO::read_whole_problem_file(@_); }; -sub convertPath { +sub convertPath { my $self = shift; - WeBWorK::PG::IO::convertPath(@_); + WeBWorK::PG::IO::convertPath(@_); }; -sub getDirDelim { +sub getDirDelim { my $self = shift; - WeBWorK::PG::IO::getDirDelim(@_); + WeBWorK::PG::IO::getDirDelim(@_); }; -sub fileFromPath { +sub fileFromPath { my $self = shift; - WeBWorK::PG::IO::fileFromPath(@_); + WeBWorK::PG::IO::fileFromPath(@_); }; -sub directoryFromPath { +sub directoryFromPath { my $self = shift; - WeBWorK::PG::IO::directoryFromPath(@_); + WeBWorK::PG::IO::directoryFromPath(@_); }; -sub createDirectory { +sub createDirectory { my $self = shift; - WeBWorK::PG::IO::createDirectory(@_); + WeBWorK::PG::IO::createDirectory(@_); }; sub AskSage { my $self = shift; @@ -796,7 +796,7 @@ sub AskSage { $options->{curlCommand} = WeBWorK::PG::IO::curlCommand(); WeBWorK::PG::IO::AskSage($python, $options); } - + sub tempDirectory { my $self = shift; return $self->{tempDirectory}; @@ -807,7 +807,7 @@ sub tempDirectory { $path = surePathToTmpFile($path); -Creates all of the intermediate directories between the tempDirectory +Creates all of the intermediate directories between the tempDirectory If $path begins with the tempDirectory path, then the path is treated as absolute. Otherwise, the path is treated as relative the the @@ -825,34 +825,34 @@ course temp directory. sub surePathToTmpFile { # constructs intermediate directories if needed beginning at ${Global::htmlDirectory}tmp/ # the input path must be either the full path, or the path relative to this tmp sub directory - + my $self = shift; my $path = shift; - my $delim = "/"; + my $delim = "/"; my $tmpDirectory = $self->tempDirectory(); unless ( -e $tmpDirectory) { # if by some unlucky chance the tmpDirectory hasn't been created, create it. - my $parentDirectory = $tmpDirectory; - $parentDirectory =~s|/$||; # remove a trailing / + my $parentDirectory = $tmpDirectory; + $parentDirectory =~s|/$||; # remove a trailing / $parentDirectory = $self->directoryFromPath($parentDirectory); - my ($perms, $groupID) = (stat $parentDirectory)[2,5]; - #warn "Creating tmp directory at $tmpDirectory, perms $perms groupID $groupID"; + my ($perms, $groupID) = (stat $parentDirectory)[2,5]; + #warn "Creating tmp directory at $tmpDirectory, perms $perms groupID $groupID"; $self->createDirectory($tmpDirectory, $perms, $groupID) or warn "Failed to create parent tmp directory at $path"; - + } # use the permissions/group on the temp directory itself as a template my ($perms, $groupID) = (stat $tmpDirectory)[2,5]; - #warn "surePathToTmpFile: directory=$tmpDirectory, perms=$perms, groupID=$groupID\n"; - + #warn "surePathToTmpFile: directory=$tmpDirectory, perms=$perms, groupID=$groupID\n"; + # if the path starts with $tmpDirectory (which is permitted but optional) remove this initial segment $path =~ s|^$tmpDirectory|| if $path =~ m|^$tmpDirectory|; - + # find the nodes on the given path - my @nodes = split("$delim",$path); - + my @nodes = split("$delim",$path); + # create new path - $path = $tmpDirectory; - + $path = $tmpDirectory; + while (@nodes>1) { $path = $path . shift (@nodes) . "/"; #convertPath($path . shift (@nodes) . "/"); @@ -862,7 +862,7 @@ sub surePathToTmpFile { } } - + $path = $path . shift(@nodes); #convertPath($path . shift(@nodes)); return $path; } diff --git a/lib/WeBWorK/PG/IO.pm b/lib/WeBWorK/PG/IO.pm index 7167864581..e018e6aa12 100644 --- a/lib/WeBWorK/PG/IO.pm +++ b/lib/WeBWorK/PG/IO.pm @@ -8,6 +8,7 @@ use warnings qw(FATAL utf8); use parent qw(Exporter); use Encode qw( encode decode); use JSON qw(decode_json); +use File::Spec::Functions qw(canonpath); use PGUtil qw(not_null); # use WeBWorK::Utils qw(path_is_subdir); # use WeBWorK::CourseEnvironment; @@ -22,8 +23,6 @@ use utf8; my $pg_envir = new PGEnvironment(); -dd $pg_envir; - =head1 NAME WeBWorK::PG::IO - Private functions used by WeBWorK::PG::Translator for file IO. From be3219dc6e514d22a648ca5f2058f8e113b6ac51 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Tue, 10 Aug 2021 07:00:31 -0400 Subject: [PATCH 009/134] Resolve problem with space after function to a power. (#598) --- lib/Parser.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Parser.pm b/lib/Parser.pm index d10af497db..d3a249932c 100644 --- a/lib/Parser.pm +++ b/lib/Parser.pm @@ -266,7 +266,7 @@ sub pushBlankOperand { # Otherwise, report the missing operand for this operator # sub Op { - my $self = shift; my $name = shift; + my $self = shift; my $name = shift; my $NAME = $name; my $ref = $self->{ref} = shift; my $context = $self->{context}; my $op; ($name,$op) = $context->operators->resolve($name); @@ -283,10 +283,10 @@ sub Op { $self->pushOperand($self->Item("UOP")->new($self,$name,$top->{value},$ref)); } else { my $def = $context->operators->resolveDef(' '); - $name = $def->{string} if defined($name) and ($name eq ' ' or $name eq $def->{space}); + $name = $def->{string} if defined($NAME) and ($NAME eq ' ' or $NAME eq $def->{space}); $self->pushOperator($name,$op->{precedence}); } - } elsif (($ref && $name ne ' ') || $self->state ne 'fn') {$self->Op($name,$ref)} + } elsif (($ref && $NAME ne ' ') || $self->state ne 'fn') {$self->Op($NAME,$ref)} } } else { ($name,$op) = $context->operators->resolve('u'.$name) From cbf4329d9a4eaf07f6cefa5ed74fe5a28d1c0a3f Mon Sep 17 00:00:00 2001 From: pschan-gh <31397850+pschan-gh@users.noreply.github.com> Date: Tue, 31 Aug 2021 21:10:23 +0800 Subject: [PATCH 010/134] initial commit --- lib/DragNDrop.pm | 172 +++++++++++ macros/draggableProof.pl | 589 ++++++++++++++++++++++--------------- macros/draggableSubsets.pl | 283 ++++++++++++++++++ 3 files changed, 799 insertions(+), 245 deletions(-) create mode 100644 lib/DragNDrop.pm create mode 100644 macros/draggableSubsets.pl diff --git a/lib/DragNDrop.pm b/lib/DragNDrop.pm new file mode 100644 index 0000000000..1208f49883 --- /dev/null +++ b/lib/DragNDrop.pm @@ -0,0 +1,172 @@ +=head1 NAME +DragNDrop.pm - Drag-N-Drop Module + +=head1 DESCRIPTION +DragNDrop.pm is a backend Perl module which facilitates the implementation of +'Drag-And-Drop' in WeBWorK problems. It is meant to be used by other perl macros +such as draggableProof.pl and draggableSubsets.pl + +=head1 TERMINOLOGY +An HTML element into or out of which other elements may be dragged will be called a "bucket". +An HTML element which houses a collection of buckets will be called a "bucket pool". + +=head1 USAGE +Each macro aiming to implement drag-n-drop features must call at its initialization: +ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); +ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1); +ADD_CSS_FILE("js/apps/DragNDrop/dragndrop.css", 0); +ADD_JS_FILE("js/apps/DragNDrop/dragndrop.js", 0, { defer => undef }); + +To initialize a bucket pool, do: + +my $bucket_pool = new DragNDrop($answerInputId, $aggregateList); + +$answerInputId is a unique identifier for the bucket_pool, it is recommended that +it be generated with NEW_HIDDEN_ANS_NAME. + +$aggregateList is a reference to an array of all "statements" intended to be draggable. +e.g. $aggregateList = ['socrates is a man', 'all men are mortal', 'therefore socrates is mortal']. +It is imperative that square brackets be used. + +############################################################################## +OPTIONAL: DragNDrop($answerInputId, $aggregateList, AllowNewBuckets => 1); +allows student to create new buckets by clicking on a button. +############################################################################## + +To add a bucket to an existing pool $bucket_pool, do: +$bucket_pool->addBucket($indices); + +$indices is the reference to the array of indices corresponding to the statements in $aggregateList to be pre-included in the bucket. +For example, if the $aggregateList is ['Socrates is a man', 'all men are mortal', 'therefore Socrates is mortal'], and the bucket consists of { 'Socrates is a man', 'therefore Socrates is mortal' }, then $indices = [0, 2]. + +An empty array reference, e.g. $bucket_pool->addBucket([]), gives an empty bucket. + +############################################################################### +OPTIONAL: $bucket_pool->addBucket($indices, label => 'Barrel', removable => 1) +puts the label 'Barrel' at the top of the bucket. +With the removable option set to 1, the bucket may be removed by the student via the click of a "Remove" button at the bottom of the bucket. +(The first created bucket may never be removed.) +############################################################################### + +To output the bucket pool to HTML, call: +$bucket_pool->HTML + +To output the bucket pool to LaTeX, call: +$bucket_pool->TeX + +=head1 EXAMPLES +See draggableProof.pl and draggableSubsets.pl +=cut +############################################################################### + +use strict; +use warnings; + +package DragNDrop; + +sub new { + my $self = shift; + my $class = ref($self) || $self; + + my $answerInputId = shift; # 'id' of html tag corresponding to the answer blank. Must be unique to each pool of DragNDrop buckets + my $aggregateList = shift; # array of all statements provided + my $defaultBuckets = shift; # instructor-provided default buckets with pre-included statements encoded by the array of corresponding statement indices + my %options = ( + AllowNewBuckets => 0, + @_ + ); + + $self = bless { + answerInputId => $answerInputId, + bucketList => [], + aggregateList => $aggregateList, + defaultBuckets => $defaultBuckets, + %options, + }, $class; + + return $self; +} + +sub addBucket { + my $self = shift; + + my $indices = shift; + + my %options = ( + label => "", + removable => 0, + @_ + ); + + my $bucket = { + indices => $indices, + list => [ map { $self->{aggregateList}->[$_] } @$indices ], + bucket_id => scalar @{ $self->{bucketList} }, + label => $options{label}, + removable => $options{removable}, + }; + push(@{$self->{bucketList}}, $bucket); + +} + +sub HTML { + my $self = shift; + + my $out = ''; + $out .= "
"; + + # buckets from instructor-defined default settings + for (my $i = 0; $i < @{$self->{defaultBuckets}}; $i++) { + my $defaultBucket = $self->{defaultBuckets}->[$i]; + $out .= ""; + } + + # buckets from past answers + for my $bucket ( @{$self->{bucketList}} ) { + $out .= ""; + } + $out .= '
'; + $out .= "
reset"; + if ($self->{AllowNewBuckets} == 1) { + $out .= "add bucket"; + } + $out .= "
"; + + return $out; +} + +sub TeX { + my $self = shift; + + my $out = ""; + + # default buckets; + for (my $i = 0; $i < @{ $self->{defaultBuckets} }; $i++) { + $out .= "\n"; + my $defaultBucket = $self->{defaultBuckets}->[$i]; + if ( @{$defaultBucket->{indices}} > 0 ) { + $out .= "\n\\hrule\n\\begin{itemize}"; + for my $j ( @{$defaultBucket->{indices}} ) { + $out .= "\n\\item[$j.]\n $self->{aggregateList}->[$j]"; + } + $out .= "\n\\end{itemize}"; + } + $out .= "\n\\hrule\n"; + } + return $out; +} +1; \ No newline at end of file diff --git a/macros/draggableProof.pl b/macros/draggableProof.pl index b2adea3d51..0b05f01e1d 100644 --- a/macros/draggableProof.pl +++ b/macros/draggableProof.pl @@ -1,284 +1,383 @@ -# Done: show possible choices in TeX mode -# To do: display student answers and correct answers in TeX mode properly. -# To do: put jquery.nestable.js in a universal spot on every webwork server. +################################################################ +=head1 NAME +draggableProof.pl + +=head1 DESCRIPTION -loadMacros("PGchoicemacros.pl"); +=head1 TERMINOLOGY +An HTML element into or out of which other elements may be dragged will be called a "bucket". +An HTML element which houses a collection of buckets will be called a "bucket pool". -sub _draggableProof_init { - PG_restricted_eval("sub DraggableProof {new draggableProof(\@_)}"); - -# post global javascript -# using sources in cdnjs.cloudflare.com - -main::POST_HEADER_TEXT(main::MODES(TeX=>"", HTML=>< - - - - -END_SCRIPTS -} # end _draggableProof_init +=head1 USAGE +To initialize a DraggableProof bucket pool in a .pg problem, insert the line: -package draggableProof; +$draggable = DraggableProof($statements, $extra, Options1 => ..., Options2 => ...); -my $n = 0; # number of nestable lists so far +before BEGIN_TEXT. -sub new { - $n++; - my $self = shift; my $class = ref($self) || $self; - my $proof = shift || []; my $extra = shift || []; - my %options = ( - SourceLabel => "Choose from these sentences:", - TargetLabel => "Your Proof:", - id => "P$n", - @_ - ); - my $lines = [@$proof,@$extra]; - my $numNeeded = scalar(@$proof); - my $numProvided = scalar(@$lines); - my @order = main::shuffle($numProvided); - my @unorder = main::invert(@order); - $self = bless { - lines => $lines, - numNeeded => $numNeeded, numProvided => $numProvided, - order => \@order, unordered => \@unorder, - proof => "$options{id}-".join(",$options{id}-",@unorder[0..$numNeeded-1]), - %options - }, $class; - $self->AnswerRule; - $self->ScriptAndStyles; - $self->GetAnswer; - return $self; -} +Then, call: -sub lines {my $self = shift; return @{$self->{lines}}} -sub numNeeded {(shift)->{numNeeded}} -sub numProvided {(shift)->{numProvided}} -sub order {my $self = shift; return @{$self->{order}}} -sub unorder {my $self = shift; return @{$self->{unorder}}} +$draggable->Print -# In principle, the styles (for example, color and size of the tiles) -# could be customized for each draggableProof instance. This is why -# each instance receives its own CSS container class. +within the BEGIN_TEXT / END_TEXT environment; -sub ScriptAndStyles { - my $self = shift; my $id = $self->{id}; - - main::POST_HEADER_TEXT(main::MODES(TeX=>"", HTML=><<" SCRIPT_AND_STYLE")); - - - - SCRIPT_AND_STYLE -} +$statements, e.g. ["Socrates is a man.", "Socrates is mortal.", ...], is an array reference to the list of statements used in the correct proof. It is imperative that square brackets be used. -sub AnswerRule { - my $self = shift; - my $rule = main::ans_rule(1); - $self->{tgtAns} = ""; $self->{tgtAns} = $1 if $rule =~ m/id="(.*?)"/; - $self->{srcAns} = $self->{tgtAns}."-src"; - main::RECORD_FORM_LABEL($self->{srcAns}); # use this for release 2.13 and comment out for develop - #$main::PG->store_persistent_data; # uncomment this for develop and releases beyond 2.13 - my $ext = main::NAMED_ANS_RULE_EXTENSION($self->{srcAns},1,answer_group_name=>$self->{srcAns}.'-src'); - main::TEXT( main::MODES(TeX=>"", HTML=>'')); -} +$extra, e.g. ["Roses are red."], is an array reference to the list statements extraneous to the proof. If there are no extraneous statements, use the empty array reference []. -sub GetAnswer { - my $self = shift; my $previous; +By default, the score of the student answer is 100% if the draggable statements are placed in the exact same order as in the array referenced by $statements, with no inclusion of any statement from $extra. The score is 0% otherwise. - # Retrieve the previous state of the right column. - $previous = $main::inputs_ref->{$self->{tgtAns}} || ""; - $previous =~ s/$self->{id}-//g; $self->{previousTarget} = [split(/,/,$previous)]; +Available Options: +NumBuckets => 1 or 2 +SourceLabel => +TargetLabel => +Levenshtein => 0 or 1 +DamerauLevenshtein => 0 or 1 +InferenceMatrix => +IrrelevancePenalty => - # Calculate the complement of the right column. - my %prevTarget = map {$_ => 1} @{$self->{previousTarget}}; - my @diff = grep {not $prevTarget{$_}} (0..$self->{numProvided}-1); +Their usage is explained in the example below. - # If the previous state of the left column has been saved, use it. (This ensures that the tiles - # in the left column are kept in the same order that the user had arranged them). If it has not - # been saved, use the complement of the right column. - $previous = $main::inputs_ref->{$self->{srcAns}} || "$self->{id}-".join(",$self->{id}-",@diff); - $previous =~ s/$self->{id}-//g; $self->{previousSource} = [split(/,/,$previous)]; -} +=head1 EXAMPLE +DOCUMENT(); +loadMacros( + "PGstandard.pl", + "MathObjects.pl", + "draggableProof.pl" +); -sub Print { - my $self = shift; +TEXT(beginproblem()); - if ($main::displayMode ne "TeX") { # HTML mode +$statements = [ +"All men are mortal.", #0 +"Socrates is a man.", #1 +"Socrates is mortal." #2 +]; - return join("\n", - '
', - $self->Source, - $self->Target, - '
', - '
', - ); +$extra = [ +"Some animals are men.", +"Beauty is immortal.", +"Not all animals are men." +]; - } else { # TeX mode +$draggable = DraggableProof( +$statements, +$extra, +NumBuckets => 2, # either 1 or 2. +SourceLabel => "Axioms", # label of first bucket if NumBuckets = 2. +TargetLabel => "Reasoning", # label of second bucket if NumBuckets = 2, of the only bucket if NumBuckets = 1. +################################################################ +# Levenshtein => 1, +# If equal to 1, scoring is determined by the Levenshtein edit distance between student answer and correct answer. +################################################################ +# DamerauLevenshtein => 1, +# If equal to 1, scoring is determined by the Damerau-Levenshtein distance between student answer and correct answer. +# A pair of transposed adjacent statements is counted as two mistakes under Levenshtein scoring, but as one mistake under Damerau-Levenshtein scoring. +################################################################ +InferenceMatrix => [ +[0, 0, 1], +[0, 0, 1], +[0, 0, 0] +], +# (i, j)-entry is nonzero <=> statement i implies statement j. The score of each corresponding inference is weighted according to the value of the matrix entry. +################################################################ +IrrelevancePenalty => 1 # This option is processed only if the InferenceMatrix option is set. Penalty for each extraneous statement in the student answer is divided by the total number of inference points (i.e. sum of all entries in the InferenceMatrix). Default value = 1. +); - return join("\n", - $self->Source, - $self->Target, - ); +Context()->texStrings; - } +BEGIN_TEXT -} +Show that Socrates is mortal by dragging the relevant $BBOLD Axioms $EBOLD +into the $BBOLD Reasoning $EBOLD box in an appropriate order. -sub Source { - my $self = shift; - return $self->Bucket("source",$self->{srcAns},$self->{SourceLabel},$self->{previousSource}); -} +$PAR -sub Target { - my $self = shift; - return $self->Bucket("target",$self->{tgtAns},$self->{TargetLabel},$self->{previousTarget}); -} +\{ $draggable->Print \} + +END_TEXT +Context()->normalStrings; -sub Bucket { - my $self = shift; my $id = $self->{id}; - my ($name,$ans,$label,$previous) = @_; +ANS($draggable->cmp); - if ($main::displayMode ne "TeX") { # HTML mode +ENDDOCUMENT(); +=cut +################################################################ - my @lines = (); - push(@lines, '
', - '
'.$label.'
', - '
' +loadMacros( +"PGchoicemacros.pl", +"MathObjects.pl", +); + +sub _draggableProof_init { + ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); + ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1); + ADD_CSS_FILE("js/apps/DragNDrop/dragndrop.css", 0); + ADD_JS_FILE("js/apps/DragNDrop/dragndrop.js", 0, { defer => undef }); + PG_restricted_eval("sub DraggableProof {new draggableProof(\@_)}"); +} + +package draggableProof; + +sub new { + my $self = shift; + my $class = ref($self) || $self; + + # user arguments + my $proof = shift; + my $extra = shift; + my %options = ( + SourceLabel => "Choose from these sentences:", + TargetLabel => "Your Proof:", + NumBuckets => 2, + Levenshtein => 0, + DamerauLevenshtein => 0, + InferenceMatrix => [], + IrrelevancePenalty => 1, + @_ ); - if (scalar @{$previous} > 0) { - push(@lines, '
    '); - foreach my $i (@{$previous}) { - push(@lines, '
  1. '.$self->{lines}[$self->{order}[$i]].'
  2. '); - } - push(@lines, '
'); + # end user arguments + + my $lines = [ @$proof, @$extra ]; + my $numNeeded = scalar(@$proof); + my $numProvided = scalar(@$lines); + my @order = main::shuffle($numProvided); + my @unorder = main::invert(@order); + my $shuffledLines = [ map {$lines->[$_]} @order ]; + + my $answerInputId = main::NEW_ANS_NAME() unless $self->{answerInputId}; + my $ans_rule = main::NAMED_HIDDEN_ANS_RULE($answerInputId); + + my $dnd; + if ($options{NumBuckets} == 2) { + $dnd = new DragNDrop($answerInputId, $shuffledLines, + [ + { + indices => [0..$numProvided-1], + label => $options{'SourceLabel'} + }, + { + indices => [], + label => $options{'TargetLabel'} + } + ], + AllowNewBuckets => 0); + } elsif($options{NumBuckets} == 1) { + $dnd = new DragNDrop($answerInputId, $shuffledLines, + [ + { + indices => [0..$numProvided-1], + label => $options{'TargetLabel'} + } + ], + AllowNewBuckets => 0); } - push(@lines, - '
', - '
' - ); - return join("\n",@lines); + + my $proof = $options{NumBuckets} == 2 ? main::List( + main::List(@unorder[$numNeeded .. $numProvided - 1]), + main::List(@unorder[0..$numNeeded-1]) + ) : main::List('('.join(',', @unorder[0..$numNeeded-1]).')'); + + my $extra = main::Set(@unorder[$numNeeded .. $numProvided - 1]); + + my $InferenceMatrix = $options{InferenceMatrix}; + + $self = bless { + lines => $lines, + shuffledLines => $shuffledLines, + numNeeded => $numNeeded, + numProvided => $numProvided, + order => \@order, + unorder => \@unorder, + proof => $proof, + extra => $extra, + answerInputId => $answerInputId, + dnd => $dnd, + ans_rule => $ans_rule, + inferenceMatrix => $InferenceMatrix, + %options, + }, $class; + + my $previous = $main::inputs_ref->{$answerInputId} || ''; + + if ($previous eq "") { + if ($self->{NumBuckets} == 2) { + $dnd->addBucket([0..$numProvided-1], label => $options{'SourceLabel'}); + $dnd->addBucket([], label => $options{'TargetLabel'}); + } elsif ($self->{NumBuckets} == 1) { + $dnd->addBucket([0..$numProvided-1], label => $options{'TargetLabel'}); + } + } else { + my @matches = ( $previous =~ /(\([^\(\)]*\)|-?\d+)/g ); + if ($self->{NumBuckets} == 2) { + my $indices1 = [ split(',', @matches[0] =~ s/\(|\)//gr) ]; + $dnd->addBucket($indices1->[0] != -1 ? $indices1 : [], label => $options{'SourceLabel'}); + my $indices2 = [ split(',', @matches[1] =~ s/\(|\)//gr) ]; + $dnd->addBucket($indices2->[0] != -1 ? $indices2 : [], label => $options{'TargetLabel'}); + } else { + my $indices1 = [ split(',', @matches[0] =~ s/\(|\)//gr) ]; + $dnd->addBucket($indices1->[0] != -1 ? $indices1 : [], label => $options{'TargetLabel'}); + } + } + + return $self; +} - } else { # TeX mode +sub Levenshtein { + my @ar1 = split /$_[2]/, $_[0]; + my @ar2 = split /$_[2]/, $_[1]; + + my @dist = ([0 .. @ar2]); + $dist[$_][0] = $_ for (1 .. @ar1); + + for my $i (0 .. $#ar1) { + for my $j (0 .. $#ar2) { + $dist[$i+1][$j+1] = main::min($dist[$i][$j+1] + 1, $dist[$i+1][$j] + 1, + $dist[$i][$j] + ($ar1[$i] ne $ar2[$j]) ); + } + } + $dist[-1][-1]; +} - if (@{$previous}) { # array is nonempty - my @lines = ('\\begin{itemize}'); - foreach my $i (@{$previous}) { - push(@lines,'\\item '.$self->{lines}[$self->{order}[$i]] ) - } - push(@lines,'\\end{itemize}'); - return join("\n",@lines); - } else { - return ''; +sub DamerauLevenshtein { + # Damerau–Levenshtein distance with adjacent transpositions + # https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance + + my $discourse1 = shift; + my $discourse2 = shift; + my $delimiter = shift; + my $numProvided = shift; + + my @ar1 = split /$delimiter/, $discourse1; + my @ar2 = split /$delimiter/, $discourse2; + + my @da = (0) x $numProvided; + my @d = (); + + my $maxdist = @ar1 + @ar2; + for my $i (1 .. @ar1 + 1) { + push(@d, [ (0) x (@ar2 + 2) ] ); + $d[$i][0] = $maxdist; + $d[$i][1] = $i - 1; + } + for my $j (1 .. @ar2 + 1) { + $d[0][$j] = $maxdist; + $d[1][$j] = $j - 1; } - } + my $db; + for my $i (2 .. @ar1 + 1) { + $db = 0; + my $k, $l, $cost; + for my $j (2 .. @ar2 + 1) { + $k = $da[$ar2[$j - 2]]; + $l = $db; + if ($ar1[$i - 2] == $ar2[$j - 2]) { + $cost = 0; + $db = $j; + } else { + $cost = 1; + } + $d[$i][$j] = main::min($d[$i-1][$j-1] + $cost, + $d[$i][$j-1] + 1, + $d[$i-1][$j] + 1, + $d[$k-1][$l-1] + ($i - $k - 1) + 1 + ($j - $l - 1)); + } + $da[$ar1[$i - 2]] = $i; + } + $d[-1][-1]; +} +sub Print { + my $self = shift; + + my $ans_rule = $self->{ans_rule}; + + if ($main::displayMode ne "TeX") { + # HTML mode + return join("\n", + '
', + $ans_rule, + $self->{dnd}->HTML, + '
', + '
', + ); + } else { + # TeX mode + return $self->{dnd}->TeX; + } } sub cmp { - my $self = shift; - return main::str_cmp($self->{proof})->withPreFilter("erase")->withPostFilter(sub {$self->filter(@_)}); + my $self = shift; + return $self->{proof}->cmp(ordered => 1, removeParens => 1)->withPreFilter("erase")->withPostFilter(sub {$self->filter(@_)}); } sub filter { - my $self = shift; my $ans = shift; - my @line = $self->lines; my @order = $self->order; - my $correct = $ans->{correct_ans}; $correct =~ s/$self->{id}-//g; - my $student = $ans->{student_ans}; $student =~ s/$self->{id}-//g; - my @correct = @line[map {@order[$_]} split(/,/,$correct)]; - my @student = @line[map {@order[$_]} split(/,/,$student)]; - $ans->{preview_latex_string} = "\\begin{array}{l}\\text{".join("}\\\\\\text{",@student)."}\\end{array}"; - $ans->{student_ans} = "(see preview)"; - $ans->{correct_ans_latex_string} = "\\begin{array}{l}\\text{".join("}\\\\\\text{",@correct)."}\\end{array}"; - $ans->{correct_ans} = join("
",@correct); - return $ans; + my $self = shift; + my $anshash = shift; + + my @lines = @{$self->{lines}}; + my @order = @{$self->{order}}; + + my $actualAnswer; + my $correctProcessed; + + if ($self->{NumBuckets} == 1) { + $actualAnswer = $anshash->{student_ans} =~ s/\(|\)|\s*//gr; + $correctProcessed = $anshash->{correct_ans} =~ s/\(|\)|\s*//gr; + } elsif ($self->{NumBuckets} == 2) { + my @matches = ( $anshash->{student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); + $actualAnswer = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; + @matches = ( $anshash->{correct_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); + $correctProcessed = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; + } + + my $correct_ans = main::List($correctProcessed); + + if ($self->{Levenshtein} == 1) { + $anshash->{score} = 1 - main::min(1, Levenshtein($correct_ans, $actualAnswer, ',')/$self->{numNeeded}); + } elsif ($self->{DamerauLevenshtein} == 1) { + $anshash->{score} = 1 - main::min(1, DamerauLevenshtein($correct_ans, $actualAnswer, ',', $self->{numProvided})/$self->{numNeeded}); + } elsif (@{ $self->{inferenceMatrix} } != 0) { + my @unshuffledStudentIndices = map { $self->{order}[$_]} split(',', $actualAnswer); + my @inferenceMatrix = @{ $self->{inferenceMatrix} }; + my $inferenceScore = 0; + for (my $j = 0; $j < @unshuffledStudentIndices; $j++ ) { + if ($unshuffledStudentIndices[$j] < $self->{numNeeded}) { + for (my $i = $j - 1; $i >= 0; $i--) { + if ($unshuffledStudentIndices[$i] < $self->{numNeeded}) { + $inferenceScore += $inferenceMatrix[$unshuffledStudentIndices[$i]][$unshuffledStudentIndices[$j]]; + } + } + } + } + my $total = 0; + for my $row ( @inferenceMatrix ) { + foreach (@$row) { + $total += $_; + } + } + $anshash->{score} = $inferenceScore / $total; + + my %invoked = map { $_ => 1 } split(',', $actualAnswer); + foreach ( split(',', $self->{extra}->string =~ s/{|}|\s*//gr ) ) { + if ( exists($invoked{$_}) ) { + $anshash->{score} = main::max(0, $anshash->{score} - ($self->{IrrelevancePenalty}/$total)); + } + } + } else { + $anshash->{score} = $correct_ans eq main::List($actualAnswer) ? 1 : 0; + } + + + my @correct = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $correct_ans); + my @student = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $actualAnswer); + + $anshash->{non_tex_preview} = 1; + $anshash->{student_ans} = "(see preview)"; + $anshash->{preview_latex_string} = "
  • ".join("
  • ",@student)."
"; + $anshash->{correct_ans_latex_string} = "
  • ".join("
  • ",@correct)."
"; + + return $anshash; } - 1; diff --git a/macros/draggableSubsets.pl b/macros/draggableSubsets.pl new file mode 100644 index 0000000000..ab0d90fa4f --- /dev/null +++ b/macros/draggableSubsets.pl @@ -0,0 +1,283 @@ +################################################################ +=head1 NAME +draggableSubsets.pl + +=head1 DESCRIPTION +draggableSubsets.pl helps the instructor create a drag-and-drop environment in which a pre-specified set of elements may be dragged to different "buckets", effectively partitioning the original set into subsets. + +=head1 TERMINOLOGY +An HTML element into or out of which other elements may be dragged will be called a "bucket". +An HTML element which houses a collection of buckets will be called a "bucket pool". + +=head1 USAGE +To initialize a DraggableSubset bucket pool in a .pg problem, insert the line: + +$draggable = DraggableSubsets($full_set, $ans, Options1 => ..., Options2 => ...); + +before BEGIN_TEXT. + +Then, call: + +$draggable->Print + +within the BEGIN_TEXT / END_TEXT environment; + +$full_set, e.g. ["statement1", "statement2", ...], is an array reference to the list of elements, given as strings, in the original full set. +$ans, e.g. [[1, 2, 3], [4, 5], ...], is an array reference to the list of array references corresponding to the correct answer which is a set of subsets. Each subset is specified via the indices of the elements according to their positions in $full_set, with the first element having index 0. + +Available Options: +DefaultSubsets => +OrderedSubsets => 0 or 1 +AllowNewBuckets => 0 or 1 + +Their usage is explained in the example below. + +=head1 EXAMPLE +DOCUMENT(); +loadMacros( + "PGstandard.pl", + "MathObjects.pl", + "draggableSubsets.pl", +); + +TEXT(beginproblem()); +$D3 = [ +"\(e\)", #0 +"\(r\)", #1 +"\(r^2\)", #2 +"\(s\)", #3 +"\(sr\)", #4 +"\(sr^2\)", #5 +]; + +$subgroup = "e, s"; + +$subsets = [ +[0, 3], +[1, 4], +[2, 5] +]; + +$draggable = DraggableSubsets( +$D3, # full set. Square brackets must be used. +$subsets, # reference to array of arrays of indices, corresponding to correct set of subsets. Square brackets must be used. +DefaultSubsets => [ # default instructor-provided subsets. Default value = []. +{ + label => 'coset 1', # label of the bucket. + indices => [ 1, 3, 4, 5 ], # specifies pre-included elements in the bucket via their indices. + removable => 0 # specifies whether student may remove bucket. +}, +{ + label => 'coset 2', + indices => [ 0 ], + removable => 1 +}, +{ + label => 'coset 3', + indices => [ 2 ], + removable => 1 +} +], +# OrderedSubsets => 0, # OrderedSubsets => 0 means order of subsets does not matter. 1 means otherwise. (The order of elements within each subset never matters.) Default value = 0. +# AllowNewBuckets => 1, # AllowNewBuckets => 0 means no new buckets may be added by student. 1 means otherwise. Default value = 1. +); + +Context()->texStrings; + +BEGIN_TEXT + +Let \[ +G=D_3=\lbrace e,r,r^2, s,sr,sr^2 \rbrace +\] +be the Dihedral group of order \(6\), where \(r\) is counter-clockwise rotation by \(2\pi/3\), and \(s\) is the reflection across the \(x\)-axis. + +Partition \(G=D_3\) into $BBOLD right $EBOLD cosets of the subgroup +\(H=\lbrace $subgroup \rbrace\). Give your result by dragging the following elements into separate buckets, each corresponding to a coset. + +$PAR +\{ $draggable->Print \} + +END_TEXT +Context()->normalStrings; + +# Answer Evaluation + +ANS($draggable->cmp); + +ENDDOCUMENT(); +=cut +################################################################ + +loadMacros( +"PGchoicemacros.pl", +"MathObjects.pl", +); + +sub _draggableSubsets_init { + ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); + ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1); + ADD_CSS_FILE("js/apps/DragNDrop/dragndrop.css", 0); + ADD_JS_FILE("js/apps/DragNDrop/dragndrop.js", 0, { defer => undef }); + PG_restricted_eval("sub DraggableSubsets {new draggableSubsets(\@_)}"); +} + +package draggableSubsets; + +sub new { + my $self = shift; + my $class = ref($self) || $self; + + # user arguments + my $set = shift; + my $subsets = shift; + my %options = ( + DefaultSubsets => [], + OrderedSubsets => 0, + AllowNewBuckets => 1, + @_ + ); + # end user arguments + + my $numProvided = scalar(@$set); + my @order = main::shuffle($numProvided); + my @unorder = main::invert(@order); + + my $shuffledSet = [ map {$set->[$_]} @order ]; + + my $defaultBuckets = $options{DefaultSubsets}; + my $defaultShuffledBuckets = []; + if (@$defaultBuckets) { + for my $defaultBucket (@$defaultBuckets) { + my $shuffledIndices = [ map {$unorder[$_]} @{ $defaultBucket->{indices} } ]; + my $default_shuffled_bucket = { + label => $defaultBucket->{label}, + indices => $shuffledIndices, + removable => $defaultBucket->{removable}, + }; + push(@$defaultShuffledBuckets, $default_shuffled_bucket); + } + } else { + push(@$defaultShuffledBuckets, [ { + label => '', + indices => [ 0..$numProvided-1 ] + } ]); + } + + my $answerInputId = main::NEW_ANS_NAME() unless $self->{answerInputId}; + my $ans_rule = main::NAMED_HIDDEN_ANS_RULE($answerInputId); + my $dnd = new DragNDrop( + $answerInputId, + $shuffledSet, + $defaultShuffledBuckets, + AllowNewBuckets => $options{AllowNewBuckets}, + ); + + my $previous = $main::inputs_ref->{$answerInputId} || ''; + + if ($previous eq '') { + for my $defaultBucket ( @$defaultShuffledBuckets ) { + $dnd->addBucket($defaultBucket->{indices}, label => $defaultBucket->{label}); + } + } else { + my @matches = ( $previous =~ /(\([^\(\)]*\)|-?\d+)+/g ); + for(my $i = 0; $i < @matches; $i++) { + my $match = @matches[$i] =~ s/\(|\)//gr; + my $indices = [ split(',', $match) ]; + my $label = $i < @$defaultShuffledBuckets ? $defaultShuffledBuckets->[$i]->{label} : ''; + my $removable = $i < @$defaultShuffledBuckets ? $defaultShuffledBuckets->[$i]->{removable} : 1; + $dnd->addBucket($indices->[0] != -1 ? $indices : [], label => $label, removable => $removable); + } + } + + my @shuffled_subsets_array = (); + for my $subset ( @$subsets ) { + my @shuffled_subset = map {$unorder[$_]} @$subset; + push(@shuffled_subsets_array, @$subset != 0 ? main::Set(join(',', @shuffled_subset)) : main::Set()); + } + my $shuffled_subsets = main::List(@shuffled_subsets_array); + + $self = bless { + set => $set, + shuffledSet => $shuffledSet, + numProvided => $numProvided, + order => \@order, + unordered => \@unorder, + shuffled_subsets => $shuffled_subsets, + answerInputId => $answerInputId, + dnd => $dnd, + ans_rule => $ans_rule, + OrderedSubsets => $options{OrderedSubsets}, + AllowNewBuckets => $options{AllowNewBuckets}, + }, $class; + + return $self; +} + +sub Print { + my $self = shift; + + my $ans_rule = $self->{ans_rule}; + + if ($main::displayMode ne "TeX") { + # HTML mode + return join("\n", + '
', + $ans_rule, + $self->{dnd}->HTML, + '
', + '
', + ); + } else { + # TeX mode + return $self->{dnd}->TeX; + } +} + +sub cmp { + my $self = shift; + return $self->{shuffled_subsets}->cmp(ordered => $self->{OrderedSubsets}, removeParens => 1, partialCredit => 1)->withPreFilter(sub {$self->prefilter(@_)})->withPostFilter(sub {$self->filter(@_)}); +} + +sub prefilter { + my $self = shift; my $anshash = shift; + + my @student = ( $anshash->{original_student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); + + my @studentAnsArray; + for my $match ( @student ) { + if ($match =~ /-1/) { + push(@studentAnsArray, main::Set()); # index -1 corresponds to empty set + } else { + push(@studentAnsArray, main::Set($match =~ s/\(|\)//gr)); + } + } + + $anshash->{student_ans} = main::List(@studentAnsArray); + + return $anshash; +} + +sub filter { + my $self = shift; my $anshash = shift; + + my @order = @{ $self->{order} }; + my @student = ( $anshash->{original_student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); + my @correct = ( $anshash->{correct_ans} =~ /({[^{}]*}|-?\d+)/g ); + + $anshash->{correct_ans_latex_string} = join (",", map { + "\\{\\text{".join(",", (map { + $_ != -1 ? $self->{shuffledSet}->[$_] : '' + } (split(',', $_ =~ s/{|}//gr)) ))."}\\}" + } @correct); + + $anshash->{preview_latex_string} = join (",", map { + "\\{\\text{".join(",", (map { + $_ != -1 ? $self->{shuffledSet}->[$_] : '' + } (split(',', $_ =~ s/\(|\)//gr)) ))."}\\}" + } @student); + + $anshash->{student_ans} = "(see preview)"; + + return $anshash; +} +1; \ No newline at end of file From 7acd81d821bca1e8064f03dfd5928fde9c6d2017 Mon Sep 17 00:00:00 2001 From: pschan-gh <31397850+pschan-gh@users.noreply.github.com> Date: Thu, 2 Sep 2021 12:36:16 +0800 Subject: [PATCH 011/134] Added prefilter to answer checker to deal with $showPartialCorrectAnswers = 1; Copied over functions (e.g. "numNeeded") from the original draggableProof.pl which might be called by certain pg problems. --- macros/draggableProof.pl | 69 ++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/macros/draggableProof.pl b/macros/draggableProof.pl index 0b05f01e1d..f14e71e00f 100644 --- a/macros/draggableProof.pl +++ b/macros/draggableProof.pl @@ -1,7 +1,7 @@ ################################################################ =head1 NAME draggableProof.pl - + =head1 DESCRIPTION =head1 TERMINOLOGY @@ -41,9 +41,9 @@ =head1 USAGE =head1 EXAMPLE DOCUMENT(); loadMacros( - "PGstandard.pl", - "MathObjects.pl", - "draggableProof.pl" +"PGstandard.pl", +"MathObjects.pl", +"draggableProof.pl" ); TEXT(beginproblem()); @@ -123,7 +123,6 @@ sub new { my $self = shift; my $class = ref($self) || $self; - # user arguments my $proof = shift; my $extra = shift; my %options = ( @@ -136,7 +135,6 @@ sub new { IrrelevancePenalty => 1, @_ ); - # end user arguments my $lines = [ @$proof, @$extra ]; my $numNeeded = scalar(@$proof); @@ -174,8 +172,8 @@ sub new { } my $proof = $options{NumBuckets} == 2 ? main::List( - main::List(@unorder[$numNeeded .. $numProvided - 1]), - main::List(@unorder[0..$numNeeded-1]) + main::List(@unorder[$numNeeded .. $numProvided - 1]), + main::List(@unorder[0..$numNeeded-1]) ) : main::List('('.join(',', @unorder[0..$numNeeded-1]).')'); my $extra = main::Set(@unorder[$numNeeded .. $numProvided - 1]); @@ -223,6 +221,12 @@ sub new { return $self; } +sub lines {@{shift->{lines}}} +sub numNeeded {shift->{numNeeded}} +sub numProvided {shift->{numProvided}} +sub order {@{shift->{order}}} +sub unorder {@{shift->{unorder}}} + sub Levenshtein { my @ar1 = split /$_[2]/, $_[0]; my @ar2 = split /$_[2]/, $_[1]; @@ -309,7 +313,29 @@ sub Print { sub cmp { my $self = shift; - return $self->{proof}->cmp(ordered => 1, removeParens => 1)->withPreFilter("erase")->withPostFilter(sub {$self->filter(@_)}); + + return $self->{proof}->cmp(ordered => 1, removeParens => 1)->withPreFilter(sub {$self->prefilter(@_)})->withPostFilter(sub {$self->filter(@_)}); + +} + +sub prefilter { + my $self = shift; + my $anshash = shift; + + my $correctProcessed; + + $anshash->{original_correct_value} = $anshash->{correct_value}; + + if ($self->{NumBuckets} == 1) { + $correctProcessed = $anshash->{correct_value} =~ s/\(|\)|\s*//gr; + } elsif ($self->{NumBuckets} == 2) { + my @matches = ( $anshash->{correct_value} =~ /(\([^\(\)]*\)|-?\d+)/g ); + $correctProcessed = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; + } + + $anshash->{correct_value} = main::List($correctProcessed); + + return $anshash; } sub filter { @@ -319,25 +345,20 @@ sub filter { my @lines = @{$self->{lines}}; my @order = @{$self->{order}}; + my $correct_value = $anshash->{correct_value}; my $actualAnswer; - my $correctProcessed; if ($self->{NumBuckets} == 1) { - $actualAnswer = $anshash->{student_ans} =~ s/\(|\)|\s*//gr; - $correctProcessed = $anshash->{correct_ans} =~ s/\(|\)|\s*//gr; + $actualAnswer = $anshash->{student_value} =~ s/\(|\)|\s*//gr; } elsif ($self->{NumBuckets} == 2) { - my @matches = ( $anshash->{student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); + my @matches = ( $anshash->{student_value} =~ /(\([^\(\)]*\)|-?\d+)/g ); $actualAnswer = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; - @matches = ( $anshash->{correct_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); - $correctProcessed = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; - } - - my $correct_ans = main::List($correctProcessed); + } if ($self->{Levenshtein} == 1) { - $anshash->{score} = 1 - main::min(1, Levenshtein($correct_ans, $actualAnswer, ',')/$self->{numNeeded}); + $anshash->{score} = 1 - main::min(1, Levenshtein($correct_value, $actualAnswer, ',')/$self->{numNeeded}); } elsif ($self->{DamerauLevenshtein} == 1) { - $anshash->{score} = 1 - main::min(1, DamerauLevenshtein($correct_ans, $actualAnswer, ',', $self->{numProvided})/$self->{numNeeded}); + $anshash->{score} = 1 - main::min(1, DamerauLevenshtein($correct_value, $actualAnswer, ',', $self->{numProvided})/$self->{numNeeded}); } elsif (@{ $self->{inferenceMatrix} } != 0) { my @unshuffledStudentIndices = map { $self->{order}[$_]} split(',', $actualAnswer); my @inferenceMatrix = @{ $self->{inferenceMatrix} }; @@ -366,18 +387,18 @@ sub filter { } } } else { - $anshash->{score} = $correct_ans eq main::List($actualAnswer) ? 1 : 0; + $anshash->{score} = $correct_value eq main::List($actualAnswer) ? 1 : 0; } - - my @correct = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $correct_ans); + my @correct = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $correct_value); my @student = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $actualAnswer); $anshash->{non_tex_preview} = 1; $anshash->{student_ans} = "(see preview)"; $anshash->{preview_latex_string} = "
  • ".join("
  • ",@student)."
"; $anshash->{correct_ans_latex_string} = "
  • ".join("
  • ",@correct)."
"; - + $anshash->{correct_value} = $anshash->{original_correct_value}; + return $anshash; } 1; From 3437f7a4a98a98367356bf7f842ff0259a48849a Mon Sep 17 00:00:00 2001 From: pschan-gh <31397850+pschan-gh@users.noreply.github.com> Date: Fri, 3 Sep 2021 18:26:09 +0800 Subject: [PATCH 012/134] Formatting and white-space cleanup. Also, a previous update broke scoring-by-edit-distance. The variable $actualAnswer in draggableProof.pl is redefined to address this. --- lib/DragNDrop.pm | 193 +++++++----- macros/draggableProof.pl | 599 ++++++++++++++++++++----------------- macros/draggableSubsets.pl | 151 ++++++---- 3 files changed, 532 insertions(+), 411 deletions(-) diff --git a/lib/DragNDrop.pm b/lib/DragNDrop.pm index 1208f49883..038666206a 100644 --- a/lib/DragNDrop.pm +++ b/lib/DragNDrop.pm @@ -1,16 +1,35 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2021 The WeBWorK Project, http://openwebwork.sf.net/ +# +# 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 + DragNDrop.pm - Drag-N-Drop Module - + =head1 DESCRIPTION -DragNDrop.pm is a backend Perl module which facilitates the implementation of + +DragNDrop.pm is a backend Perl module which facilitates the implementation of 'Drag-And-Drop' in WeBWorK problems. It is meant to be used by other perl macros such as draggableProof.pl and draggableSubsets.pl =head1 TERMINOLOGY + An HTML element into or out of which other elements may be dragged will be called a "bucket". An HTML element which houses a collection of buckets will be called a "bucket pool". =head1 USAGE + Each macro aiming to implement drag-n-drop features must call at its initialization: ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1); @@ -36,15 +55,18 @@ allows student to create new buckets by clicking on a button. To add a bucket to an existing pool $bucket_pool, do: $bucket_pool->addBucket($indices); -$indices is the reference to the array of indices corresponding to the statements in $aggregateList to be pre-included in the bucket. -For example, if the $aggregateList is ['Socrates is a man', 'all men are mortal', 'therefore Socrates is mortal'], and the bucket consists of { 'Socrates is a man', 'therefore Socrates is mortal' }, then $indices = [0, 2]. +$indices is the reference to the array of indices corresponding to the statements in $aggregateList +to be pre-included in the bucket. +For example, if the $aggregateList is ['Socrates is a man', 'all men are mortal', 'therefore Socrates is mortal'], +and the bucket consists of { 'Socrates is a man', 'therefore Socrates is mortal' }, then $indices = [0, 2]. An empty array reference, e.g. $bucket_pool->addBucket([]), gives an empty bucket. ############################################################################### OPTIONAL: $bucket_pool->addBucket($indices, label => 'Barrel', removable => 1) puts the label 'Barrel' at the top of the bucket. -With the removable option set to 1, the bucket may be removed by the student via the click of a "Remove" button at the bottom of the bucket. +With the removable option set to 1, the bucket may be removed by the student via the click of a "Remove" button +at the bottom of the bucket. (The first created bucket may never be removed.) ############################################################################### @@ -55,8 +77,11 @@ To output the bucket pool to LaTeX, call: $bucket_pool->TeX =head1 EXAMPLES + See draggableProof.pl and draggableSubsets.pl + =cut + ############################################################################### use strict; @@ -64,100 +89,108 @@ use warnings; package DragNDrop; -sub new { - my $self = shift; - my $class = ref($self) || $self; - - my $answerInputId = shift; # 'id' of html tag corresponding to the answer blank. Must be unique to each pool of DragNDrop buckets - my $aggregateList = shift; # array of all statements provided - my $defaultBuckets = shift; # instructor-provided default buckets with pre-included statements encoded by the array of corresponding statement indices - my %options = ( - AllowNewBuckets => 0, - @_ +sub new { + my $self = shift; + my $class = ref($self) || $self; + + # 'id' of html tag corresponding to the answer blank. Must be unique to each pool of DragNDrop buckets + my $answerInputId = shift; + + # array of all statements provided + my $aggregateList = shift; + + # instructor-provided default buckets with pre-included statements encoded + # by the array of corresponding statement indices + my $defaultBuckets = shift; + + my %options = ( + AllowNewBuckets => 0, + @_ ); - - $self = bless { - answerInputId => $answerInputId, - bucketList => [], - aggregateList => $aggregateList, - defaultBuckets => $defaultBuckets, + + $self = bless { + answerInputId => $answerInputId, + bucketList => [], + aggregateList => $aggregateList, + defaultBuckets => $defaultBuckets, %options, - }, $class; - - return $self; + }, $class; + + return $self; } -sub addBucket { - my $self = shift; - - my $indices = shift; +sub addBucket { + my $self = shift; + + my $indices = shift; my %options = ( label => "", removable => 0, @_ - ); + ); my $bucket = { - indices => $indices, - list => [ map { $self->{aggregateList}->[$_] } @$indices ], - bucket_id => scalar @{ $self->{bucketList} }, + indices => $indices, + list => [ map { $self->{aggregateList}->[$_] } @$indices ], + bucket_id => scalar @{ $self->{bucketList} }, label => $options{label}, - removable => $options{removable}, - }; - push(@{$self->{bucketList}}, $bucket); - + removable => $options{removable}, + }; + push(@{$self->{bucketList}}, $bucket); + } sub HTML { - my $self = shift; - - my $out = ''; - $out .= "
"; - - # buckets from instructor-defined default settings - for (my $i = 0; $i < @{$self->{defaultBuckets}}; $i++) { - my $defaultBucket = $self->{defaultBuckets}->[$i]; - $out .= ""; - } - + my $self = shift; + + my $out = ''; + $out .= "
"; + + # buckets from instructor-defined default settings + for (my $i = 0; $i < @{$self->{defaultBuckets}}; $i++) { + my $defaultBucket = $self->{defaultBuckets}->[$i]; + $out .= ""; + } + # buckets from past answers - for my $bucket ( @{$self->{bucketList}} ) { - $out .= ""; - } - $out .= '
'; - $out .= "
reset"; - if ($self->{AllowNewBuckets} == 1) { - $out .= "add bucket"; - } + for my $bucket ( @{$self->{bucketList}} ) { + $out .= ""; + } + $out .= '
'; + $out .= "
reset"; + if ($self->{AllowNewBuckets} == 1) { + $out .= "add bucket"; + } $out .= "
"; - - return $out; + + return $out; } sub TeX { - my $self = shift; - - my $out = ""; - - # default buckets; - for (my $i = 0; $i < @{ $self->{defaultBuckets} }; $i++) { + my $self = shift; + + my $out = ""; + + # default buckets; + for (my $i = 0; $i < @{ $self->{defaultBuckets} }; $i++) { $out .= "\n"; - my $defaultBucket = $self->{defaultBuckets}->[$i]; + my $defaultBucket = $self->{defaultBuckets}->[$i]; if ( @{$defaultBucket->{indices}} > 0 ) { $out .= "\n\\hrule\n\\begin{itemize}"; for my $j ( @{$defaultBucket->{indices}} ) { @@ -166,7 +199,7 @@ sub TeX { $out .= "\n\\end{itemize}"; } $out .= "\n\\hrule\n"; - } - return $out; + } + return $out; } 1; \ No newline at end of file diff --git a/macros/draggableProof.pl b/macros/draggableProof.pl index f14e71e00f..a16a6d8695 100644 --- a/macros/draggableProof.pl +++ b/macros/draggableProof.pl @@ -1,14 +1,34 @@ -################################################################ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2021 The WeBWorK Project, http://openwebwork.sf.net/ +# +# 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 + draggableProof.pl =head1 DESCRIPTION +This macro helps the instructor create a drag-and-drop environment in which students are asked to +arrange predefined statements into a correct sequence. + =head1 TERMINOLOGY + An HTML element into or out of which other elements may be dragged will be called a "bucket". An HTML element which houses a collection of buckets will be called a "bucket pool". =head1 USAGE + To initialize a DraggableProof bucket pool in a .pg problem, insert the line: $draggable = DraggableProof($statements, $extra, Options1 => ..., Options2 => ...); @@ -21,11 +41,16 @@ =head1 USAGE within the BEGIN_TEXT / END_TEXT environment; -$statements, e.g. ["Socrates is a man.", "Socrates is mortal.", ...], is an array reference to the list of statements used in the correct proof. It is imperative that square brackets be used. +$statements, e.g. ["Socrates is a man.", "Socrates is mortal.", ...], +is an array reference to the list of statements used in the correct proof. +It is imperative that square brackets be used. -$extra, e.g. ["Roses are red."], is an array reference to the list statements extraneous to the proof. If there are no extraneous statements, use the empty array reference []. +$extra, e.g. ["Roses are red."], is an array reference to the list statements extraneous to the proof. +If there are no extraneous statements, use the empty array reference []. -By default, the score of the student answer is 100% if the draggable statements are placed in the exact same order as in the array referenced by $statements, with no inclusion of any statement from $extra. The score is 0% otherwise. +By default, the score of the student answer is 100% +if the draggable statements are placed in the exact same order as in the array referenced by $statements, +with no inclusion of any statement from $extra. The score is 0% otherwise. Available Options: NumBuckets => 1 or 2 @@ -39,6 +64,7 @@ =head1 USAGE Their usage is explained in the example below. =head1 EXAMPLE + DOCUMENT(); loadMacros( "PGstandard.pl", @@ -65,33 +91,41 @@ =head1 EXAMPLE $extra, NumBuckets => 2, # either 1 or 2. SourceLabel => "Axioms", # label of first bucket if NumBuckets = 2. -TargetLabel => "Reasoning", # label of second bucket if NumBuckets = 2, of the only bucket if NumBuckets = 1. +# +TargetLabel => "Reasoning", +# label of second bucket if NumBuckets = 2, +# of the only bucket if NumBuckets = 1. ################################################################ -# Levenshtein => 1, -# If equal to 1, scoring is determined by the Levenshtein edit distance between student answer and correct answer. +# Levenshtein => 1, +# If equal to 1, scoring is determined by the Levenshtein edit distance between student answer and correct answer. ################################################################ -# DamerauLevenshtein => 1, +# DamerauLevenshtein => 1, # If equal to 1, scoring is determined by the Damerau-Levenshtein distance between student answer and correct answer. -# A pair of transposed adjacent statements is counted as two mistakes under Levenshtein scoring, but as one mistake under Damerau-Levenshtein scoring. +# A pair of transposed adjacent statements is counted as two mistakes under Levenshtein scoring, +# but as one mistake under Damerau-Levenshtein scoring. ################################################################ InferenceMatrix => [ [0, 0, 1], [0, 0, 1], [0, 0, 0] ], -# (i, j)-entry is nonzero <=> statement i implies statement j. The score of each corresponding inference is weighted according to the value of the matrix entry. +# (i, j)-entry is nonzero <=> statement i implies statement j. +# The score of each corresponding inference is weighted according to the value of the matrix entry. ################################################################ -IrrelevancePenalty => 1 # This option is processed only if the InferenceMatrix option is set. Penalty for each extraneous statement in the student answer is divided by the total number of inference points (i.e. sum of all entries in the InferenceMatrix). Default value = 1. +IrrelevancePenalty => 1 # This option is processed only if the InferenceMatrix option is set. +# Penalty for each extraneous statement in the student answer is +# divided by the total number of inference points (i.e. sum of all entries in the InferenceMatrix). +# Default value = 1. ); -Context()->texStrings; +Context()->texStrings; # must be placed AFTER defining all DraggableSubsets. BEGIN_TEXT Show that Socrates is mortal by dragging the relevant $BBOLD Axioms $EBOLD into the $BBOLD Reasoning $EBOLD box in an appropriate order. -$PAR +$PAR \{ $draggable->Print \} @@ -101,7 +135,9 @@ =head1 EXAMPLE ANS($draggable->cmp); ENDDOCUMENT(); + =cut + ################################################################ loadMacros( @@ -110,115 +146,115 @@ =head1 EXAMPLE ); sub _draggableProof_init { - ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); - ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1); - ADD_CSS_FILE("js/apps/DragNDrop/dragndrop.css", 0); - ADD_JS_FILE("js/apps/DragNDrop/dragndrop.js", 0, { defer => undef }); - PG_restricted_eval("sub DraggableProof {new draggableProof(\@_)}"); + ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); + ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1); + ADD_CSS_FILE("js/apps/DragNDrop/dragndrop.css", 0); + ADD_JS_FILE("js/apps/DragNDrop/dragndrop.js", 0, { defer => undef }); + PG_restricted_eval("sub DraggableProof {new draggableProof(\@_)}"); } package draggableProof; sub new { - my $self = shift; - my $class = ref($self) || $self; - - my $proof = shift; - my $extra = shift; - my %options = ( - SourceLabel => "Choose from these sentences:", - TargetLabel => "Your Proof:", - NumBuckets => 2, - Levenshtein => 0, - DamerauLevenshtein => 0, - InferenceMatrix => [], - IrrelevancePenalty => 1, - @_ - ); - - my $lines = [ @$proof, @$extra ]; - my $numNeeded = scalar(@$proof); - my $numProvided = scalar(@$lines); - my @order = main::shuffle($numProvided); - my @unorder = main::invert(@order); - my $shuffledLines = [ map {$lines->[$_]} @order ]; - - my $answerInputId = main::NEW_ANS_NAME() unless $self->{answerInputId}; - my $ans_rule = main::NAMED_HIDDEN_ANS_RULE($answerInputId); - - my $dnd; - if ($options{NumBuckets} == 2) { - $dnd = new DragNDrop($answerInputId, $shuffledLines, - [ - { - indices => [0..$numProvided-1], - label => $options{'SourceLabel'} - }, - { - indices => [], - label => $options{'TargetLabel'} - } - ], - AllowNewBuckets => 0); - } elsif($options{NumBuckets} == 1) { - $dnd = new DragNDrop($answerInputId, $shuffledLines, - [ - { - indices => [0..$numProvided-1], - label => $options{'TargetLabel'} - } - ], - AllowNewBuckets => 0); - } - - my $proof = $options{NumBuckets} == 2 ? main::List( - main::List(@unorder[$numNeeded .. $numProvided - 1]), - main::List(@unorder[0..$numNeeded-1]) - ) : main::List('('.join(',', @unorder[0..$numNeeded-1]).')'); - - my $extra = main::Set(@unorder[$numNeeded .. $numProvided - 1]); - - my $InferenceMatrix = $options{InferenceMatrix}; - - $self = bless { - lines => $lines, - shuffledLines => $shuffledLines, - numNeeded => $numNeeded, - numProvided => $numProvided, - order => \@order, - unorder => \@unorder, - proof => $proof, - extra => $extra, - answerInputId => $answerInputId, - dnd => $dnd, - ans_rule => $ans_rule, - inferenceMatrix => $InferenceMatrix, - %options, - }, $class; - - my $previous = $main::inputs_ref->{$answerInputId} || ''; - - if ($previous eq "") { - if ($self->{NumBuckets} == 2) { - $dnd->addBucket([0..$numProvided-1], label => $options{'SourceLabel'}); - $dnd->addBucket([], label => $options{'TargetLabel'}); - } elsif ($self->{NumBuckets} == 1) { - $dnd->addBucket([0..$numProvided-1], label => $options{'TargetLabel'}); - } - } else { - my @matches = ( $previous =~ /(\([^\(\)]*\)|-?\d+)/g ); - if ($self->{NumBuckets} == 2) { - my $indices1 = [ split(',', @matches[0] =~ s/\(|\)//gr) ]; - $dnd->addBucket($indices1->[0] != -1 ? $indices1 : [], label => $options{'SourceLabel'}); - my $indices2 = [ split(',', @matches[1] =~ s/\(|\)//gr) ]; - $dnd->addBucket($indices2->[0] != -1 ? $indices2 : [], label => $options{'TargetLabel'}); - } else { - my $indices1 = [ split(',', @matches[0] =~ s/\(|\)//gr) ]; - $dnd->addBucket($indices1->[0] != -1 ? $indices1 : [], label => $options{'TargetLabel'}); - } - } - - return $self; + my $self = shift; + my $class = ref($self) || $self; + + my $proof = shift; + my $extra = shift; + my %options = ( + SourceLabel => "Choose from these sentences:", + TargetLabel => "Your Proof:", + NumBuckets => 2, + Levenshtein => 0, + DamerauLevenshtein => 0, + InferenceMatrix => [], + IrrelevancePenalty => 1, + @_ + ); + + my $lines = [ @$proof, @$extra ]; + my $numNeeded = scalar(@$proof); + my $numProvided = scalar(@$lines); + my @order = main::shuffle($numProvided); + my @unorder = main::invert(@order); + my $shuffledLines = [ map {$lines->[$_]} @order ]; + + my $answerInputId = main::NEW_ANS_NAME() unless $self->{answerInputId}; + my $ans_rule = main::NAMED_HIDDEN_ANS_RULE($answerInputId); + + my $dnd; + if ($options{NumBuckets} == 2) { + $dnd = new DragNDrop($answerInputId, $shuffledLines, + [ + { + indices => [0..$numProvided-1], + label => $options{'SourceLabel'} + }, + { + indices => [], + label => $options{'TargetLabel'} + } + ], + AllowNewBuckets => 0); + } elsif($options{NumBuckets} == 1) { + $dnd = new DragNDrop($answerInputId, $shuffledLines, + [ + { + indices => [0..$numProvided-1], + label => $options{'TargetLabel'} + } + ], + AllowNewBuckets => 0); + } + + my $proof = $options{NumBuckets} == 2 ? main::List( + main::List(@unorder[$numNeeded .. $numProvided - 1]), + main::List(@unorder[0..$numNeeded-1]) + ) : main::List('('.join(',', @unorder[0..$numNeeded-1]).')'); + + my $extra = main::Set(@unorder[$numNeeded .. $numProvided - 1]); + + my $InferenceMatrix = $options{InferenceMatrix}; + + $self = bless { + lines => $lines, + shuffledLines => $shuffledLines, + numNeeded => $numNeeded, + numProvided => $numProvided, + order => \@order, + unorder => \@unorder, + proof => $proof, + extra => $extra, + answerInputId => $answerInputId, + dnd => $dnd, + ans_rule => $ans_rule, + inferenceMatrix => $InferenceMatrix, + %options, + }, $class; + + my $previous = $main::inputs_ref->{$answerInputId} || ''; + + if ($previous eq "") { + if ($self->{NumBuckets} == 2) { + $dnd->addBucket([0..$numProvided-1], label => $options{'SourceLabel'}); + $dnd->addBucket([], label => $options{'TargetLabel'}); + } elsif ($self->{NumBuckets} == 1) { + $dnd->addBucket([0..$numProvided-1], label => $options{'TargetLabel'}); + } + } else { + my @matches = ( $previous =~ /(\([^\(\)]*\)|-?\d+)/g ); + if ($self->{NumBuckets} == 2) { + my $indices1 = [ split(',', @matches[0] =~ s/\(|\)//gr) ]; + $dnd->addBucket($indices1->[0] != -1 ? $indices1 : [], label => $options{'SourceLabel'}); + my $indices2 = [ split(',', @matches[1] =~ s/\(|\)//gr) ]; + $dnd->addBucket($indices2->[0] != -1 ? $indices2 : [], label => $options{'TargetLabel'}); + } else { + my $indices1 = [ split(',', @matches[0] =~ s/\(|\)//gr) ]; + $dnd->addBucket($indices1->[0] != -1 ? $indices1 : [], label => $options{'TargetLabel'}); + } + } + + return $self; } sub lines {@{shift->{lines}}} @@ -227,178 +263,191 @@ sub new { sub order {@{shift->{order}}} sub unorder {@{shift->{unorder}}} -sub Levenshtein { - my @ar1 = split /$_[2]/, $_[0]; - my @ar2 = split /$_[2]/, $_[1]; - - my @dist = ([0 .. @ar2]); - $dist[$_][0] = $_ for (1 .. @ar1); - - for my $i (0 .. $#ar1) { - for my $j (0 .. $#ar2) { - $dist[$i+1][$j+1] = main::min($dist[$i][$j+1] + 1, $dist[$i+1][$j] + 1, - $dist[$i][$j] + ($ar1[$i] ne $ar2[$j]) ); - } - } - $dist[-1][-1]; +sub Levenshtein { + my @ar1 = split /$_[2]/, $_[0]; + my @ar2 = split /$_[2]/, $_[1]; + + my @dist = ([0 .. @ar2]); + $dist[$_][0] = $_ for (1 .. @ar1); + + for my $i (0 .. $#ar1) { + for my $j (0 .. $#ar2) { + $dist[$i+1][$j+1] = main::min($dist[$i][$j+1] + 1, $dist[$i+1][$j] + 1, + $dist[$i][$j] + ($ar1[$i] ne $ar2[$j]) ); + } + } + $dist[-1][-1]; } sub DamerauLevenshtein { - # Damerau–Levenshtein distance with adjacent transpositions - # https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance - - my $discourse1 = shift; - my $discourse2 = shift; - my $delimiter = shift; - my $numProvided = shift; - - my @ar1 = split /$delimiter/, $discourse1; - my @ar2 = split /$delimiter/, $discourse2; - - my @da = (0) x $numProvided; - my @d = (); - - my $maxdist = @ar1 + @ar2; - for my $i (1 .. @ar1 + 1) { - push(@d, [ (0) x (@ar2 + 2) ] ); - $d[$i][0] = $maxdist; - $d[$i][1] = $i - 1; - } - for my $j (1 .. @ar2 + 1) { - $d[0][$j] = $maxdist; - $d[1][$j] = $j - 1; - } - my $db; - for my $i (2 .. @ar1 + 1) { - $db = 0; - my $k, $l, $cost; - for my $j (2 .. @ar2 + 1) { - $k = $da[$ar2[$j - 2]]; - $l = $db; - if ($ar1[$i - 2] == $ar2[$j - 2]) { - $cost = 0; - $db = $j; - } else { - $cost = 1; - } - $d[$i][$j] = main::min($d[$i-1][$j-1] + $cost, - $d[$i][$j-1] + 1, - $d[$i-1][$j] + 1, - $d[$k-1][$l-1] + ($i - $k - 1) + 1 + ($j - $l - 1)); - } - $da[$ar1[$i - 2]] = $i; - } - $d[-1][-1]; + # Damerau–Levenshtein distance with adjacent transpositions + # https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance + + my $discourse1 = shift; + my $discourse2 = shift; + my $delimiter = shift; + my $numProvided = shift; + + my @ar1 = split /$delimiter/, $discourse1; + my @ar2 = split /$delimiter/, $discourse2; + + my @da = (0) x $numProvided; + my @d = (); + + my $maxdist = @ar1 + @ar2; + for my $i (1 .. @ar1 + 1) { + push(@d, [ (0) x (@ar2 + 2) ] ); + $d[$i][0] = $maxdist; + $d[$i][1] = $i - 1; + } + for my $j (1 .. @ar2 + 1) { + $d[0][$j] = $maxdist; + $d[1][$j] = $j - 1; + } + my $db; + for my $i (2 .. @ar1 + 1) { + $db = 0; + my $k, $l, $cost; + for my $j (2 .. @ar2 + 1) { + $k = $da[$ar2[$j - 2]]; + $l = $db; + if ($ar1[$i - 2] == $ar2[$j - 2]) { + $cost = 0; + $db = $j; + } else { + $cost = 1; + } + $d[$i][$j] = main::min($d[$i-1][$j-1] + $cost, + $d[$i][$j-1] + 1, + $d[$i-1][$j] + 1, + $d[$k-1][$l-1] + ($i - $k - 1) + 1 + ($j - $l - 1)); + } + $da[$ar1[$i - 2]] = $i; + } + $d[-1][-1]; } sub Print { - my $self = shift; - - my $ans_rule = $self->{ans_rule}; - - if ($main::displayMode ne "TeX") { - # HTML mode - return join("\n", - '
', - $ans_rule, - $self->{dnd}->HTML, - '
', - '
', - ); - } else { - # TeX mode - return $self->{dnd}->TeX; - } + my $self = shift; + + my $ans_rule = $self->{ans_rule}; + + if ($main::displayMode ne "TeX") { + # HTML mode + return join("\n", + '
', + $ans_rule, + $self->{dnd}->HTML, + '
', + '
', + ); + } else { + # TeX mode + return $self->{dnd}->TeX; + } } sub cmp { - my $self = shift; - - return $self->{proof}->cmp(ordered => 1, removeParens => 1)->withPreFilter(sub {$self->prefilter(@_)})->withPostFilter(sub {$self->filter(@_)}); - + my $self = shift; + return $self->{proof} + ->cmp(ordered => 1, removeParens => 1) + ->withPreFilter(sub {$self->prefilter(@_)}) + ->withPostFilter(sub {$self->filter(@_)}); } sub prefilter { - my $self = shift; - my $anshash = shift; - - my $correctProcessed; - - $anshash->{original_correct_value} = $anshash->{correct_value}; - - if ($self->{NumBuckets} == 1) { - $correctProcessed = $anshash->{correct_value} =~ s/\(|\)|\s*//gr; - } elsif ($self->{NumBuckets} == 2) { - my @matches = ( $anshash->{correct_value} =~ /(\([^\(\)]*\)|-?\d+)/g ); - $correctProcessed = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; - } - - $anshash->{correct_value} = main::List($correctProcessed); - - return $anshash; + my $self = shift; + my $anshash = shift; + + my $correctProcessed; + + $anshash->{original_correct_value} = $anshash->{correct_value}; + + if ($self->{NumBuckets} == 1) { + $correctProcessed = $anshash->{correct_value} =~ s/\(|\)|\s*//gr; + } elsif ($self->{NumBuckets} == 2) { + my @matches = ( $anshash->{correct_value} =~ /(\([^\(\)]*\)|-?\d+)/g ); + $correctProcessed = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; + } + + $anshash->{correct_value} = main::List($correctProcessed); + + return $anshash; } sub filter { - my $self = shift; - my $anshash = shift; - - my @lines = @{$self->{lines}}; - my @order = @{$self->{order}}; - - my $correct_value = $anshash->{correct_value}; - my $actualAnswer; - - if ($self->{NumBuckets} == 1) { - $actualAnswer = $anshash->{student_value} =~ s/\(|\)|\s*//gr; - } elsif ($self->{NumBuckets} == 2) { - my @matches = ( $anshash->{student_value} =~ /(\([^\(\)]*\)|-?\d+)/g ); - $actualAnswer = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; - } - - if ($self->{Levenshtein} == 1) { - $anshash->{score} = 1 - main::min(1, Levenshtein($correct_value, $actualAnswer, ',')/$self->{numNeeded}); - } elsif ($self->{DamerauLevenshtein} == 1) { - $anshash->{score} = 1 - main::min(1, DamerauLevenshtein($correct_value, $actualAnswer, ',', $self->{numProvided})/$self->{numNeeded}); - } elsif (@{ $self->{inferenceMatrix} } != 0) { - my @unshuffledStudentIndices = map { $self->{order}[$_]} split(',', $actualAnswer); - my @inferenceMatrix = @{ $self->{inferenceMatrix} }; - my $inferenceScore = 0; - for (my $j = 0; $j < @unshuffledStudentIndices; $j++ ) { - if ($unshuffledStudentIndices[$j] < $self->{numNeeded}) { - for (my $i = $j - 1; $i >= 0; $i--) { - if ($unshuffledStudentIndices[$i] < $self->{numNeeded}) { - $inferenceScore += $inferenceMatrix[$unshuffledStudentIndices[$i]][$unshuffledStudentIndices[$j]]; - } - } - } - } - my $total = 0; - for my $row ( @inferenceMatrix ) { - foreach (@$row) { - $total += $_; - } - } - $anshash->{score} = $inferenceScore / $total; - - my %invoked = map { $_ => 1 } split(',', $actualAnswer); - foreach ( split(',', $self->{extra}->string =~ s/{|}|\s*//gr ) ) { - if ( exists($invoked{$_}) ) { - $anshash->{score} = main::max(0, $anshash->{score} - ($self->{IrrelevancePenalty}/$total)); - } - } - } else { - $anshash->{score} = $correct_value eq main::List($actualAnswer) ? 1 : 0; - } - - my @correct = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $correct_value); - my @student = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $actualAnswer); - - $anshash->{non_tex_preview} = 1; - $anshash->{student_ans} = "(see preview)"; - $anshash->{preview_latex_string} = "
  • ".join("
  • ",@student)."
"; - $anshash->{correct_ans_latex_string} = "
  • ".join("
  • ",@correct)."
"; - $anshash->{correct_value} = $anshash->{original_correct_value}; - - return $anshash; + my $self = shift; + my $anshash = shift; + + my @lines = @{$self->{lines}}; + my @order = @{$self->{order}}; + + my $correct_value = $anshash->{correct_value}; + my $actualAnswer; + + if ($self->{NumBuckets} == 1) { + $actualAnswer = main::List( $anshash->{student_value} =~ s/\(|\)|\s*//gr ); + } elsif ($self->{NumBuckets} == 2) { + my @matches = ( $anshash->{student_value} =~ /(\([^\(\)]*\)|-?\d+)/g ); + $actualAnswer = main::List( @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : '' ); + } + if ($self->{Levenshtein} == 1) { + $anshash->{score} = 1 - main::min(1, Levenshtein($correct_value, $actualAnswer, ',')/$self->{numNeeded}); + } elsif ($self->{DamerauLevenshtein} == 1) { + my $DLDistance = DamerauLevenshtein($correct_value, $actualAnswer, ',', $self->{numProvided}); + $anshash->{score} = 1 - main::min(1, $DLDistance/($self->{numNeeded})); + } elsif (@{ $self->{inferenceMatrix} } != 0) { + my @unshuffledStudentIndices = map { $self->{order}[$_]} split(',', $actualAnswer); + my @inferenceMatrix = @{ $self->{inferenceMatrix} }; + my $inferenceScore = 0; + for (my $j = 0; $j < @unshuffledStudentIndices; $j++ ) { + if ($unshuffledStudentIndices[$j] < $self->{numNeeded}) { + for (my $i = $j - 1; $i >= 0; $i--) { + if ($unshuffledStudentIndices[$i] < $self->{numNeeded}) { + $inferenceScore += + $inferenceMatrix[$unshuffledStudentIndices[$i]][$unshuffledStudentIndices[$j]]; + } + } + } + } + my $total = 0; + for my $row ( @inferenceMatrix ) { + foreach (@$row) { + $total += $_; + } + } + $anshash->{score} = $inferenceScore / $total; + + my %invoked = map { $_ => 1 } split(',', $actualAnswer); + foreach ( split(',', $self->{extra}->string =~ s/{|}|\s*//gr ) ) { + if ( exists($invoked{$_}) ) { + $anshash->{score} = main::max(0, $anshash->{score} - ($self->{IrrelevancePenalty}/$total)); + } + } + } else { + $anshash->{score} = $correct_value eq main::List($actualAnswer) ? 1 : 0; + } + + my @correct = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $correct_value); + my @student = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $actualAnswer); + + $anshash->{non_tex_preview} = 1; + $anshash->{student_ans} = "(see preview)"; + + $anshash->{preview_latex_string} = join('', ( + "
  • ", + join("
  • ",@student), + "
" + )); + + $anshash->{correct_ans_latex_string} = join('', ( + "
  • ", + join("
  • ",@correct), + "
" + )); + + $anshash->{correct_value} = $anshash->{original_correct_value}; + + return $anshash; } 1; diff --git a/macros/draggableSubsets.pl b/macros/draggableSubsets.pl index ab0d90fa4f..d60273f4f2 100644 --- a/macros/draggableSubsets.pl +++ b/macros/draggableSubsets.pl @@ -1,15 +1,34 @@ -################################################################ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2021 The WeBWorK Project, http://openwebwork.sf.net/ +# +# 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 + draggableSubsets.pl - + =head1 DESCRIPTION -draggableSubsets.pl helps the instructor create a drag-and-drop environment in which a pre-specified set of elements may be dragged to different "buckets", effectively partitioning the original set into subsets. + +This macro helps the instructor create a drag-and-drop environment in which a pre-specified set +of elements may be dragged to different "buckets", effectively partitioning the original set into subsets. =head1 TERMINOLOGY + An HTML element into or out of which other elements may be dragged will be called a "bucket". An HTML element which houses a collection of buckets will be called a "bucket pool". =head1 USAGE + To initialize a DraggableSubset bucket pool in a .pg problem, insert the line: $draggable = DraggableSubsets($full_set, $ans, Options1 => ..., Options2 => ...); @@ -22,8 +41,13 @@ =head1 USAGE within the BEGIN_TEXT / END_TEXT environment; -$full_set, e.g. ["statement1", "statement2", ...], is an array reference to the list of elements, given as strings, in the original full set. -$ans, e.g. [[1, 2, 3], [4, 5], ...], is an array reference to the list of array references corresponding to the correct answer which is a set of subsets. Each subset is specified via the indices of the elements according to their positions in $full_set, with the first element having index 0. +$full_set, e.g. ["statement1", "statement2", ...], is an array reference to the list of elements, given as strings, +in the original full set. + +$ans, e.g. [[1, 2, 3], [4, 5], ...], is an array reference to the list of array references +corresponding to the correct answer which is a set of subsets. +Each subset is specified via the indices of the elements according to their positions in $full_set, +with the first element having index 0. Available Options: DefaultSubsets => @@ -33,14 +57,16 @@ =head1 USAGE Their usage is explained in the example below. =head1 EXAMPLE + DOCUMENT(); loadMacros( - "PGstandard.pl", - "MathObjects.pl", - "draggableSubsets.pl", +"PGstandard.pl", +"MathObjects.pl", +"draggableSubsets.pl", ); TEXT(beginproblem()); + $D3 = [ "\(e\)", #0 "\(r\)", #1 @@ -50,7 +76,7 @@ =head1 EXAMPLE "\(sr^2\)", #5 ]; -$subgroup = "e, s"; +$subgroup = "e, s"; $subsets = [ [0, 3], @@ -60,39 +86,46 @@ =head1 EXAMPLE $draggable = DraggableSubsets( $D3, # full set. Square brackets must be used. -$subsets, # reference to array of arrays of indices, corresponding to correct set of subsets. Square brackets must be used. +# +$subsets, # reference to array of arrays of indices, corresponding to correct set of subsets. +# Square brackets must be used. +# DefaultSubsets => [ # default instructor-provided subsets. Default value = []. { - label => 'coset 1', # label of the bucket. - indices => [ 1, 3, 4, 5 ], # specifies pre-included elements in the bucket via their indices. - removable => 0 # specifies whether student may remove bucket. +label => 'coset 1', # label of the bucket. +indices => [ 1, 3, 4, 5 ], # specifies pre-included elements in the bucket via their indices. +removable => 0 # specifies whether student may remove bucket. }, { - label => 'coset 2', - indices => [ 0 ], - removable => 1 +label => 'coset 2', +indices => [ 0 ], +removable => 1 }, { - label => 'coset 3', - indices => [ 2 ], - removable => 1 +label => 'coset 3', +indices => [ 2 ], +removable => 1 } ], -# OrderedSubsets => 0, # OrderedSubsets => 0 means order of subsets does not matter. 1 means otherwise. (The order of elements within each subset never matters.) Default value = 0. -# AllowNewBuckets => 1, # AllowNewBuckets => 0 means no new buckets may be added by student. 1 means otherwise. Default value = 1. +# OrderedSubsets => 0, # means order of subsets does not matter. 1 means otherwise. +# (The order of elements within each subset never matters.) Default value = 0. +# +# AllowNewBuckets => 0, # means no new buckets may be added by student. 1 means otherwise. Default value = 1. ); -Context()->texStrings; +Context()->texStrings; # must be placed AFTER defining all DraggableSubsets. BEGIN_TEXT Let \[ G=D_3=\lbrace e,r,r^2, s,sr,sr^2 \rbrace \] -be the Dihedral group of order \(6\), where \(r\) is counter-clockwise rotation by \(2\pi/3\), and \(s\) is the reflection across the \(x\)-axis. +be the Dihedral group of order \(6\), where \(r\) is counter-clockwise rotation by \(2\pi/3\), +and \(s\) is the reflection across the \(x\)-axis. Partition \(G=D_3\) into $BBOLD right $EBOLD cosets of the subgroup -\(H=\lbrace $subgroup \rbrace\). Give your result by dragging the following elements into separate buckets, each corresponding to a coset. +\(H=\lbrace $subgroup \rbrace\). Give your result by dragging the following elements into separate buckets, +each corresponding to a coset. $PAR \{ $draggable->Print \} @@ -105,7 +138,9 @@ =head1 EXAMPLE ANS($draggable->cmp); ENDDOCUMENT(); + =cut + ################################################################ loadMacros( @@ -124,24 +159,24 @@ sub _draggableSubsets_init { package draggableSubsets; sub new { - my $self = shift; + my $self = shift; my $class = ref($self) || $self; # user arguments - my $set = shift; - my $subsets = shift; + my $set = shift; + my $subsets = shift; my %options = ( DefaultSubsets => [], OrderedSubsets => 0, AllowNewBuckets => 1, - @_ - ); + @_ + ); # end user arguments my $numProvided = scalar(@$set); my @order = main::shuffle($numProvided); my @unorder = main::invert(@order); - + my $shuffledSet = [ map {$set->[$_]} @order ]; my $defaultBuckets = $options{DefaultSubsets}; @@ -149,26 +184,26 @@ sub new { if (@$defaultBuckets) { for my $defaultBucket (@$defaultBuckets) { my $shuffledIndices = [ map {$unorder[$_]} @{ $defaultBucket->{indices} } ]; - my $default_shuffled_bucket = { - label => $defaultBucket->{label}, + my $default_shuffled_bucket = { + label => $defaultBucket->{label}, indices => $shuffledIndices, removable => $defaultBucket->{removable}, }; push(@$defaultShuffledBuckets, $default_shuffled_bucket); - } + } } else { - push(@$defaultShuffledBuckets, [ { - label => '', + push(@$defaultShuffledBuckets, [ { + label => '', indices => [ 0..$numProvided-1 ] } ]); } - + my $answerInputId = main::NEW_ANS_NAME() unless $self->{answerInputId}; my $ans_rule = main::NAMED_HIDDEN_ANS_RULE($answerInputId); my $dnd = new DragNDrop( - $answerInputId, - $shuffledSet, - $defaultShuffledBuckets, + $answerInputId, + $shuffledSet, + $defaultShuffledBuckets, AllowNewBuckets => $options{AllowNewBuckets}, ); @@ -188,19 +223,19 @@ sub new { $dnd->addBucket($indices->[0] != -1 ? $indices : [], label => $label, removable => $removable); } } - + my @shuffled_subsets_array = (); for my $subset ( @$subsets ) { my @shuffled_subset = map {$unorder[$_]} @$subset; push(@shuffled_subsets_array, @$subset != 0 ? main::Set(join(',', @shuffled_subset)) : main::Set()); } my $shuffled_subsets = main::List(@shuffled_subsets_array); - + $self = bless { set => $set, shuffledSet => $shuffledSet, numProvided => $numProvided, - order => \@order, + order => \@order, unordered => \@unorder, shuffled_subsets => $shuffled_subsets, answerInputId => $answerInputId, @@ -221,21 +256,25 @@ sub Print { if ($main::displayMode ne "TeX") { # HTML mode return join("\n", - '
', - $ans_rule, - $self->{dnd}->HTML, - '
', - '
', + '
', + $ans_rule, + $self->{dnd}->HTML, + '
', + '
', ); } else { # TeX mode - return $self->{dnd}->TeX; + return $self->{dnd}->TeX; } } sub cmp { my $self = shift; - return $self->{shuffled_subsets}->cmp(ordered => $self->{OrderedSubsets}, removeParens => 1, partialCredit => 1)->withPreFilter(sub {$self->prefilter(@_)})->withPostFilter(sub {$self->filter(@_)}); + + return $self->{shuffled_subsets} + ->cmp(ordered => $self->{OrderedSubsets}, removeParens => 1, partialCredit => 1) + ->withPreFilter(sub {$self->prefilter(@_)}) + ->withPostFilter(sub {$self->filter(@_)}); } sub prefilter { @@ -253,7 +292,7 @@ sub prefilter { } $anshash->{student_ans} = main::List(@studentAnsArray); - + return $anshash; } @@ -264,16 +303,16 @@ sub filter { my @student = ( $anshash->{original_student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); my @correct = ( $anshash->{correct_ans} =~ /({[^{}]*}|-?\d+)/g ); - $anshash->{correct_ans_latex_string} = join (",", map { - "\\{\\text{".join(",", (map { + $anshash->{correct_ans_latex_string} = join (",", map { + "\\{\\text{".join(",", (map { $_ != -1 ? $self->{shuffledSet}->[$_] : '' - } (split(',', $_ =~ s/{|}//gr)) ))."}\\}" + } (split(',', $_ =~ s/{|}//gr)) ))."}\\}" } @correct); - $anshash->{preview_latex_string} = join (",", map { - "\\{\\text{".join(",", (map { + $anshash->{preview_latex_string} = join (",", map { + "\\{\\text{".join(",", (map { $_ != -1 ? $self->{shuffledSet}->[$_] : '' - } (split(',', $_ =~ s/\(|\)//gr)) ))."}\\}" + } (split(',', $_ =~ s/\(|\)//gr)) ))."}\\}" } @student); $anshash->{student_ans} = "(see preview)"; From c24bf37d528d9c4a034a51aa1ea007c671119ba8 Mon Sep 17 00:00:00 2001 From: pschan-gh <31397850+pschan-gh@users.noreply.github.com> Date: Fri, 3 Sep 2021 19:14:28 +0800 Subject: [PATCH 013/134] remove trailing whitespace --- lib/DragNDrop.pm | 38 ++++++++++---------- macros/draggableProof.pl | 74 +++++++++++++++++++------------------- macros/draggableSubsets.pl | 68 +++++++++++++++++------------------ 3 files changed, 90 insertions(+), 90 deletions(-) diff --git a/lib/DragNDrop.pm b/lib/DragNDrop.pm index 038666206a..5163098beb 100644 --- a/lib/DragNDrop.pm +++ b/lib/DragNDrop.pm @@ -92,22 +92,22 @@ package DragNDrop; sub new { my $self = shift; my $class = ref($self) || $self; - + # 'id' of html tag corresponding to the answer blank. Must be unique to each pool of DragNDrop buckets my $answerInputId = shift; - + # array of all statements provided my $aggregateList = shift; - + # instructor-provided default buckets with pre-included statements encoded # by the array of corresponding statement indices my $defaultBuckets = shift; - + my %options = ( AllowNewBuckets => 0, @_ ); - + $self = bless { answerInputId => $answerInputId, bucketList => [], @@ -115,21 +115,21 @@ sub new { defaultBuckets => $defaultBuckets, %options, }, $class; - + return $self; } sub addBucket { my $self = shift; - + my $indices = shift; - + my %options = ( label => "", removable => 0, @_ ); - + my $bucket = { indices => $indices, list => [ map { $self->{aggregateList}->[$_] } @$indices ], @@ -138,15 +138,15 @@ sub addBucket { removable => $options{removable}, }; push(@{$self->{bucketList}}, $bucket); - + } sub HTML { my $self = shift; - + my $out = ''; $out .= "
"; - + # buckets from instructor-defined default settings for (my $i = 0; $i < @{$self->{defaultBuckets}}; $i++) { my $defaultBucket = $self->{defaultBuckets}->[$i]; @@ -158,14 +158,14 @@ sub HTML { } $out .= "
"; } - + # buckets from past answers for my $bucket ( @{$self->{bucketList}} ) { $out .= ""; - + return $out; } sub TeX { my $self = shift; - + my $out = ""; - + # default buckets; for (my $i = 0; $i < @{ $self->{defaultBuckets} }; $i++) { $out .= "\n"; my $defaultBucket = $self->{defaultBuckets}->[$i]; if ( @{$defaultBucket->{indices}} > 0 ) { - $out .= "\n\\hrule\n\\begin{itemize}"; + $out .= "\n\\hrule\n\\begin{itemize}"; for my $j ( @{$defaultBucket->{indices}} ) { $out .= "\n\\item[$j.]\n $self->{aggregateList}->[$j]"; } @@ -202,4 +202,4 @@ sub TeX { } return $out; } -1; \ No newline at end of file +1; diff --git a/macros/draggableProof.pl b/macros/draggableProof.pl index a16a6d8695..5c09a947ab 100644 --- a/macros/draggableProof.pl +++ b/macros/draggableProof.pl @@ -158,9 +158,9 @@ package draggableProof; sub new { my $self = shift; my $class = ref($self) || $self; - + my $proof = shift; - my $extra = shift; + my $extra = shift; my %options = ( SourceLabel => "Choose from these sentences:", TargetLabel => "Your Proof:", @@ -171,17 +171,17 @@ sub new { IrrelevancePenalty => 1, @_ ); - + my $lines = [ @$proof, @$extra ]; my $numNeeded = scalar(@$proof); my $numProvided = scalar(@$lines); my @order = main::shuffle($numProvided); my @unorder = main::invert(@order); my $shuffledLines = [ map {$lines->[$_]} @order ]; - + my $answerInputId = main::NEW_ANS_NAME() unless $self->{answerInputId}; my $ans_rule = main::NAMED_HIDDEN_ANS_RULE($answerInputId); - + my $dnd; if ($options{NumBuckets} == 2) { $dnd = new DragNDrop($answerInputId, $shuffledLines, @@ -206,16 +206,16 @@ sub new { ], AllowNewBuckets => 0); } - + my $proof = $options{NumBuckets} == 2 ? main::List( main::List(@unorder[$numNeeded .. $numProvided - 1]), main::List(@unorder[0..$numNeeded-1]) ) : main::List('('.join(',', @unorder[0..$numNeeded-1]).')'); - + my $extra = main::Set(@unorder[$numNeeded .. $numProvided - 1]); - + my $InferenceMatrix = $options{InferenceMatrix}; - + $self = bless { lines => $lines, shuffledLines => $shuffledLines, @@ -231,9 +231,9 @@ sub new { inferenceMatrix => $InferenceMatrix, %options, }, $class; - + my $previous = $main::inputs_ref->{$answerInputId} || ''; - + if ($previous eq "") { if ($self->{NumBuckets} == 2) { $dnd->addBucket([0..$numProvided-1], label => $options{'SourceLabel'}); @@ -244,7 +244,7 @@ sub new { } else { my @matches = ( $previous =~ /(\([^\(\)]*\)|-?\d+)/g ); if ($self->{NumBuckets} == 2) { - my $indices1 = [ split(',', @matches[0] =~ s/\(|\)//gr) ]; + my $indices1 = [ split(',', @matches[0] =~ s/\(|\)//gr) ]; $dnd->addBucket($indices1->[0] != -1 ? $indices1 : [], label => $options{'SourceLabel'}); my $indices2 = [ split(',', @matches[1] =~ s/\(|\)//gr) ]; $dnd->addBucket($indices2->[0] != -1 ? $indices2 : [], label => $options{'TargetLabel'}); @@ -253,7 +253,7 @@ sub new { $dnd->addBucket($indices1->[0] != -1 ? $indices1 : [], label => $options{'TargetLabel'}); } } - + return $self; } @@ -266,10 +266,10 @@ sub new { sub Levenshtein { my @ar1 = split /$_[2]/, $_[0]; my @ar2 = split /$_[2]/, $_[1]; - + my @dist = ([0 .. @ar2]); $dist[$_][0] = $_ for (1 .. @ar1); - + for my $i (0 .. $#ar1) { for my $j (0 .. $#ar2) { $dist[$i+1][$j+1] = main::min($dist[$i][$j+1] + 1, $dist[$i+1][$j] + 1, @@ -282,18 +282,18 @@ sub Levenshtein { sub DamerauLevenshtein { # Damerau–Levenshtein distance with adjacent transpositions # https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance - + my $discourse1 = shift; my $discourse2 = shift; my $delimiter = shift; my $numProvided = shift; - + my @ar1 = split /$delimiter/, $discourse1; my @ar2 = split /$delimiter/, $discourse2; - + my @da = (0) x $numProvided; my @d = (); - + my $maxdist = @ar1 + @ar2; for my $i (1 .. @ar1 + 1) { push(@d, [ (0) x (@ar2 + 2) ] ); @@ -329,9 +329,9 @@ sub DamerauLevenshtein { sub Print { my $self = shift; - + my $ans_rule = $self->{ans_rule}; - + if ($main::displayMode ne "TeX") { # HTML mode return join("\n", @@ -352,39 +352,39 @@ sub cmp { return $self->{proof} ->cmp(ordered => 1, removeParens => 1) ->withPreFilter(sub {$self->prefilter(@_)}) - ->withPostFilter(sub {$self->filter(@_)}); + ->withPostFilter(sub {$self->filter(@_)}); } sub prefilter { my $self = shift; my $anshash = shift; - + my $correctProcessed; - + $anshash->{original_correct_value} = $anshash->{correct_value}; - + if ($self->{NumBuckets} == 1) { $correctProcessed = $anshash->{correct_value} =~ s/\(|\)|\s*//gr; } elsif ($self->{NumBuckets} == 2) { my @matches = ( $anshash->{correct_value} =~ /(\([^\(\)]*\)|-?\d+)/g ); $correctProcessed = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; } - + $anshash->{correct_value} = main::List($correctProcessed); - + return $anshash; } sub filter { my $self = shift; my $anshash = shift; - + my @lines = @{$self->{lines}}; my @order = @{$self->{order}}; - + my $correct_value = $anshash->{correct_value}; my $actualAnswer; - + if ($self->{NumBuckets} == 1) { $actualAnswer = main::List( $anshash->{student_value} =~ s/\(|\)|\s*//gr ); } elsif ($self->{NumBuckets} == 2) { @@ -417,7 +417,7 @@ sub filter { } } $anshash->{score} = $inferenceScore / $total; - + my %invoked = map { $_ => 1 } split(',', $actualAnswer); foreach ( split(',', $self->{extra}->string =~ s/{|}|\s*//gr ) ) { if ( exists($invoked{$_}) ) { @@ -427,27 +427,27 @@ sub filter { } else { $anshash->{score} = $correct_value eq main::List($actualAnswer) ? 1 : 0; } - + my @correct = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $correct_value); my @student = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $actualAnswer); - + $anshash->{non_tex_preview} = 1; $anshash->{student_ans} = "(see preview)"; - + $anshash->{preview_latex_string} = join('', ( "
  • ", join("
  • ",@student), "
" )); - + $anshash->{correct_ans_latex_string} = join('', ( "
  • ", join("
  • ",@correct), "
" )); - + $anshash->{correct_value} = $anshash->{original_correct_value}; - + return $anshash; } 1; diff --git a/macros/draggableSubsets.pl b/macros/draggableSubsets.pl index d60273f4f2..db39ef814d 100644 --- a/macros/draggableSubsets.pl +++ b/macros/draggableSubsets.pl @@ -161,7 +161,7 @@ package draggableSubsets; sub new { my $self = shift; my $class = ref($self) || $self; - + # user arguments my $set = shift; my $subsets = shift; @@ -172,13 +172,13 @@ sub new { @_ ); # end user arguments - + my $numProvided = scalar(@$set); my @order = main::shuffle($numProvided); my @unorder = main::invert(@order); - + my $shuffledSet = [ map {$set->[$_]} @order ]; - + my $defaultBuckets = $options{DefaultSubsets}; my $defaultShuffledBuckets = []; if (@$defaultBuckets) { @@ -189,7 +189,7 @@ sub new { indices => $shuffledIndices, removable => $defaultBucket->{removable}, }; - push(@$defaultShuffledBuckets, $default_shuffled_bucket); + push(@$defaultShuffledBuckets, $default_shuffled_bucket); } } else { push(@$defaultShuffledBuckets, [ { @@ -197,18 +197,18 @@ sub new { indices => [ 0..$numProvided-1 ] } ]); } - - my $answerInputId = main::NEW_ANS_NAME() unless $self->{answerInputId}; + + my $answerInputId = main::NEW_ANS_NAME() unless $self->{answerInputId}; my $ans_rule = main::NAMED_HIDDEN_ANS_RULE($answerInputId); my $dnd = new DragNDrop( $answerInputId, $shuffledSet, $defaultShuffledBuckets, AllowNewBuckets => $options{AllowNewBuckets}, - ); - + ); + my $previous = $main::inputs_ref->{$answerInputId} || ''; - + if ($previous eq '') { for my $defaultBucket ( @$defaultShuffledBuckets ) { $dnd->addBucket($defaultBucket->{indices}, label => $defaultBucket->{label}); @@ -216,21 +216,21 @@ sub new { } else { my @matches = ( $previous =~ /(\([^\(\)]*\)|-?\d+)+/g ); for(my $i = 0; $i < @matches; $i++) { - my $match = @matches[$i] =~ s/\(|\)//gr; + my $match = @matches[$i] =~ s/\(|\)//gr; my $indices = [ split(',', $match) ]; my $label = $i < @$defaultShuffledBuckets ? $defaultShuffledBuckets->[$i]->{label} : ''; my $removable = $i < @$defaultShuffledBuckets ? $defaultShuffledBuckets->[$i]->{removable} : 1; $dnd->addBucket($indices->[0] != -1 ? $indices : [], label => $label, removable => $removable); } - } - + } + my @shuffled_subsets_array = (); for my $subset ( @$subsets ) { my @shuffled_subset = map {$unorder[$_]} @$subset; push(@shuffled_subsets_array, @$subset != 0 ? main::Set(join(',', @shuffled_subset)) : main::Set()); - } + } my $shuffled_subsets = main::List(@shuffled_subsets_array); - + $self = bless { set => $set, shuffledSet => $shuffledSet, @@ -244,15 +244,15 @@ sub new { OrderedSubsets => $options{OrderedSubsets}, AllowNewBuckets => $options{AllowNewBuckets}, }, $class; - + return $self; } sub Print { my $self = shift; - + my $ans_rule = $self->{ans_rule}; - + if ($main::displayMode ne "TeX") { # HTML mode return join("\n", @@ -264,13 +264,13 @@ sub Print { ); } else { # TeX mode - return $self->{dnd}->TeX; + return $self->{dnd}->TeX; } } sub cmp { - my $self = shift; - + my $self = shift; + return $self->{shuffled_subsets} ->cmp(ordered => $self->{OrderedSubsets}, removeParens => 1, partialCredit => 1) ->withPreFilter(sub {$self->prefilter(@_)}) @@ -278,10 +278,10 @@ sub cmp { } sub prefilter { - my $self = shift; my $anshash = shift; - - my @student = ( $anshash->{original_student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); - + my $self = shift; my $anshash = shift; + + my @student = ( $anshash->{original_student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); + my @studentAnsArray; for my $match ( @student ) { if ($match =~ /-1/) { @@ -290,33 +290,33 @@ sub prefilter { push(@studentAnsArray, main::Set($match =~ s/\(|\)//gr)); } } - + $anshash->{student_ans} = main::List(@studentAnsArray); - + return $anshash; } sub filter { - my $self = shift; my $anshash = shift; - + my $self = shift; my $anshash = shift; + my @order = @{ $self->{order} }; my @student = ( $anshash->{original_student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); my @correct = ( $anshash->{correct_ans} =~ /({[^{}]*}|-?\d+)/g ); - + $anshash->{correct_ans_latex_string} = join (",", map { "\\{\\text{".join(",", (map { $_ != -1 ? $self->{shuffledSet}->[$_] : '' } (split(',', $_ =~ s/{|}//gr)) ))."}\\}" } @correct); - + $anshash->{preview_latex_string} = join (",", map { "\\{\\text{".join(",", (map { $_ != -1 ? $self->{shuffledSet}->[$_] : '' } (split(',', $_ =~ s/\(|\)//gr)) ))."}\\}" } @student); - + $anshash->{student_ans} = "(see preview)"; - + return $anshash; } -1; \ No newline at end of file +1; From 64a87bd295b2b221961901c51427533e42d9345d Mon Sep 17 00:00:00 2001 From: pschan-gh <31397850+pschan-gh@users.noreply.github.com> Date: Sat, 4 Sep 2021 01:11:55 +0800 Subject: [PATCH 014/134] forces normalStrings context in answer checker --- macros/draggableProof.pl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/macros/draggableProof.pl b/macros/draggableProof.pl index 5c09a947ab..b5dfccadd6 100644 --- a/macros/draggableProof.pl +++ b/macros/draggableProof.pl @@ -361,6 +361,8 @@ sub prefilter { my $correctProcessed; + main::Context()->normalStrings; + $anshash->{original_correct_value} = $anshash->{correct_value}; if ($self->{NumBuckets} == 1) { @@ -425,7 +427,7 @@ sub filter { } } } else { - $anshash->{score} = $correct_value eq main::List($actualAnswer) ? 1 : 0; + $anshash->{score} = $correct_value eq $actualAnswer ? 1 : 0; } my @correct = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $correct_value); From c9e1ab483eb80278def3fda1ad4847d7b3871ce8 Mon Sep 17 00:00:00 2001 From: pschan-gh <31397850+pschan-gh@users.noreply.github.com> Date: Sat, 4 Sep 2021 01:39:56 +0800 Subject: [PATCH 015/134] compare answers as strings instead of lists for edit distance scoring --- macros/draggableProof.pl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/macros/draggableProof.pl b/macros/draggableProof.pl index b5dfccadd6..7cffe8c2b5 100644 --- a/macros/draggableProof.pl +++ b/macros/draggableProof.pl @@ -384,14 +384,14 @@ sub filter { my @lines = @{$self->{lines}}; my @order = @{$self->{order}}; - my $correct_value = $anshash->{correct_value}; + my $correct_value = $anshash->{correct_value} =~ s/\(|\)|\s*//gr; my $actualAnswer; if ($self->{NumBuckets} == 1) { - $actualAnswer = main::List( $anshash->{student_value} =~ s/\(|\)|\s*//gr ); + $actualAnswer = $anshash->{student_value} =~ s/\(|\)|\s*//gr; } elsif ($self->{NumBuckets} == 2) { my @matches = ( $anshash->{student_value} =~ /(\([^\(\)]*\)|-?\d+)/g ); - $actualAnswer = main::List( @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : '' ); + $actualAnswer = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; } if ($self->{Levenshtein} == 1) { $anshash->{score} = 1 - main::min(1, Levenshtein($correct_value, $actualAnswer, ',')/$self->{numNeeded}); @@ -427,7 +427,7 @@ sub filter { } } } else { - $anshash->{score} = $correct_value eq $actualAnswer ? 1 : 0; + $anshash->{score} = main::List($correct_value) eq main::List($actualAnswer) ? 1 : 0; } my @correct = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $correct_value); From 95ce3ecab4cf140a3907a88ee4583a2c4e7e9830 Mon Sep 17 00:00:00 2001 From: pschan-gh <31397850+pschan-gh@users.noreply.github.com> Date: Sat, 4 Sep 2021 22:57:48 +0800 Subject: [PATCH 016/134] Set defer option for jquery.nestable.js Fixed NEW_ANS_NAME in DragNDrop.pm POD Removed obsolete comment in POD of draggableProof.pl and draggableSubets.pl --- lib/DragNDrop.pm | 2 +- macros/draggableProof.pl | 4 ++-- macros/draggableSubsets.pl | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/DragNDrop.pm b/lib/DragNDrop.pm index 5163098beb..34b6617ffc 100644 --- a/lib/DragNDrop.pm +++ b/lib/DragNDrop.pm @@ -41,7 +41,7 @@ To initialize a bucket pool, do: my $bucket_pool = new DragNDrop($answerInputId, $aggregateList); $answerInputId is a unique identifier for the bucket_pool, it is recommended that -it be generated with NEW_HIDDEN_ANS_NAME. +it be generated with NEW_ANS_NAME. $aggregateList is a reference to an array of all "statements" intended to be draggable. e.g. $aggregateList = ['socrates is a man', 'all men are mortal', 'therefore socrates is mortal']. diff --git a/macros/draggableProof.pl b/macros/draggableProof.pl index 7cffe8c2b5..7046f9b72e 100644 --- a/macros/draggableProof.pl +++ b/macros/draggableProof.pl @@ -118,7 +118,7 @@ =head1 EXAMPLE # Default value = 1. ); -Context()->texStrings; # must be placed AFTER defining all DraggableSubsets. +Context()->texStrings; BEGIN_TEXT @@ -147,7 +147,7 @@ =head1 EXAMPLE sub _draggableProof_init { ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); - ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1); + ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1, { defer => undef }); ADD_CSS_FILE("js/apps/DragNDrop/dragndrop.css", 0); ADD_JS_FILE("js/apps/DragNDrop/dragndrop.js", 0, { defer => undef }); PG_restricted_eval("sub DraggableProof {new draggableProof(\@_)}"); diff --git a/macros/draggableSubsets.pl b/macros/draggableSubsets.pl index db39ef814d..7b03b5e49f 100644 --- a/macros/draggableSubsets.pl +++ b/macros/draggableSubsets.pl @@ -113,7 +113,7 @@ =head1 EXAMPLE # AllowNewBuckets => 0, # means no new buckets may be added by student. 1 means otherwise. Default value = 1. ); -Context()->texStrings; # must be placed AFTER defining all DraggableSubsets. +Context()->texStrings; BEGIN_TEXT @@ -150,7 +150,7 @@ =head1 EXAMPLE sub _draggableSubsets_init { ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); - ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1); + ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1, { defer => undef }); ADD_CSS_FILE("js/apps/DragNDrop/dragndrop.css", 0); ADD_JS_FILE("js/apps/DragNDrop/dragndrop.js", 0, { defer => undef }); PG_restricted_eval("sub DraggableSubsets {new draggableSubsets(\@_)}"); From e6aec07a87c2c9b159641f276df9b7af4c14eb68 Mon Sep 17 00:00:00 2001 From: pschan-gh <31397850+pschan-gh@users.noreply.github.com> Date: Sun, 5 Sep 2021 00:05:43 +0800 Subject: [PATCH 017/134] POD cleanup --- lib/DragNDrop.pm | 49 ++++++++--- macros/draggableProof.pl | 156 +++++++++++++++++----------------- macros/draggableSubsets.pl | 167 +++++++++++++++++++------------------ 3 files changed, 199 insertions(+), 173 deletions(-) diff --git a/lib/DragNDrop.pm b/lib/DragNDrop.pm index 34b6617ffc..d4fa06c246 100644 --- a/lib/DragNDrop.pm +++ b/lib/DragNDrop.pm @@ -31,39 +31,60 @@ An HTML element which houses a collection of buckets will be called a "bucket po =head1 USAGE Each macro aiming to implement drag-n-drop features must call at its initialization: -ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); -ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1); -ADD_CSS_FILE("js/apps/DragNDrop/dragndrop.css", 0); -ADD_JS_FILE("js/apps/DragNDrop/dragndrop.js", 0, { defer => undef }); + + ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); + ADD_JS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.js", 1, { defer => undef }); + ADD_CSS_FILE("js/apps/DragNDrop/dragndrop.css", 0); + ADD_JS_FILE("js/apps/DragNDrop/dragndrop.js", 0, { defer => undef }); To initialize a bucket pool, do: -my $bucket_pool = new DragNDrop($answerInputId, $aggregateList); + my $bucket_pool = new DragNDrop($answerInputId, $aggregateList); $answerInputId is a unique identifier for the bucket_pool, it is recommended that it be generated with NEW_ANS_NAME. $aggregateList is a reference to an array of all "statements" intended to be draggable. -e.g. $aggregateList = ['socrates is a man', 'all men are mortal', 'therefore socrates is mortal']. +Example: + + $aggregateList = ['socrates is a man', 'all men are mortal', 'therefore socrates is mortal']; + It is imperative that square brackets be used. ############################################################################## -OPTIONAL: DragNDrop($answerInputId, $aggregateList, AllowNewBuckets => 1); +OPTIONAL: + + DragNDrop($answerInputId, $aggregateList, AllowNewBuckets => 1); + allows student to create new buckets by clicking on a button. ############################################################################## To add a bucket to an existing pool $bucket_pool, do: -$bucket_pool->addBucket($indices); + + $bucket_pool->addBucket($indices); $indices is the reference to the array of indices corresponding to the statements in $aggregateList to be pre-included in the bucket. -For example, if the $aggregateList is ['Socrates is a man', 'all men are mortal', 'therefore Socrates is mortal'], -and the bucket consists of { 'Socrates is a man', 'therefore Socrates is mortal' }, then $indices = [0, 2]. + +For example, if the $aggregateList is: + + ['Socrates is a man', 'all men are mortal', 'therefore Socrates is mortal'] + +and the bucket consists of: + + { 'Socrates is a man', 'therefore Socrates is mortal' } + +then: + + $indices = [0, 2]. An empty array reference, e.g. $bucket_pool->addBucket([]), gives an empty bucket. ############################################################################### -OPTIONAL: $bucket_pool->addBucket($indices, label => 'Barrel', removable => 1) +OPTIONAL: + + $bucket_pool->addBucket($indices, label => 'Barrel', removable => 1) + puts the label 'Barrel' at the top of the bucket. With the removable option set to 1, the bucket may be removed by the student via the click of a "Remove" button at the bottom of the bucket. @@ -71,10 +92,12 @@ at the bottom of the bucket. ############################################################################### To output the bucket pool to HTML, call: -$bucket_pool->HTML + + $bucket_pool->HTML To output the bucket pool to LaTeX, call: -$bucket_pool->TeX + + $bucket_pool->TeX =head1 EXAMPLES diff --git a/macros/draggableProof.pl b/macros/draggableProof.pl index 7046f9b72e..6d54149b85 100644 --- a/macros/draggableProof.pl +++ b/macros/draggableProof.pl @@ -53,88 +53,90 @@ =head1 USAGE with no inclusion of any statement from $extra. The score is 0% otherwise. Available Options: -NumBuckets => 1 or 2 -SourceLabel => -TargetLabel => -Levenshtein => 0 or 1 -DamerauLevenshtein => 0 or 1 -InferenceMatrix => -IrrelevancePenalty => + + NumBuckets => 1 or 2 + SourceLabel => + TargetLabel => + Levenshtein => 0 or 1 + DamerauLevenshtein => 0 or 1 + InferenceMatrix => + IrrelevancePenalty => Their usage is explained in the example below. =head1 EXAMPLE -DOCUMENT(); -loadMacros( -"PGstandard.pl", -"MathObjects.pl", -"draggableProof.pl" -); - -TEXT(beginproblem()); - -$statements = [ -"All men are mortal.", #0 -"Socrates is a man.", #1 -"Socrates is mortal." #2 -]; - -$extra = [ -"Some animals are men.", -"Beauty is immortal.", -"Not all animals are men." -]; - -$draggable = DraggableProof( -$statements, -$extra, -NumBuckets => 2, # either 1 or 2. -SourceLabel => "Axioms", # label of first bucket if NumBuckets = 2. -# -TargetLabel => "Reasoning", -# label of second bucket if NumBuckets = 2, -# of the only bucket if NumBuckets = 1. -################################################################ -# Levenshtein => 1, -# If equal to 1, scoring is determined by the Levenshtein edit distance between student answer and correct answer. -################################################################ -# DamerauLevenshtein => 1, -# If equal to 1, scoring is determined by the Damerau-Levenshtein distance between student answer and correct answer. -# A pair of transposed adjacent statements is counted as two mistakes under Levenshtein scoring, -# but as one mistake under Damerau-Levenshtein scoring. -################################################################ -InferenceMatrix => [ -[0, 0, 1], -[0, 0, 1], -[0, 0, 0] -], -# (i, j)-entry is nonzero <=> statement i implies statement j. -# The score of each corresponding inference is weighted according to the value of the matrix entry. -################################################################ -IrrelevancePenalty => 1 # This option is processed only if the InferenceMatrix option is set. -# Penalty for each extraneous statement in the student answer is -# divided by the total number of inference points (i.e. sum of all entries in the InferenceMatrix). -# Default value = 1. -); - -Context()->texStrings; - -BEGIN_TEXT - -Show that Socrates is mortal by dragging the relevant $BBOLD Axioms $EBOLD -into the $BBOLD Reasoning $EBOLD box in an appropriate order. - -$PAR - -\{ $draggable->Print \} - -END_TEXT -Context()->normalStrings; - -ANS($draggable->cmp); - -ENDDOCUMENT(); + DOCUMENT(); + loadMacros( + "PGstandard.pl", + "MathObjects.pl", + "draggableProof.pl" + ); + + TEXT(beginproblem()); + + $statements = [ + "All men are mortal.", #0 + "Socrates is a man.", #1 + "Socrates is mortal." #2 + ]; + + $extra = [ + "Some animals are men.", + "Beauty is immortal.", + "Not all animals are men." + ]; + + $draggable = DraggableProof( + $statements, + $extra, + NumBuckets => 2, # either 1 or 2. + SourceLabel => "Axioms", # label of first bucket if NumBuckets = 2. + # + TargetLabel => "Reasoning", + # label of second bucket if NumBuckets = 2, + # of the only bucket if NumBuckets = 1. + # + # Levenshtein => 1, + # If equal to 1, scoring is determined by the Levenshtein edit distance between student answer and correct answer. + # + # DamerauLevenshtein => 1, + # If equal to 1, scoring is determined by the Damerau-Levenshtein distance between student answer and correct answer. + # A pair of transposed adjacent statements is counted as two mistakes under Levenshtein scoring, + # but as one mistake under Damerau-Levenshtein scoring. + # + InferenceMatrix => [ + [0, 0, 1], + [0, 0, 1], + [0, 0, 0] + ], + # (i, j)-entry is nonzero <=> statement i implies statement j. + # The score of each corresponding inference is weighted according to the value of the matrix entry. + # + IrrelevancePenalty => 1 + # This option is processed only if the InferenceMatrix option is set. + # Penalty for each extraneous statement in the student answer is + # divided by the total number of inference points (i.e. sum of all entries in the InferenceMatrix). + # Default value = 1. + ); + + Context()->texStrings; + + BEGIN_TEXT + + Show that Socrates is mortal by dragging the relevant $BBOLD Axioms $EBOLD + into the $BBOLD Reasoning $EBOLD box in an appropriate order. + + $PAR + + \{ $draggable->Print \} + + END_TEXT + Context()->normalStrings; + + ANS($draggable->cmp); + + ENDDOCUMENT(); =cut diff --git a/macros/draggableSubsets.pl b/macros/draggableSubsets.pl index 7b03b5e49f..49cefc9666 100644 --- a/macros/draggableSubsets.pl +++ b/macros/draggableSubsets.pl @@ -50,94 +50,95 @@ =head1 USAGE with the first element having index 0. Available Options: -DefaultSubsets => -OrderedSubsets => 0 or 1 -AllowNewBuckets => 0 or 1 + + DefaultSubsets => + OrderedSubsets => 0 or 1 + AllowNewBuckets => 0 or 1 Their usage is explained in the example below. =head1 EXAMPLE -DOCUMENT(); -loadMacros( -"PGstandard.pl", -"MathObjects.pl", -"draggableSubsets.pl", -); - -TEXT(beginproblem()); - -$D3 = [ -"\(e\)", #0 -"\(r\)", #1 -"\(r^2\)", #2 -"\(s\)", #3 -"\(sr\)", #4 -"\(sr^2\)", #5 -]; - -$subgroup = "e, s"; - -$subsets = [ -[0, 3], -[1, 4], -[2, 5] -]; - -$draggable = DraggableSubsets( -$D3, # full set. Square brackets must be used. -# -$subsets, # reference to array of arrays of indices, corresponding to correct set of subsets. -# Square brackets must be used. -# -DefaultSubsets => [ # default instructor-provided subsets. Default value = []. -{ -label => 'coset 1', # label of the bucket. -indices => [ 1, 3, 4, 5 ], # specifies pre-included elements in the bucket via their indices. -removable => 0 # specifies whether student may remove bucket. -}, -{ -label => 'coset 2', -indices => [ 0 ], -removable => 1 -}, -{ -label => 'coset 3', -indices => [ 2 ], -removable => 1 -} -], -# OrderedSubsets => 0, # means order of subsets does not matter. 1 means otherwise. -# (The order of elements within each subset never matters.) Default value = 0. -# -# AllowNewBuckets => 0, # means no new buckets may be added by student. 1 means otherwise. Default value = 1. -); - -Context()->texStrings; - -BEGIN_TEXT - -Let \[ -G=D_3=\lbrace e,r,r^2, s,sr,sr^2 \rbrace -\] -be the Dihedral group of order \(6\), where \(r\) is counter-clockwise rotation by \(2\pi/3\), -and \(s\) is the reflection across the \(x\)-axis. - -Partition \(G=D_3\) into $BBOLD right $EBOLD cosets of the subgroup -\(H=\lbrace $subgroup \rbrace\). Give your result by dragging the following elements into separate buckets, -each corresponding to a coset. - -$PAR -\{ $draggable->Print \} - -END_TEXT -Context()->normalStrings; - -# Answer Evaluation - -ANS($draggable->cmp); - -ENDDOCUMENT(); + DOCUMENT(); + loadMacros( + "PGstandard.pl", + "MathObjects.pl", + "draggableSubsets.pl", + ); + + TEXT(beginproblem()); + + $D3 = [ + "\(e\)", #0 + "\(r\)", #1 + "\(r^2\)", #2 + "\(s\)", #3 + "\(sr\)", #4 + "\(sr^2\)", #5 + ]; + + $subgroup = "e, s"; + + $subsets = [ + [0, 3], + [1, 4], + [2, 5] + ]; + + $draggable = DraggableSubsets( + $D3, # full set. Square brackets must be used. + # + $subsets, # reference to array of arrays of indices, corresponding to correct set of subsets. + # Square brackets must be used. + # + DefaultSubsets => [ # default instructor-provided subsets. Default value = []. + { + label => 'coset 1', # label of the bucket. + indices => [ 1, 3, 4, 5 ], # specifies pre-included elements in the bucket via their indices. + removable => 0 # specifies whether student may remove bucket. + }, + { + label => 'coset 2', + indices => [ 0 ], + removable => 1 + }, + { + label => 'coset 3', + indices => [ 2 ], + removable => 1 + } + ], + # OrderedSubsets => 0, # means order of subsets does not matter. 1 means otherwise. + # (The order of elements within each subset never matters.) Default value = 0. + # + # AllowNewBuckets => 0, # means no new buckets may be added by student. 1 means otherwise. Default value = 1. + ); + + Context()->texStrings; + + BEGIN_TEXT + + Let \[ + G=D_3=\lbrace e,r,r^2, s,sr,sr^2 \rbrace + \] + be the Dihedral group of order \(6\), where \(r\) is counter-clockwise rotation by \(2\pi/3\), + and \(s\) is the reflection across the \(x\)-axis. + + Partition \(G=D_3\) into $BBOLD right $EBOLD cosets of the subgroup + \(H=\lbrace $subgroup \rbrace\). Give your result by dragging the following elements into separate buckets, + each corresponding to a coset. + + $PAR + \{ $draggable->Print \} + + END_TEXT + Context()->normalStrings; + + # Answer Evaluation + + ANS($draggable->cmp); + + ENDDOCUMENT(); =cut From 33a154e7da19ff3a69a58f68863b7df52fd7e876 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Wed, 15 Sep 2021 11:37:07 -0700 Subject: [PATCH 018/134] Rename TikZImage as LaTeXImage --- lib/{TikZImage.pm => LaTeXImage.pm} | 2 +- macros/PGbasicmacros.pl | 4 ++-- macros/PGtikz.pl | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) rename lib/{TikZImage.pm => LaTeXImage.pm} (99%) diff --git a/lib/TikZImage.pm b/lib/LaTeXImage.pm similarity index 99% rename from lib/TikZImage.pm rename to lib/LaTeXImage.pm index 8c3bb06ebc..b3bc3e16fd 100644 --- a/lib/TikZImage.pm +++ b/lib/LaTeXImage.pm @@ -24,7 +24,7 @@ use Carp; use WeBWorK::PG::IO; use WeBWorK::PG::ImageGenerator; -package TikZImage; +package LaTeXImage; # The constructor (it takes no parameters) sub new { diff --git a/macros/PGbasicmacros.pl b/macros/PGbasicmacros.pl index 7768cdbf41..da7d275200 100644 --- a/macros/PGbasicmacros.pl +++ b/macros/PGbasicmacros.pl @@ -2914,7 +2914,7 @@ =head2 Macros for displaying images image($image, width => 100, height => 100, tex_size => 800, alt => 'alt text', extra_html_tags => 'style="border:solid black 1pt"'); -where C<$image> can be a local file path, URL, WWPlot object, or TikZImage object, +where C<$image> can be a local file path, URL, WWPlot object, or LaTeXImage object, C and C are pixel counts for HTML display, while C is per 1000 applied to linewidth (for example 800 leads to 0.8\linewidth) @@ -2979,7 +2979,7 @@ sub image { while(@image_list) { my $image_item = shift @image_list; $image_item = insertGraph($image_item) - if (ref $image_item eq 'WWPlot' || ref $image_item eq 'TikZImage' || ref $image_item eq 'PGtikz'); + if (ref $image_item eq 'WWPlot' || ref $image_item eq 'LaTeXImage' || ref $image_item eq 'PGtikz'); my $imageURL = alias($image_item)//''; $imageURL = ($envir{use_site_prefix})? $envir{use_site_prefix}.$imageURL : $imageURL; my $out = ""; diff --git a/macros/PGtikz.pl b/macros/PGtikz.pl index 5cf478ab2b..fe65e6098b 100644 --- a/macros/PGtikz.pl +++ b/macros/PGtikz.pl @@ -19,7 +19,7 @@ =head1 NAME =head1 DESCRIPTION -This is a convenience macro for utilizing the TikZImage object to insert TikZ +This is a convenience macro for utilizing the LaTeXImage object to insert TikZ images into problems. Create a TikZ image as follows: $image = createTikZImage(); @@ -35,8 +35,8 @@ =head1 DESCRIPTION =head1 DETAILED USAGE -There are several TikZImage parameters that may need to be set for the -TikZImage object return by createTikZImage to generate the desired image. +There are several LaTeXImage parameters that may need to be set for the +LaTeXImage object return by createTikZImage to generate the desired image. $image->tex() Add the tikz commands that define the image. This takes a single string parameter. It is @@ -102,9 +102,9 @@ sub _PGtikz_init { } package PGtikz; -our @ISA = qw(TikZImage); +our @ISA = qw(LaTeXImage); -# Not much needs to be done here. The real work is done in TikZImage.pm. +# Not much needs to be done here. The real work is done in LaTeXImage.pm. sub new { my $self = shift; my $class = ref($self) || $self; From 7a3c3a1d40c04f9b449cf00cf374dc5798fd70e0 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Wed, 15 Sep 2021 12:42:07 -0700 Subject: [PATCH 019/134] Make LaTeXImage.pl not be tikz-centric --- lib/LaTeXImage.pm | 32 ++++++++++++++++++++++++-------- macros/PGtikz.pl | 8 +++++--- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/LaTeXImage.pm b/lib/LaTeXImage.pm index b3bc3e16fd..b0483e6ce5 100644 --- a/lib/LaTeXImage.pm +++ b/lib/LaTeXImage.pm @@ -15,7 +15,7 @@ ################################################################################ # This is a Perl module which simplifies and automates the process of generating -# simple images using TikZ, and converting them into a web-useable format. Its +# simple images using LaTeX, and converting them into a web-useable format. Its # typical usage is via the macro PGtikz.pl and is documented there. use strict; @@ -31,7 +31,9 @@ sub new { my $class = shift; my $data = { tex => '', - tikzOptions => '', + environment => '', + envirOptions => '', + tikzOptions => '', # legacy tikzLibraries => '', texPackages => [], addToPreamble => '', @@ -61,14 +63,27 @@ sub new { # Accessors -# Set TikZ image code, not including begin and end tags, as a single -# string parameter. Works best single quoted. +# Set LaTeX image code as a single string parameter. Works best single quoted. sub tex { my $self = shift; return &$self('tex', @_); } -# Set TikZ picture options as a single string parameter. +# Set LaTeX environment as a single string parameter. +sub environment { + my $self = shift; + return &$self('environment', @_); +} + +# Set LaTeX environment options as a single string parameter. +sub envirOptions { + my $self = shift; + # legacy support for tikzOptions + return $self->tikzOptions if ($self->tikzOptions ne ''); + return &$self('envirOptions', @_); +} + +# Legacy support for tikzOptions sub tikzOptions { my $self = shift; return &$self('tikzOptions', @_); @@ -137,15 +152,16 @@ sub header { push(@output, "\\usetikzlibrary{" . $self->tikzLibraries . "}") if ($self->tikzLibraries ne ""); push(@output, $self->addToPreamble); push(@output, "\\begin{document}\n"); - push(@output, "\\begin{tikzpicture}"); - push(@output, "[" . $self->tikzOptions . "]") if ($self->tikzOptions ne ""); + push(@output, "\\begin{" , $self->environment . "}") if $self->environment; + push(@output, "[" . $self->envirOptions . "]") if ($self->environment && $self->envirOptions ne ""); + push(@output, "\n") if $self->environment; @output; } sub footer { my $self = shift; my @output = (); - push(@output, "\\end{tikzpicture}\n"); + push(@output, "\\end{" , $self->environment . "}\n") if $self->environment; push(@output, "\\end{document}\n"); @output; } diff --git a/macros/PGtikz.pl b/macros/PGtikz.pl index fe65e6098b..b3a649f7dc 100644 --- a/macros/PGtikz.pl +++ b/macros/PGtikz.pl @@ -44,11 +44,11 @@ =head1 DETAILED USAGE string. Escaping of special characters may be needed in some cases. - $image->tikzOptions() Add options that will be passed to + $image->envirOptions() Add options that will be passed to \begin{tikzpicture}. This takes a single string parameter. For example: - $image->tikzOptions( + $image->envirOptions( "x=.5cm,y=.5cm,declare function={f(\x)=sqrt(\x);}" ); @@ -104,12 +104,14 @@ sub _PGtikz_init { package PGtikz; our @ISA = qw(LaTeXImage); -# Not much needs to be done here. The real work is done in LaTeXImage.pm. +# Not much needs to be done here except flag this as needing the tikz environment wrapper. +# The real work is done in LaTeXImage.pm. sub new { my $self = shift; my $class = ref($self) || $self; my $image = $class->SUPER::new(@_); + $image->environment('tikzpicture'); $image->svgMethod($main::envir{tikzSVGMethod} // 'pdf2svg'); $image->convertOptions($main::envir{tikzConvertOptions} // {input => {},output => {}}); $image->SUPER::ext('pdf') if $main::displayMode eq 'TeX'; From 521bf12128286d0023b439eca74e65097c401fc5 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Wed, 15 Sep 2021 21:40:38 -0700 Subject: [PATCH 020/134] PGlateximage.pl for general LaTeX imagery --- lib/LaTeXImage.pm | 36 +++++---- lib/WeBWorK/PG/Translator.pm | 2 + macros/PGlateximage.pl | 138 +++++++++++++++++++++++++++++++++++ macros/PGtikz.pl | 17 +++-- 4 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 macros/PGlateximage.pl diff --git a/lib/LaTeXImage.pm b/lib/LaTeXImage.pm index b0483e6ce5..d167b323fb 100644 --- a/lib/LaTeXImage.pm +++ b/lib/LaTeXImage.pm @@ -32,7 +32,6 @@ sub new { my $data = { tex => '', environment => '', - envirOptions => '', tikzOptions => '', # legacy tikzLibraries => '', texPackages => [], @@ -69,20 +68,19 @@ sub tex { return &$self('tex', @_); } -# Set LaTeX environment as a single string parameter. +# Set an environment to surround the tex(). This can be a string naming the environment. +# Or it can be an array reference. The first element of this array should be the name of +# the environment. If there is a second element, it should be a string with options for +# the environment. This could be extended to support environments with multiple option +# fields that may use parentheses for delimiters. sub environment { my $self = shift; + # legacy support + return ['tikzpicture',$self->tikzOptions] if ($self->tikzOptions ne ''); + return [&$self('environment', @_), ''] if (ref(&$self('environment', @_)) ne 'ARRAY'); return &$self('environment', @_); } -# Set LaTeX environment options as a single string parameter. -sub envirOptions { - my $self = shift; - # legacy support for tikzOptions - return $self->tikzOptions if ($self->tikzOptions ne ''); - return &$self('envirOptions', @_); -} - # Legacy support for tikzOptions sub tikzOptions { my $self = shift; @@ -101,6 +99,11 @@ sub tikzLibraries { # element the package options). sub texPackages { my $self = shift; + # Legacy: if tikzpicture is the environment, make sure tikz package is included + if ($self->environment->[0] eq 'tikzpicture') { + return &$self('texPackages', ['tikz',@$_[0]]) if ref($_[0]) eq "ARRAY"; + return &$self('texPackages', ['tikz']); + } return &$self('texPackages', $_[0]) if ref($_[0]) eq "ARRAY"; return &$self('texPackages'); } @@ -145,23 +148,24 @@ sub header { my @xcolorOpts = grep { ref $_ eq "ARRAY" && $_->[0] eq "xcolor" && defined $_->[1] } @{$self->texPackages}; my $xcolorOpts = @xcolorOpts ? $xcolorOpts[0][1] : 'svgnames'; push(@output, "\\usepackage[$xcolorOpts]{xcolor}\n"); - push(@output, "\\usepackage{tikz}\n"); push(@output, map { "\\usepackage" . (ref $_ eq "ARRAY" && @$_ > 1 && $_->[1] ne "" ? "[$_->[1]]" : "") . "{" . (ref $_ eq "ARRAY" ? $_->[0] : $_) . "}\n" } grep { (ref $_ eq "ARRAY" && $_->[0] ne 'xcolor') || $_ ne 'xcolor' } @{$self->texPackages}); push(@output, "\\usetikzlibrary{" . $self->tikzLibraries . "}") if ($self->tikzLibraries ne ""); push(@output, $self->addToPreamble); push(@output, "\\begin{document}\n"); - push(@output, "\\begin{" , $self->environment . "}") if $self->environment; - push(@output, "[" . $self->envirOptions . "]") if ($self->environment && $self->envirOptions ne ""); - push(@output, "\n") if $self->environment; + if ($self->environment->[0]) { + push(@output, "\\begin{" , $self->environment->[0] . "}"); + push(@output, "[" . $self->environment->[1] . "]") if (defined $self->environment->[1] && $self->environment->[1] ne ""); + push(@output, "\n"); + } @output; } sub footer { my $self = shift; my @output = (); - push(@output, "\\end{" , $self->environment . "}\n") if $self->environment; + push(@output, "\\end{" , $self->environment->[0] . "}\n") if $self->environment->[0]; push(@output, "\\end{document}\n"); @output; } @@ -214,7 +218,7 @@ sub draw { if (-r "$working_dir/image.dvi") { $self->use_svgMethod($working_dir); } else { - warn "The dvi file was not created."; + warn "The dvi file was not created."; if (open(my $err_fh, "<", "$working_dir/latex.stdout")) { while (my $error = <$err_fh>) { warn $error; diff --git a/lib/WeBWorK/PG/Translator.pm b/lib/WeBWorK/PG/Translator.pm index 519e4f458e..5af571dd88 100644 --- a/lib/WeBWorK/PG/Translator.pm +++ b/lib/WeBWorK/PG/Translator.pm @@ -1873,6 +1873,8 @@ sub default_preprocess_code { $evalString =~ s/ENDDOCUMENT.*/ENDDOCUMENT();/s; # remove text after ENDDOCUMENT $evalString =~ s/\n\h*(.*)\h*->\h*BEGIN_TIKZ[\h;]*\n/\n$1->tex\(<\h*BEGIN_LATEX_IMAGE[\h;]*\n/\n$1->tex\(<texPackages([['xy','all']]); + $image->BEGIN_LATEX_IMAGE + \xymatrix{ A \ar[r] & B \ar[d] \\\\ + D \ar[u] & C \ar[l] } + END_LATEX_IMAGE + +The LaTeX code is in a perl interpolated heredoc, so you may need to be careful +with backslashes. In the above, \\\\ becomes \\, because a simple \\ would +become \. But \x and \a do not require escaping the backslash. (It would be +harmless to escape these too though.) + +If math content is wihtin the LaTeX code, delimit it with \(...\) instead of +with dollar signs. + +Then insert the image into the problem with + + image(insertGraph($image)); + +=head1 DETAILED USAGE + +There are several LaTeXImage parameters that may need to be set for the +LaTeXImage object return by createLaTeXImage to generate the desired image. + + $image->tex() Add the tex commands that define the image. + This takes a single string parameter. It is + generally best to use single quotes around the + string. Escaping of special characters may be + needed in some cases. + + $image->environment() Either a string naming an environment to wrap the + tex() in, or an array where the first element is + the name of an environment and an optional second + argument is a string with options for the environment. + For example: + $image->texPackages(['circuitikz']); + $image->environment(['circuitikz','scale=1.2, transform shape']); + + $image->tikzLibraries() Add additional tikz libraries to load. This + takes a single string parameter. + For example: + $image->tikzLibraries("arrows.meta,calc"); + + $image->texPackages() Add tex packages to load. This takes an array for + its parameter. Each element of this array should + either be the package name as a string, or an + array with two elements, the first of which is the + package name as a string and the second of which + is a string containing the options for the package. + For example: + $image->texPackages([ + "pgfplots", + ["hf-tikz", "customcolors"], + ["xcolor", "cmyk,table"] + ]); + + $image->addToPreamble() Additional commands to add to the TeX preamble. + This takes a single string parameter. + + $image->ext() Set the file type to be used for the image. + The valid image types are 'png', 'gif', 'svg', + and 'pdf'. The default is an 'svg' image. You + should determine if an 'svg' image works well with + the LaTeX code that you utilize. If not, then use + this method to change the exension to 'png' or + 'gif'. + + This macro sets the extension to 'pdf' when a + hardcopy is generated. + + $image->convertOptions() If ImageMagick's convert command is used to build + the output image (presently only done for 'png' + output) these input and output options will be + used. For example: + $image->convertOptions({ + input => {density => 300}, + output => {quality => 100, resize => "500x500"} + }); + For a complete list of options, see: + https://imagemagick.org/script/command-line-options.php + +=cut + +sub _PGlateximage_init { + main::PG_restricted_eval('sub createLaTeXImage { PGlateximage->new(@_); }'); +} + +package PGlateximage; +our @ISA = qw(LaTeXImage); + +# Not much needs to be done here. +# The real work is done in LaTeXImage.pm. +sub new { + my $self = shift; + my $class = ref($self) || $self; + + my $image = $class->SUPER::new(@_); + $image->svgMethod($main::envir{latexImageSVGMethod} // 'pdf2svg'); + $image->convertOptions($main::envir{latexImageConvertOptions} // {input => {},output => {}}); + $image->SUPER::ext('pdf') if $main::displayMode eq 'TeX'; + $image->SUPER::ext('tgz') if $main::displayMode eq 'PTX'; + $image->imageName($main::PG->getUniqueName($image->ext)); + + return bless $image, $class; +} + +sub ext { + my $self = shift; + my $ext = shift; + return $self->SUPER::ext($ext) if $ext && $main::displayMode ne 'TeX'; + return $self->SUPER::ext; +} + +1; diff --git a/macros/PGtikz.pl b/macros/PGtikz.pl index b3a649f7dc..3dadc6f36c 100644 --- a/macros/PGtikz.pl +++ b/macros/PGtikz.pl @@ -29,6 +29,13 @@ =head1 DESCRIPTION \draw (0,0) circle[radius=1.5]; END_TIKZ +The LaTeX code is in a perl interpolated heredoc, so you may need to be careful +with backslashes. In the above, \d does not require escaping the backslash. But +if the code needed a double backslash line break, you would need to use \\\\. + +If math content is within the LaTeX code, delimit it with \(...\) instead of +with dollar signs. + Then insert the image into the problem with image(insertGraph($image)); @@ -44,13 +51,11 @@ =head1 DETAILED USAGE string. Escaping of special characters may be needed in some cases. - $image->envirOptions() Add options that will be passed to + $image->tikzOptions() Add options that will be passed to \begin{tikzpicture}. This takes a single string parameter. For example: - $image->envirOptions( - "x=.5cm,y=.5cm,declare function={f(\x)=sqrt(\x);}" - ); + $image->tikzOptions( $image->tikzLibraries() Add additional tikz libraries to load. This takes a single string parameter. @@ -112,8 +117,8 @@ sub new { my $image = $class->SUPER::new(@_); $image->environment('tikzpicture'); - $image->svgMethod($main::envir{tikzSVGMethod} // 'pdf2svg'); - $image->convertOptions($main::envir{tikzConvertOptions} // {input => {},output => {}}); + $image->svgMethod($main::envir{latexImageSVGMethod} // 'pdf2svg'); + $image->convertOptions($main::envir{latexImageConvertOptions} // {input => {},output => {}}); $image->SUPER::ext('pdf') if $main::displayMode eq 'TeX'; $image->SUPER::ext('tgz') if $main::displayMode eq 'PTX'; $image->imageName($main::PG->getUniqueName($image->ext)); From c4fc334ea2f3e3cdacd176a5665b895431132475 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Wed, 15 Sep 2021 21:46:55 -0700 Subject: [PATCH 021/134] Test problems for PGlateximage.pl --- t/latex_image_test/latex_image_test1.pg | 41 +++++++++++++++++++++++ t/latex_image_test/latex_image_test2.pg | 44 +++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 t/latex_image_test/latex_image_test1.pg create mode 100644 t/latex_image_test/latex_image_test2.pg diff --git a/t/latex_image_test/latex_image_test1.pg b/t/latex_image_test/latex_image_test1.pg new file mode 100644 index 0000000000..109e42beed --- /dev/null +++ b/t/latex_image_test/latex_image_test1.pg @@ -0,0 +1,41 @@ +##DESCRIPTION +# TEST tikz from a pg problem +##ENDDESCRIPTION + +DOCUMENT(); + +loadMacros( + "PGstandard.pl", + "MathObjects.pl", + "PGlateximage.pl" +); + +TEXT(beginproblem()); + +############################################################## +# Setup +############################################################## + +$drawing = createLaTeXImage(); +$drawing->texPackages([['xy','all']]); +$drawing->BEGIN_LATEX_IMAGE +\xymatrix{ A \ar[r] & B \ar[d] \\\\ + D \ar[u] & C \ar[l] } +END_LATEX_IMAGE + +$path = insertGraph($drawing); + +Context("Numeric"); + +############################################################## +# Text +############################################################## + +BEGIN_TEXT +\{protect_underbar("path = $path")\}; +$BR alias = \{protect_underbar(alias($path))\} +$PAR image = \{image($path, width => 228, height => 114, tex_size => 400)\} +$PAR svg = \{embedSVG($path)\} +END_TEXT + +ENDDOCUMENT(); diff --git a/t/latex_image_test/latex_image_test2.pg b/t/latex_image_test/latex_image_test2.pg new file mode 100644 index 0000000000..686ad21551 --- /dev/null +++ b/t/latex_image_test/latex_image_test2.pg @@ -0,0 +1,44 @@ +##DESCRIPTION +# TEST tikz from a pg problem +##ENDDESCRIPTION + +DOCUMENT(); + +loadMacros( + "PGstandard.pl", + "MathObjects.pl", + "PGlateximage.pl" +); + +TEXT(beginproblem()); + +############################################################## +# Setup +############################################################## + +$drawing = createLaTeXImage(); +$drawing->texPackages(['circuitikz']); +$drawing->environment(['circuitikz','scale=1.2, transform shape']); +$drawing->BEGIN_LATEX_IMAGE +\draw (60,1) to [battery2, v_=\(V_{cc}\), name=B] ++(0,2); +\node[draw,red,circle,inner sep=4pt] at(B.left) {}; +\node[draw,red,circle,inner sep=4pt] at(B.right) {}; +END_LATEX_IMAGE + +$path = insertGraph($drawing); + +Context("Numeric"); + +############################################################## +# Text +############################################################## + +BEGIN_TEXT +\{protect_underbar("path = $path")\}; +$BR alias = \{protect_underbar(alias($path))\} +$PAR image = \{image($path, width => 228, height => 114, tex_size => 400)\} +$PAR svg = \{embedSVG($path)\} +END_TEXT + +ENDDOCUMENT(); + From ee1f194f9eb7942fb3f7f77a42430b995d018d63 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Thu, 16 Sep 2021 13:09:06 -0700 Subject: [PATCH 022/134] fix accidental edits --- macros/PGlateximage.pl | 2 +- macros/PGtikz.pl | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/macros/PGlateximage.pl b/macros/PGlateximage.pl index 606bf99063..59df97a77e 100644 --- a/macros/PGlateximage.pl +++ b/macros/PGlateximage.pl @@ -34,7 +34,7 @@ =head1 DESCRIPTION become \. But \x and \a do not require escaping the backslash. (It would be harmless to escape these too though.) -If math content is wihtin the LaTeX code, delimit it with \(...\) instead of +If math content is within the LaTeX code, delimit it with \(...\) instead of with dollar signs. Then insert the image into the problem with diff --git a/macros/PGtikz.pl b/macros/PGtikz.pl index 3dadc6f36c..851fd87f77 100644 --- a/macros/PGtikz.pl +++ b/macros/PGtikz.pl @@ -56,6 +56,8 @@ =head1 DETAILED USAGE string parameter. For example: $image->tikzOptions( + "x=.5cm,y=.5cm,declare function={f(\x)=sqrt(\x);}" + ); $image->tikzLibraries() Add additional tikz libraries to load. This takes a single string parameter. From cd07355abfb09da368e17e77c5b0a1c917283cb5 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Thu, 16 Sep 2021 13:16:01 -0700 Subject: [PATCH 023/134] change commentary around tikzOptions --- lib/LaTeXImage.pm | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/LaTeXImage.pm b/lib/LaTeXImage.pm index d167b323fb..055bb37dbf 100644 --- a/lib/LaTeXImage.pm +++ b/lib/LaTeXImage.pm @@ -32,7 +32,9 @@ sub new { my $data = { tex => '', environment => '', - tikzOptions => '', # legacy + # if tikzOptions is nonempty, then environment + # will effectively be ['tikzpicture', tikzOptions] + tikzOptions => '', tikzLibraries => '', texPackages => [], addToPreamble => '', @@ -73,15 +75,15 @@ sub tex { # the environment. If there is a second element, it should be a string with options for # the environment. This could be extended to support environments with multiple option # fields that may use parentheses for delimiters. +# If tikzOptions is nonempty, the input is ignored and output is ['tikzpicture',tikzOptions]. sub environment { my $self = shift; - # legacy support return ['tikzpicture',$self->tikzOptions] if ($self->tikzOptions ne ''); return [&$self('environment', @_), ''] if (ref(&$self('environment', @_)) ne 'ARRAY'); return &$self('environment', @_); } -# Legacy support for tikzOptions +# Set TikZ picture options as a single string parameter. sub tikzOptions { my $self = shift; return &$self('tikzOptions', @_); @@ -99,7 +101,7 @@ sub tikzLibraries { # element the package options). sub texPackages { my $self = shift; - # Legacy: if tikzpicture is the environment, make sure tikz package is included + # if tikzpicture is the environment, make sure tikz package is included if ($self->environment->[0] eq 'tikzpicture') { return &$self('texPackages', ['tikz',@$_[0]]) if ref($_[0]) eq "ARRAY"; return &$self('texPackages', ['tikz']); From 1cafae4f3f9645f4238b223bea502f3279a0dca2 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Thu, 16 Sep 2021 15:15:13 -0700 Subject: [PATCH 024/134] LaTeXImage.pm: load tikz package when tikzpicture environment implies --- lib/LaTeXImage.pm | 8 ++--- t/tikz_test/tikz_test3.pg | 72 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 t/tikz_test/tikz_test3.pg diff --git a/lib/LaTeXImage.pm b/lib/LaTeXImage.pm index 055bb37dbf..7b9daaed71 100644 --- a/lib/LaTeXImage.pm +++ b/lib/LaTeXImage.pm @@ -101,11 +101,6 @@ sub tikzLibraries { # element the package options). sub texPackages { my $self = shift; - # if tikzpicture is the environment, make sure tikz package is included - if ($self->environment->[0] eq 'tikzpicture') { - return &$self('texPackages', ['tikz',@$_[0]]) if ref($_[0]) eq "ARRAY"; - return &$self('texPackages', ['tikz']); - } return &$self('texPackages', $_[0]) if ref($_[0]) eq "ARRAY"; return &$self('texPackages'); } @@ -150,6 +145,9 @@ sub header { my @xcolorOpts = grep { ref $_ eq "ARRAY" && $_->[0] eq "xcolor" && defined $_->[1] } @{$self->texPackages}; my $xcolorOpts = @xcolorOpts ? $xcolorOpts[0][1] : 'svgnames'; push(@output, "\\usepackage[$xcolorOpts]{xcolor}\n"); + # Load tikz if environment is tikzpicture, but not if texPackages contains tikz already + my %istikzused = map {ref $_ eq "ARRAY" ? ($_->[0] => ($_->[0] eq 'tikz')) : ($_ => ($_ eq 'tikz'))} @{$self->texPackages}; + push(@output, "\\usepackage{tikz}\n") if ($self->environment->[0] eq 'tikzpicture' && !$istikzused{'tikz'}); push(@output, map { "\\usepackage" . (ref $_ eq "ARRAY" && @$_ > 1 && $_->[1] ne "" ? "[$_->[1]]" : "") . "{" . (ref $_ eq "ARRAY" ? $_->[0] : $_) . "}\n" } grep { (ref $_ eq "ARRAY" && $_->[0] ne 'xcolor') || $_ ne 'xcolor' } @{$self->texPackages}); diff --git a/t/tikz_test/tikz_test3.pg b/t/tikz_test/tikz_test3.pg new file mode 100644 index 0000000000..0f686dc8d8 --- /dev/null +++ b/t/tikz_test/tikz_test3.pg @@ -0,0 +1,72 @@ +##DESCRIPTION +# TEST tikz from a pgml problem +##ENDDESCRIPTION + +DOCUMENT(); + +loadMacros( + "PGstandard.pl", + "MathObjects.pl", + "PGML.pl", + "PGtikz.pl" +); + +TEXT(beginproblem()); + +############################################################## +# Setup +############################################################## + +$a = random(1, 4); +$b = random(3, 6); +$c = random(5, 8); +$d = random(7, 10); + +$tikz_code = <texPackages([["pgfplots"]]); +$drawing->addToPreamble("\pgfplotsset{compat=1.15}"); +$drawing->tikzOptions("main_node/.style={circle,fill=blue!20,draw,minimum size=1em,inner sep=3pt}"); +$drawing->tex($tikz_code); + + +$path = insertGraph($drawing); + +Context("Numeric"); + +############################################################## +# Text +############################################################## + +BEGIN_PGML +path = [@ protect_underbar($path) @] +[@ $BR @]* +alias = [@ protect_underbar(alias($path)) @]* + +image = [@ image($path, width => 100, tex_size => 400) @]* + +svg = [@ embedSVG($path) @]* +END_PGML + +ENDDOCUMENT(); From 167aa660cc1cc87bf88f4659429a3894aa5b3b85 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Thu, 16 Sep 2021 15:50:43 -0700 Subject: [PATCH 025/134] more clear code to check if tikz used Co-authored-by: Glenn Rice <47527406+drgrice1@users.noreply.github.com> --- lib/LaTeXImage.pm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/LaTeXImage.pm b/lib/LaTeXImage.pm index 7b9daaed71..aec9db1b4c 100644 --- a/lib/LaTeXImage.pm +++ b/lib/LaTeXImage.pm @@ -146,8 +146,9 @@ sub header { my $xcolorOpts = @xcolorOpts ? $xcolorOpts[0][1] : 'svgnames'; push(@output, "\\usepackage[$xcolorOpts]{xcolor}\n"); # Load tikz if environment is tikzpicture, but not if texPackages contains tikz already - my %istikzused = map {ref $_ eq "ARRAY" ? ($_->[0] => ($_->[0] eq 'tikz')) : ($_ => ($_ eq 'tikz'))} @{$self->texPackages}; - push(@output, "\\usepackage{tikz}\n") if ($self->environment->[0] eq 'tikzpicture' && !$istikzused{'tikz'}); + push(@output, "\\usepackage{tikz}\n") + if ($self->environment->[0] eq 'tikzpicture' && + !grep { (ref $_ eq "ARRAY" && $_->[0] eq 'tikz') || $_ eq 'tikz' } @{$self->texPackages}); push(@output, map { "\\usepackage" . (ref $_ eq "ARRAY" && @$_ > 1 && $_->[1] ne "" ? "[$_->[1]]" : "") . "{" . (ref $_ eq "ARRAY" ? $_->[0] : $_) . "}\n" } grep { (ref $_ eq "ARRAY" && $_->[0] ne 'xcolor') || $_ ne 'xcolor' } @{$self->texPackages}); From c81f1a5b03f2b223b5fabd4f3bd8e8bb3091453b Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Thu, 21 Oct 2021 14:06:33 -0700 Subject: [PATCH 026/134] add point tool to GraphTool --- macros/parserGraphTool.pl | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/macros/parserGraphTool.pl b/macros/parserGraphTool.pl index 1703311883..65031a39fa 100644 --- a/macros/parserGraphTool.pl +++ b/macros/parserGraphTool.pl @@ -53,12 +53,16 @@ =head1 DESCRIPTION =head1 GRAPH OBJECTS -There are four types of graph objects that the students can graph. Lines, circles, parabolas, -and fills (or shading of a region). The syntax for each of these objects to pass to the -GraphTool constructor is summarized as follows. Each object must be enclosed in braces. The -first element in the braces must be the name of the object. The following elements in the +There are five types of graph objects that the students can graph. Points, lines, circles, +parabolas, and fills (or shading of a region). The syntax for each of these objects to pass to +the GraphTool constructor is summarized as follows. Each object must be enclosed in braces. +The first element in the braces must be the name of the object. The following elements in the braces depend on the type of element. +For points the name "point" must be followed by the coordinates. For example: + + "{point,(3,5)}" + For lines the name "line" must be followed by the word "solid" or "dashed" to indicate if the line is expected to be drawn solid or dashed. That is followed by two distinct points on the line. For example: @@ -147,8 +151,8 @@ =head1 OPTIONS This is an array of tools that will be made available for students to use in the graph tool. The order the tools are listed here will also be the order the tools are presented in the graph -tool button box. All of the tools that may be included are listed in the default options above. -Note that the case of the tool names must match what is shown. +tool button box. All of the tools that may be included are listed in the default options above, +except for the "PointTool". Note that the case of the tool names must match what is shown. =item staticObjects (Default: staticObjects => []) @@ -179,7 +183,8 @@ sub _parserGraphTool_init { ADD_CSS_FILE("js/vendor/jsxgraph/jsxgraph.css"); ADD_CSS_FILE("js/apps/GraphTool/graphtool.css"); ADD_JS_FILE("js/vendor/jsxgraph/jsxgraphcore.js", 0, { defer => undef }); - ADD_JS_FILE("js/apps/GraphTool/graphtool.min.js", 0, { defer => undef }); + #ADD_JS_FILE("js/apps/GraphTool/graphtool.min.js", 0, { defer => undef }); + ADD_JS_FILE("js/apps/GraphTool/graphtool.js", 0, { defer => undef }); main::PG_restricted_eval('sub GraphTool { parser::GraphTool->new(@_) }'); } @@ -190,6 +195,7 @@ package parser::GraphTool; our @ISA = qw(Value::List); our %contextStrings = ( + point => {}, line => {}, circle => {}, parabola => {}, @@ -216,6 +222,7 @@ sub new { ticksDistanceX => 2, ticksDistanceY => 2, minorTicksX => 1, minorTicksY => 1, availableTools => [ + "PointTool", "LineTool", "CircleTool", "VerticalParabolaTool", @@ -228,6 +235,17 @@ sub new { } our %graphObjectTikz = ( + point => { + code => sub { + my $self = shift; + my ($x, $y) = @{$_->{data}[2]{data}}; + my $point = "($x,$y)"; + return ("\\draw[thick,blue,fill=blue!30] $point circle[radius=5pt];", [ + $point, + sub { return ($_[0] - $x)**2 + ($_[1] - $y)**2; } + ]); + } + }, line => { code => sub { my $self = shift; @@ -446,7 +464,7 @@ sub ans_rule { "($self->{bBox}[0],$self->{bBox}[3]) rectangle ($self->{bBox}[2],$self->{bBox}[1]);\n"; - # Graph the lines, circles, and parabolas. + # Graph the points, lines, circles, and parabolas. if (@{$self->{staticObjects}}) { my $obj = $self->SUPER::new($self->{context}, @{$self->{staticObjects}}); From 1ba5dac6049983c043c153ebe0296f3d3b00a537 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Fri, 22 Oct 2021 11:52:39 -0700 Subject: [PATCH 027/134] Update macros/parserGraphTool.pl Co-authored-by: Glenn Rice <47527406+drgrice1@users.noreply.github.com> --- macros/parserGraphTool.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/parserGraphTool.pl b/macros/parserGraphTool.pl index 65031a39fa..0b9aba2d89 100644 --- a/macros/parserGraphTool.pl +++ b/macros/parserGraphTool.pl @@ -238,7 +238,7 @@ sub new { point => { code => sub { my $self = shift; - my ($x, $y) = @{$_->{data}[2]{data}}; + my ($x, $y) = @{$_->{data}[1]{data}}; my $point = "($x,$y)"; return ("\\draw[thick,blue,fill=blue!30] $point circle[radius=5pt];", [ $point, From 63a0e0283bfb283b9eabd8e92798c25f578cb6fc Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Fri, 22 Oct 2021 11:53:27 -0700 Subject: [PATCH 028/134] Update macros/parserGraphTool.pl Co-authored-by: Glenn Rice <47527406+drgrice1@users.noreply.github.com> --- macros/parserGraphTool.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macros/parserGraphTool.pl b/macros/parserGraphTool.pl index 0b9aba2d89..13e55f3de3 100644 --- a/macros/parserGraphTool.pl +++ b/macros/parserGraphTool.pl @@ -240,7 +240,7 @@ sub new { my $self = shift; my ($x, $y) = @{$_->{data}[1]{data}}; my $point = "($x,$y)"; - return ("\\draw[thick,blue,fill=blue!30] $point circle[radius=5pt];", [ + return ("\\draw[line width=4pt,blue,fill=red] $point circle[radius=5pt];", [ $point, sub { return ($_[0] - $x)**2 + ($_[1] - $y)**2; } ]); From 5b4e59a80266a71b6004b5a1a0d3e161aa7d8528 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Sat, 23 Oct 2021 15:02:55 -0700 Subject: [PATCH 029/134] development cleanup --- macros/parserGraphTool.pl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/macros/parserGraphTool.pl b/macros/parserGraphTool.pl index 13e55f3de3..7325c39d6c 100644 --- a/macros/parserGraphTool.pl +++ b/macros/parserGraphTool.pl @@ -183,8 +183,7 @@ sub _parserGraphTool_init { ADD_CSS_FILE("js/vendor/jsxgraph/jsxgraph.css"); ADD_CSS_FILE("js/apps/GraphTool/graphtool.css"); ADD_JS_FILE("js/vendor/jsxgraph/jsxgraphcore.js", 0, { defer => undef }); - #ADD_JS_FILE("js/apps/GraphTool/graphtool.min.js", 0, { defer => undef }); - ADD_JS_FILE("js/apps/GraphTool/graphtool.js", 0, { defer => undef }); + ADD_JS_FILE("js/apps/GraphTool/graphtool.min.js", 0, { defer => undef }); main::PG_restricted_eval('sub GraphTool { parser::GraphTool->new(@_) }'); } @@ -222,7 +221,6 @@ sub new { ticksDistanceX => 2, ticksDistanceY => 2, minorTicksX => 1, minorTicksY => 1, availableTools => [ - "PointTool", "LineTool", "CircleTool", "VerticalParabolaTool", From 0339c0f4a97d417efc4971323e857580aad60147 Mon Sep 17 00:00:00 2001 From: thetravisweber Date: Sat, 23 Oct 2021 19:52:43 -0700 Subject: [PATCH 030/134] fix: spelling error --- lib/WeBWorK/PG/Translator.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/WeBWorK/PG/Translator.pm b/lib/WeBWorK/PG/Translator.pm index 519e4f458e..4257444723 100644 --- a/lib/WeBWorK/PG/Translator.pm +++ b/lib/WeBWorK/PG/Translator.pm @@ -132,7 +132,7 @@ sub evaluate_modules { Loads extra packages for modules that contain more than one package. Works in conjunction with evaluate_modules. It is assumed that the file containing the extra packages (along with the base -pachage name which is the same as the name of the file minus the .pm extension) has already been +package name which is the same as the name of the file minus the .pm extension) has already been loaded using evaluate_modules =cut From 956bf63cd85f0233167302bc48624b86d2add5f0 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sun, 24 Oct 2021 16:25:07 -0500 Subject: [PATCH 031/134] Add an option to the graph tool to not show the coordinate hints in the lower left corner. --- macros/parserGraphTool.pl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/macros/parserGraphTool.pl b/macros/parserGraphTool.pl index 7325c39d6c..e73a9e086f 100644 --- a/macros/parserGraphTool.pl +++ b/macros/parserGraphTool.pl @@ -146,6 +146,10 @@ =head1 OPTIONS These restrict the x coordinate and y coordinate of points that can be graphed to being multiples of the respective parameter. These values must be greater than zero. +=item showCoordinateHints (Default: showCoordinateHints => 1) + +Set this to 0 to disable the display of the coordinates in the lower right corner of the graph. + =item availableTools (Default: availableTools => [ "LineTool", "CircleTool", "VerticalParabolaTool", "HorizontalParabolaTool", "FillTool", "SolidDashTool" ]) @@ -220,6 +224,7 @@ sub new { gridX => 1, gridY => 1, snapSizeX => 1, snapSizeY => 1, ticksDistanceX => 2, ticksDistanceY => 2, minorTicksX => 1, minorTicksY => 1, + showCoordinateHints => 1, availableTools => [ "LineTool", "CircleTool", @@ -513,6 +518,7 @@ sub ans_rule { "staticObjects: '" . join(',', @{$self->{staticObjects}}) . "'," . "snapSizeX: $self->{snapSizeX}," . "snapSizeY: $self->{snapSizeY}," . + "showCoordinateHints: $self->{showCoordinateHints}," . "customGraphObjects: {$customGraphObjects}," . "customTools: {$customTools}," . "availableTools: ['" . join("','", @{$self->{availableTools}}) . "']," . From 18951f7a521bb2b6077a05ea08954d6d3fb8bc45 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Thu, 11 Nov 2021 07:49:20 -0500 Subject: [PATCH 032/134] Don't allow entryType change to modify original Value::Type entries --- macros/contextInequalities.pl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/macros/contextInequalities.pl b/macros/contextInequalities.pl index 8ce011c825..6e209ce3f9 100644 --- a/macros/contextInequalities.pl +++ b/macros/contextInequalities.pl @@ -380,7 +380,7 @@ sub _check { unless $self->{lop}{isInequality} && $self->{rop}{isInequality}; $self->Error("Inequalities combined by '%s' must both use the same variable",$self->{bop}) unless $self->{lop}{varName} eq $self->{rop}{varName}; - $self->{type} = Value::Type("Interval",2); + $self->{type} = $Value::Type{interval}; $self->{varName} = $self->{lop}{varName}; $self->{isInequality} = 1; } @@ -400,7 +400,7 @@ sub _check { unless $self->{lop}{isInequality} && $self->{rop}{isInequality}; $self->Error("Inequalities combined by '%s' must both use the same variable",$self->{bop}) unless $self->{lop}{varName} eq $self->{rop}{varName}; - $self->{type} = Value::Type("Interval",2); + $self->{type} = $Value::Type{interval}; $self->{varName} = $self->{lop}{varName}; $self->{isInequality} = 1; } @@ -840,7 +840,7 @@ package Inequalities::MakeInterval; sub new { my $self = shift; - $self = $self->SUPER::new(@_); + $self = Value::Interval->new(@_); $self = $self->demote if $self->classMatch("Inequality"); return $self; } @@ -878,7 +878,7 @@ sub _check { my $entryType = $self->typeRef->{entryType}; return unless $entryType->{name} =~ m/^(unknown|Interval|Set|Union)$/; foreach my $x (@{$self->{coords}}) {return unless $x->{isInequality}}; - $entryType->{name} = "Inequality"; + $self->typeRef->{entryType} = Value::Type("Inequality", $entryType->{length}, $entryType->{entryType}); } } From 3bf990ffa82294b9f210e6567bfb9ff3ab0f9037 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Mon, 8 Nov 2021 18:12:20 -0600 Subject: [PATCH 033/134] Attempt to fix MathQuill issues with mixed numbers, units, and scientific notation. For this switch to inserting mathquill responses on the PG side of things, and add a special precedence that is used only for student answers and only when MathQuill is enabled for the answer. The mathquill latex answers are now stored in KEPT_EXTRA_ANSWERS instead of as an additional response appended to the answer. This requires changes to MathQuill and for webwork2 (or the front end) to pass in an environment variable via the translation options that tells PG that MathQuill is enabled. These changes are in a corresponding pull request to webwork2. Special handling of MultiAnswer's is implemented. It is now possible to disable a single part of a multi answer, instead of only having the option to disable all parts. One of the upsides of adding mathquill responses here is that we have access to an answer value's context. One use is implemented here. That is to make MathQuill automatically disable the rootsAreExponents option if the root function is available in the context for an answer. --- lib/Parser.pm | 4 ++- lib/Parser/Context.pm | 2 +- lib/Parser/Context/Default.pm | 1 + lib/Value/AnswerChecker.pm | 8 ++++- lib/Value/WeBWorK.pm | 2 ++ macros/PG.pl | 66 +++++++++++++++++++++++++++++++++++ macros/contextFraction.pl | 2 +- 7 files changed, 81 insertions(+), 4 deletions(-) diff --git a/lib/Parser.pm b/lib/Parser.pm index d3a249932c..7bb207d944 100644 --- a/lib/Parser.pm +++ b/lib/Parser.pm @@ -284,7 +284,9 @@ sub Op { } else { my $def = $context->operators->resolveDef(' '); $name = $def->{string} if defined($NAME) and ($NAME eq ' ' or $NAME eq $def->{space}); - $self->pushOperator($name,$op->{precedence}); + $self->pushOperator($name, + $context->flag("parseMathQuill") && defined $op->{mq_precedence} + ? $op->{mq_precedence} : $op->{precedence}); } } elsif (($ref && $NAME ne ' ') || $self->state ne 'fn') {$self->Op($NAME,$ref)} } diff --git a/lib/Parser/Context.pm b/lib/Parser/Context.pm index ce1a317324..73e876d24c 100644 --- a/lib/Parser/Context.pm +++ b/lib/Parser/Context.pm @@ -206,7 +206,7 @@ sub usePrecedence { /^Standard/i and do { $self->operators->set( - ' *' => {precedence => 3}, + ' *' => {precedence => 3, mq_precedence => 2.9}, '* ' => {precedence => 3}, ' /' => {precedence => 3}, '/ ' => {precedence => 3}, diff --git a/lib/Parser/Context/Default.pm b/lib/Parser/Context/Default.pm index fc7ed84e25..8c5073d96e 100644 --- a/lib/Parser/Context/Default.pm +++ b/lib/Parser/Context/Default.pm @@ -244,6 +244,7 @@ $flags = { allowWrongArgCount => 0, # 1 = numbers need not be correct parseAlternatives => 0, # 1 = allow parsing of alternative tokens in the context convertFullWidthCharacters => 0, # 1 = convert Unicode full width characters to ASCII positions + useMathQuill => 0, }; ############################################################################ diff --git a/lib/Value/AnswerChecker.pm b/lib/Value/AnswerChecker.pm index f5ddb665ff..173c5693fd 100644 --- a/lib/Value/AnswerChecker.pm +++ b/lib/Value/AnswerChecker.pm @@ -132,9 +132,12 @@ sub cmp_parse { # Parse and evaluate the student answer # $ans->score(0); # assume failure + $context->flags->set(parseMathQuill => $context->flag("useMathQuill") && + (!defined $context->{answerHash}{mathQuillOpts} || $context->{answerHash}{mathQuillOpts} !~ /^\s*disabled\s*$/i)); $ans->{student_value} = $ans->{student_formula} = Parser::Formula($ans->{student_ans}); $ans->{student_value} = Parser::Evaluate($ans->{student_formula}) if defined($ans->{student_formula}) && $ans->{student_formula}->isConstant; + $context->flags->set(parseMathQuill => 0); # # If it parsed OK, save the output forms and check if it is correct @@ -590,7 +593,10 @@ sub ans_collect { $entry = $ans->{original_student_ans}; $ans->{student_formula} = $ans->{student_value} = undef unless $entry =~ m/\S/; } - my $result = $data->[$i][$j]->cmp(@ans_cmp_defaults)->evaluate($entry); + # Pass the mathQuillOpts on to each entry to ensure that the correct parsing is used for each entry. + # This really only needs to know if MathQuill is disabled or not, but it is more efficient to just pass on the reference. + # The value is safely ignored if $ans->{mathQuillOpts} does not match /^\s*disabled\s*$/i. + my $result = $data->[$i][$j]->cmp(@ans_cmp_defaults, mathQuillOpts => $ans->{mathQuillOpts})->evaluate($entry); $OK &= entryCheck($result,$blank); push(@row,$result->{student_formula}); entryMessage($result->{ans_message},$errors,$i,$j,$rows,$cols); diff --git a/lib/Value/WeBWorK.pm b/lib/Value/WeBWorK.pm index 6fd1075e21..a21f1e0acc 100644 --- a/lib/Value/WeBWorK.pm +++ b/lib/Value/WeBWorK.pm @@ -93,6 +93,7 @@ my @wwEvalFields = qw( useBaseTenLog parseAlternatives convertFullWidthCharacters + useMathQuill ); sub Parser::Context::copy { @@ -114,6 +115,7 @@ sub Parser::Context::copy { useBaseTenLog => $ww->{useBaseTenLog}, parseAlternatives => $ww->{parseAlternatives}, convertFullWidthCharacters => $ww->{convertFullWidthCharacters}, + useMathQuill => $ww->{useMathQuill}, ); $context->{format}{number} = $ww->{numFormatDefault} if $ww->{numFormatDefault} ne ''; $context->update if $context->flag('parseAlternatives',0) != $self->flag('parseAlternatives',0); diff --git a/macros/PG.pl b/macros/PG.pl index b8678562db..4ac1c524d9 100644 --- a/macros/PG.pl +++ b/macros/PG.pl @@ -407,6 +407,72 @@ sub EXTEND_RESPONSE { # for radio buttons and checkboxes } sub ENDDOCUMENT { + # Request MathQuill javascript and css, and insert MathQuill responses if MathQuill is enabled. + # Add responses to each answer's response group that store the latex form of the students' + # answers and add corresponding hidden input boxes to the page. + if ($envir{useMathQuill}) { + ADD_CSS_FILE("node_modules/mathquill/dist/mathquill.css"); + ADD_CSS_FILE("js/apps/MathQuill/mqeditor.css"); + ADD_JS_FILE("node_modules/mathquill/dist/mathquill.js", 0, { defer => undef }); + ADD_JS_FILE("js/apps/MathQuill/mqeditor.js", 0, { defer => undef }); + + for my $answerLabel (keys %{$PG->{PG_ANSWERS_HASH}}) { + my $answerGroup = $PG->{PG_ANSWERS_HASH}{$answerLabel}; + my $mq_opts = $answerGroup->{ans_eval}{rh_ans}{mathQuillOpts} // {}; + + # This is a special case for multi answers. This is used to obtain mathQuillOpts set + # specifically for individual parts. + my $multiAns; + my $part; + if ($answerGroup->{ans_eval}{rh_ans}{type} =~ /MultiAnswer(?:\((\d*)\))?/) { + # This will only be set if singleResult is not enabled. + $part = $1; + # The MultiAnswer object passes itself as the first optional argument to the evaluator it creates. + # Loop through the evaluators to find it. + for (@{$answerGroup->{ans_eval}{evaluators}}) { + $multiAns = $_->[1] if (ref($_->[1]) && ref($_->[1]) eq "parser::MultiAnswer"); + } + # Pass the mathQuillOpts of the main MultiAnswer object on to each part + # (unless the part already has the option set). + if (defined $multiAns) { + for (@{$multiAns->{cmp}}) { + $_->rh_ans(mathQuillOpts => $mq_opts) unless defined $_->{rh_ans}{mathQuillOpts}; + } + } + } + + next if $mq_opts =~ /^\s*disabled\s*$/i; + + my $response_obj = $answerGroup->response_obj; + my $responseCount = -1; + for my $response ($response_obj->response_labels) { + ++$responseCount; + next if ref($response_obj->{responses}{$response}); + + my $ansHash = defined $multiAns + ? $multiAns->{cmp}[$part // $responseCount]{rh_ans} + : $answerGroup->{ans_eval}{rh_ans}; + my $mq_part_opts = $ansHash->{mathQuillOpts} // $mq_opts; + next if $mq_part_opts =~ /^\s*disabled\s*$/i; + + my $context = $ansHash->{correct_value}->context if $ansHash->{correct_value}; + $mq_part_opts->{rootsAreExponents} = 0 + if $context && $context->functions->get('root') && !defined $mq_part_opts->{rootsAreExponents}; + + my $name = "MaThQuIlL_$response"; + my $answer_value = ''; + $answer_value = $inputs_ref->{$name} if defined($inputs_ref->{$name}); + RECORD_EXTRA_ANSWERS($name); + $answer_value = encode_pg_and_html($answer_value); + 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!!)); + } + } + } + # check that answers match # gather up PG_FLAGS elements diff --git a/macros/contextFraction.pl b/macros/contextFraction.pl index 2c5585a93a..912b21cc89 100644 --- a/macros/contextFraction.pl +++ b/macros/contextFraction.pl @@ -237,7 +237,7 @@ sub Init { "/ " => {class => "context::Fraction::BOP::divide"}, " /" => {class => "context::Fraction::BOP::divide"}, "u-" => {class => "context::Fraction::UOP::minus"}, - " " => {precedence => 2.8, string => ' *'}, + " " => {precedence => 2.8, mq_precedence => 3, string => ' *'}, " *" => {class => "context::Fraction::BOP::multiply", precedence => 2.8}, # precedence is lower to get proper parens in string() and TeX() calls " " => {precedence => 2.7, associativity => 'left', type => 'bin', string => ' ', From 80812b77e445baabdc98a29ed25a56fd876af048 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 12 Nov 2021 20:08:23 -0600 Subject: [PATCH 034/134] Add a special parsing case for units when MathQuill is enabled for an answer. This allows parentheses around the numerator and denominator of a unit expression in that case. It is only enabled when parsing the student answer and MathQuill is enabled. --- lib/Parser/Legacy/NumberWithUnits.pm | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/Parser/Legacy/NumberWithUnits.pm b/lib/Parser/Legacy/NumberWithUnits.pm index 49247724f9..82b6b72734 100644 --- a/lib/Parser/Legacy/NumberWithUnits.pm +++ b/lib/Parser/Legacy/NumberWithUnits.pm @@ -80,15 +80,20 @@ sub new { # Find the units for the formula and split that off # sub splitUnits { + my $string = shift; + my $parseMathQuill = shift; my $aUnit = '(?:'.getUnitNames().')(?:\s*(?:\^|\*\*)\s*[-+]?\d+)?'; - my $unitPattern = $aUnit.'(?:\s*[/* ]\s*'.$aUnit.')*'; + my $unitPattern = $parseMathQuill + ? '\(?\s*'.$aUnit.'(?:\s*[* ]\s*'.$aUnit.')*\s*\)?' + : $aUnit.'(?:\s*[/* ]\s*'.$aUnit.')*'; + $unitPattern = $unitPattern.'(?:\/'.$unitPattern.')?' if $parseMathQuill; my $unitSpace = "($aUnit) +($aUnit)"; - my $string = shift; my ($num,$units) = $string =~ m!^(.*?(?:[)}\]0-9a-z]|\d\.))\s*($unitPattern)\s*$!; if ($units) { while ($units =~ s/$unitSpace/$1*$2/) {}; $units =~ s/ //g; $units =~ s/\*\*/^/g; + $units =~ s/^\(?([^\(\)]*)\)?\/\(?([^\(\)]*)\)?$/$1\/$2/g if $parseMathQuill; } return ($num,$units); @@ -161,7 +166,9 @@ sub cmp_parse { # Check that the units are defined and legal # - my ($num,$units) = splitUnits($ans->{student_ans}); + my ($num,$units) = splitUnits($ans->{student_ans}, + $ans->{correct_value}{context} && $ans->{correct_value}->context->flag('useMathQuill') && + (!defined $ans->{mathQuillOpts} || $ans->{mathQuillOpts} !~ /^\s*disabled\s*$/i)); unless (defined($num) && defined($units) && $units ne '') { $self->cmp_Error($ans,"Your answer doesn't look like ".lc($self->cmp_class)); return $ans; From bce91f0bc33a735d8a2dd10812753436ae68f0c4 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Tue, 16 Nov 2021 15:44:53 -0600 Subject: [PATCH 035/134] Set the defaults in pg_defaults.yml.dist to be the same as they are in site.conf.dits, and add the external program tar which is used by TikZImage.pm. Delete the commented out code in lib/WeBWorK/PG/IO.pm that was replaced with new code in this pull request, and the commented out obsolete code in lib/PGcore.pm. Run perltidy on PGEnvironment.pm using the same settings we use for ww3. That is a new file, so it should start clean. Also update the license in that file. Revert the changes to lib/chromatic/color.c. Why was that file changed? --- .gitignore | 2 +- conf/pg_defaults.yml.dist | 11 ++++++----- lib/PGEnvironment.pm | 30 +++++++++--------------------- lib/PGcore.pm | 1 - lib/WeBWorK/PG/IO.pm | 5 +---- 5 files changed, 17 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 9b77860e70..0a2e6dad2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ lib/chromatic/color -conf/*.yml \ No newline at end of file +conf/*.yml diff --git a/conf/pg_defaults.yml.dist b/conf/pg_defaults.yml.dist index e8d66400e2..fe31876a56 100644 --- a/conf/pg_defaults.yml.dist +++ b/conf/pg_defaults.yml.dist @@ -11,8 +11,9 @@ options: cp: /bin/cp mv: /bin/mv rm: /bin/rm - latex: /Library/TeX/texbin/latex - pdflatex: /Library/TeX/texbin/pdflatex - dvisvgm: /Library/TeX/texbin/dvisvgm - pdf2svf: /usr/local/bin/pdf2svg - convert: /usr/local/bin/convert + tar: /bin/tar + latex: /usr/bin/latex --no-shell-escape + pdflatex: /usr/bin/pdflatex --no-shell-escape + dvisvgm: /usr/bin/dvisvgm + pdf2svf: /usr/bin/pdf2svg + convert: /usr/bin/convert diff --git a/lib/PGEnvironment.pm b/lib/PGEnvironment.pm index 0b3749f50a..90270eb98f 100644 --- a/lib/PGEnvironment.pm +++ b/lib/PGEnvironment.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-2021 The WeBWorK Project, http://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 @@ -33,30 +32,21 @@ Otherwise, defaults are loaded from PG_ROOT/conf/pg_defaults.yml =cut - - - my $ce; my $pg_dir; use YAML::XS qw/LoadFile/; - BEGIN { eval { - require WeBWorK::CourseEnvironment; - # WeBWorK::CourseEnvironment->import(); - $ce = WeBWorK::CourseEnvironment->new({webwork_dir=>$ENV{WEBWORK_ROOT}}); - 1; + require WeBWorK::CourseEnvironment; + # WeBWorK::CourseEnvironment->import(); + $ce = WeBWorK::CourseEnvironment->new({ webwork_dir => $ENV{WEBWORK_ROOT} }); } or do { my $error = $@; $pg_dir = $ENV{PG_ROOT}; die "The environmental variable PG_ROOT must be a directory" unless -d $pg_dir; - - # Module load failed. You could recover, try loading - # an alternate module, die with $error... - # whatever's appropriate }; } @@ -64,13 +54,12 @@ sub new { my ($invocant, @rest) = @_; my $class = ref($invocant) || $invocant; - my $self = { - }; + my $self = {}; if (defined($ce)) { - $self->{webworkDirs} = $ce->{webworkDirs}; + $self->{webworkDirs} = $ce->{webworkDirs}; $self->{externalPrograms} = $ce->{externalPrograms}; - $self->{pg_dir} = $ce->{pg_dir}; + $self->{pg_dir} = $ce->{pg_dir}; } else { ## load from an conf file; $self->{pg_dir} = $ENV{PG_ROOT}; @@ -79,7 +68,7 @@ sub new { die "Cannot read the configuration file found at $defaults_file" unless -r $defaults_file; my $options = LoadFile($defaults_file); - $self->{webworkDirs} = $options->{webworkDirs}; + $self->{webworkDirs} = $options->{webworkDirs}; $self->{externalPrograms} = $options->{externalPrograms}; } @@ -89,5 +78,4 @@ sub new { return $self; } - -1; \ No newline at end of file +1; diff --git a/lib/PGcore.pm b/lib/PGcore.pm index 8061522c59..446010a40b 100755 --- a/lib/PGcore.pm +++ b/lib/PGcore.pm @@ -35,7 +35,6 @@ use PGloadfiles; use AnswerHash; use WeBWorK::PG::IO(); # don't important any command directly use Tie::IxHash; -# use WeBWorK::Debug; # removing webwork2 modules. This doesn't appear to be called at all. use MIME::Base64(); use PGUtil(); use Encode qw(encode_utf8 decode_utf8); diff --git a/lib/WeBWorK/PG/IO.pm b/lib/WeBWorK/PG/IO.pm index e018e6aa12..4d30da0bed 100644 --- a/lib/WeBWorK/PG/IO.pm +++ b/lib/WeBWorK/PG/IO.pm @@ -17,9 +17,6 @@ use utf8; #binmode(STDOUT,":encoding(UTF-8)"); #binmode(STDIN,":encoding(UTF-8)"); #binmode(INPUT,":encoding(UTF-8)"); -# my $pg_envir = new WeBWorK::CourseEnvironment({ -# webwork_dir => $ENV{WEBWORK_ROOT}, -# }); my $pg_envir = new PGEnvironment(); @@ -269,7 +266,7 @@ sub createDirectory { # This is needed for the subroutine below. It is copied from WeBWorK::Utils. # Note: if a place for common code is ever created this should go there. -sub path_is_subdir($$;$) { +sub path_is_subdir { my ($path, $dir, $allow_relative) = @_; unless ($path =~ /^\//) { From a35d2b94e6ba7ba94a8d14411344768d852f1879 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Tue, 13 Apr 2021 14:27:03 -0400 Subject: [PATCH 036/134] pre-cache WWSafe for use with standaloneRenderer --- lib/WeBWorK/PG/Translator.pm | 238 ++++++++++------------------------- 1 file changed, 69 insertions(+), 169 deletions(-) diff --git a/lib/WeBWorK/PG/Translator.pm b/lib/WeBWorK/PG/Translator.pm index 4257444723..0a0b121409 100644 --- a/lib/WeBWorK/PG/Translator.pm +++ b/lib/WeBWorK/PG/Translator.pm @@ -79,6 +79,68 @@ sets or PG macro files. Use this way to imitate the behavior of C =cut BEGIN { + my $ce = new WeBWorK::CourseEnvironment({ + webwork_dir => $ENV{WEBWORK_ROOT}, + }); + + # This safe compartment is used to read the large macro files such as + # PG.pl, PGbasicmacros.pl and PGanswermacros and cache the results so that + # future calls have preloaded versions of these large files. This saves + # approximately 200ms per render. + my $safeCache = new WWSafe; + + my @modules = @{ $ce->{pg}->{modules} }; + my $ra_included_modules = []; + + foreach my $module_packages_ref (@modules) { + my ( $module, @extra_packages ) = @$module_packages_ref; + + # the first item is the main package + $module =~ s/\.pm$//; + eval "package Main; require $module; import $module;"; + warn "Failed to evaluate module $module: $@" if $@; + push @$ra_included_modules, "\%${module}::"; + + # the remaining items are "extra" packages + foreach (@extra_packages) { + s/\.pm$//; + import $_; + warn "Failed to evaluate module $_: $@" if $@; + push @$ra_included_modules, "\%${_}::"; + } + } + + $safeCache->share_from( 'main', $ra_included_modules ); + + my $store_mask = $safeCache->mask(); + $safeCache->mask(Opcode::empty_opset()); + my $safe_cmpt_package_name = $safeCache->root(); + + # these days, the only unrestricted load is from PG.pl -- include it in pre-cache + my $filePath = $WeBWorK::Constants::PG_DIRECTORY . "/macros/PG.pl"; + my $init_subroutine_name = "${safe_cmpt_package_name}::_PG_init"; + + my $errors = ""; + if (-r $filePath ) { + my $rdoResult = $safeCache->rdo($filePath); + $errors .= "\nThere were problems compiling the file:\n $filePath\n $@\n" if $@; + } else { + $errors .= "Can't open file $filePath for reading\n"; + } + $safeCache -> mask($store_mask); + + # intialize PG.pl + my $init_subroutine = eval { \&{$init_subroutine_name} }; + my $macro_file_loaded = ref($init_subroutine) =~ /CODE/; + if ( $macro_file_loaded ) { + &$init_subroutine(); + } + $errors .= "\nUnknown error. Unable to load $filePath\n" if ($errors eq '' and not $macro_file_loaded); + die "Translator.pm [BEGIN errors]: $errors\n" if $errors; + + # stash the cache in a global variable + $WeBWorK::Translator::safeCache = $safeCache; + # allows the use of strict within macro packages. sub be_strict { require 'ww_strict.pm'; @@ -117,14 +179,7 @@ sub evaluate_modules { push @{$self->{ra_included_modules}}, "\%${_}::"; } } -# old code for runtime_use -# if ( -r "${courseScriptsDirectory}${module_name}.pm" ) { -# eval(qq! require "${courseScriptsDirectory}${module_name}.pm"; import ${module_name};! ); -# warn "Errors in including the module ${courseScriptsDirectory}$module_name.pm $@" if $@; -# } else { -# eval(qq! require "${module_name}.pm"; import ${module_name};! ); -# warn "Errors in including either the module $module_name.pm or ${courseScriptsDirectory}${module_name}.pm $@" if $@; -# } + =head2 load_extra_packages Usage: $obj -> load_extra_packages('AlgParserWithImplicitExpand', @@ -154,13 +209,14 @@ sub load_extra_packages{ =head2 new Creates the translator object. - =cut sub new { my $class = shift; - my $safe_cmpt = new WWSafe; #('PG_priv'); + # it is safe for all requests to use the safeCache because the perl + # process is forked for each render request (thanks async!) + my $safe_cmpt = $WeBWorK::Translator::safeCache; my $self = { preprocess_code => \&default_preprocess_code, postprocess_code => \&default_postprocess_code, @@ -300,167 +356,11 @@ sub initialize { #$safe_cmpt -> share('$rf_restricted_eval'); use strict; - $safe_cmpt -> share_from('main', $self->{ra_included_modules} ); - # the above line will get changed when we fix the PG modules thing. heh heh. + # $safe_cmpt -> share_from('main', $self->{ra_included_modules} ); + # the above line will get changed when we fix the PG modules thing. heh heh. [FIXED] + # now the default included modules from defaults.config are loaded at BEGIN } -# -- Preloading has not been used for some time. -# It was a method of speeding up the creation of a new safe compartment -# It may be worth saving this for a while as a reference -# ################################################################ -# # Preloading the macro files -# ################################################################ -# -# # Preloading the macro files can significantly speed up the translation process. -# # Files are read into a separate safe compartment (typically Safe::Root1::) -# # This means that all non-explicit subroutine references and those explicitly prefixed by main:: -# # are prefixed by Safe::Root1:: -# # These subroutines (but not the constants) are then explicitly exported to the current -# # safe compartment Safe::Rootx:: -# -# # Although it is not large, it is important to import PG.pl into the -# # cached safe compartment as well. This is because a call in PGbasicmacros.pl to NEW_ANSWER_NAME -# # which is defined in PG.pl would actually be a call to Safe::Root1::NEW_ANSWER_NAME since -# # PGbasicmacros is compiled into the SAfe::Root1:: compartment. If PG.pl has only been compiled into -# # the current Safe compartment, this call will fail. There are many calls between PG.pl, -# # PGbasicmacros and PGanswermacros so it is easiest to have all of them defined in Safe::Root1:: -# # There subroutines are still available in the current safe compartment. -# # Sharing the hash %Safe::Root1:: in the current compartment means that any references to Safe::Root1::NEW_ANSWER_NAME -# # will be found as long as NEW_ANSWER_NAME has been defined in Safe::Root1:: -# # -# # Constants and references to subroutines in other macro files have to be handled carefully in preloaded files. -# # For example a call to main::display_matrix (defined in PGmatrixmacros.pl) will become Safe::Root1::display_matrix and -# # will fail since PGmatrixmacros.pl is loaded only into the current safe compartment Safe::Rootx::. -# # The value of main:: has to be evaluated at runtime in order to make this work. Hence something like -# # my $temp_code = eval('\&main::display_matrix'); -# # &$temp_code($matrix_object_to_be_displayed); -# # in PGanswermacros.pl -# # would reference the run time value of main::, namely Safe::Rootx:: -# # There may be a clearer or more efficient way to obtain the runtime value of main:: -# -# -# sub pre_load_macro_files { -# time_it("Begin pre_load_macro_files"); -# my $self = shift; -# my $cached_safe_cmpt = shift; -# my $dirName = shift; -# my @fileNameList = @_; -# my $debugON = 0; # This helps with debugging the loading of macro files -# -# ################################################################ -# # prepare safe_cache -# ################################################################ -# $cached_safe_cmpt -> share_from('WeBWorK::PG::Translator', -# [keys %Translator_shared_subroutine_hash]); -# $cached_safe_cmpt -> share_from('WeBWorK::PG::IO', -# [keys %IO_shared_subroutine_hash]); -# no strict; -# local(%envir) = %{ $self ->{envir} }; -# $cached_safe_cmpt -> share('%envir'); -# use strict; -# $cached_safe_cmpt -> share_from('main', $self->{ra_included_modules} ); -# $cached_safe_cmpt->mask(Opcode::full_opset()); # allow no operations -# $cached_safe_cmpt->permit(qw( :default )); -# $cached_safe_cmpt->permit(qw(time)); # used to determine whether solutions are visible. -# $cached_safe_cmpt->permit(qw( atan2 sin cos exp log sqrt )); -# -# # just to make sure we'll deny some things specifically -# $cached_safe_cmpt->deny(qw(entereval)); -# $cached_safe_cmpt->deny(qw ( unlink symlink system exec )); -# $cached_safe_cmpt->deny(qw(print require)); -# -# ################################################################ -# # read in macro files -# ################################################################ -# -# foreach my $fileName (@fileNameList) { -# # determine whether the file has already been loaded by checking for -# # subroutine named _${macro_file_name}_init -# my $macro_file_name = $fileName; -# $macro_file_name =~s/\.pl//; # trim off the extension -# $macro_file_name =~s/\.pg//; # sometimes the extension is .pg (e.g. CAPA files) -# my $init_subroutine_name = "_${macro_file_name}_init"; -# my $macro_file_loaded = defined(&{$cached_safe_cmpt->root."::$init_subroutine_name"}) ? 1 : 0; -# -# -# if ( $macro_file_loaded ) { -# warn "$macro_file_name is already loaded" if $debugON; -# }else { -# warn "pre_load_macro_files: reading and evaluating $macro_file_name from $dirName/$fileName" ; -# ### read in file -# my $filePath = "$dirName/$fileName"; -# local(*MACROFILE); -# local($/); -# $/ = undef; # allows us to treat the file as a single line -# open(MACROFILE, "<:encoding(UTF-8)", $filePath) || die "Cannot open file: $filePath"; -# my $string = ; -# close(MACROFILE); -# -# -# ################################################################ -# # Evaluate macro files -# ################################################################ -# # FIXME The following hardwired behavior should be modifiable -# # either in the procedure call or in global.conf: -# # -# # PG.pl, IO.pl are loaded without restriction; -# # all other files are loaded with restriction -# # -# # construct a regex that matches only these three files safely -# my @unrestricted_files = (); # no longer needed? FIXME w/PG.pl IO.pl/; -# my $unrestricted_files = join("|", map { quotemeta } @unrestricted_files); -# -# my $store_mask; -# if ($fileName =~ /^($unrestricted_files)$/) { -# $store_mask = $cached_safe_cmpt->mask(); -# $cached_safe_cmpt ->mask(Opcode::empty_opset()); -# } -# $cached_safe_cmpt -> reval('BEGIN{push @main::__eval__,__FILE__}; package main; ' .$string); -# warn "preload Macros: errors in compiling $macro_file_name:
$@" if $@; -# $self->{envir}{__files__}{$cached_safe_cmpt->reval('pop @main::__eval__')} = $filePath; -# if ($fileName =~ /^($unrestricted_files)$/) { -# $cached_safe_cmpt ->mask($store_mask); -# warn "mask restored after $fileName" if $debugON; -# } -# -# -# } -# } -# -# ################################################################################ -# # load symbol table -# ################################################################################ -# warn "begin loading symbol table " if $debugON; -# no strict 'refs'; -# my %symbolHash = %{$cached_safe_cmpt->root.'::'}; -# use strict 'refs'; -# my @subroutine_names; -# -# foreach my $name (keys %symbolHash) { -# # weed out internal symbols -# next if $name =~ /^(INC|_|__ANON__|main::)$/; -# if ( defined(&{*{$symbolHash{$name}}}) ) { -# # warn "subroutine $name" if $debugON;; -# push(@subroutine_names, "&$name"); -# } -# } -# -# warn "Loading symbols into active safe compartment:
", join(" ",sort @subroutine_names) if $debugON; -# $self->{safe} -> share_from($cached_safe_cmpt->root,[@subroutine_names]); -# -# # Also need to share the cached safe compartment symbol hash in the current safe compartment. -# # This is necessary because the macro files have been read into the cached safe compartment -# # So all subroutines have the implied names Safe::Root1::subroutine -# # When they call each other we need to make sure that they can reach each other -# # through the Safe::Root1 symbol table. -# -# $self->{safe} -> share('%'.$cached_safe_cmpt->root.'::'); -# warn 'Sharing '.'%'. $cached_safe_cmpt->root. '::' if $debugON; -# time_it("End pre_load_macro_files"); -# # return empty string. -# ''; -# } - sub environment{ my $self = shift; my $envirref = shift; From a8796d35e17229cd56bbfa19809fb6d99c805ff1 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 3 Sep 2021 08:24:55 -0500 Subject: [PATCH 037/134] Modifications to the translator used by the standalone renderer. --- lib/WeBWorK/PG/Translator.pm | 133 +++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 62 deletions(-) diff --git a/lib/WeBWorK/PG/Translator.pm b/lib/WeBWorK/PG/Translator.pm index 0a0b121409..7d74407eea 100644 --- a/lib/WeBWorK/PG/Translator.pm +++ b/lib/WeBWorK/PG/Translator.pm @@ -79,67 +79,70 @@ sets or PG macro files. Use this way to imitate the behavior of C =cut BEGIN { - my $ce = new WeBWorK::CourseEnvironment({ - webwork_dir => $ENV{WEBWORK_ROOT}, - }); - - # This safe compartment is used to read the large macro files such as - # PG.pl, PGbasicmacros.pl and PGanswermacros and cache the results so that - # future calls have preloaded versions of these large files. This saves - # approximately 200ms per render. - my $safeCache = new WWSafe; - - my @modules = @{ $ce->{pg}->{modules} }; - my $ra_included_modules = []; - - foreach my $module_packages_ref (@modules) { - my ( $module, @extra_packages ) = @$module_packages_ref; - - # the first item is the main package - $module =~ s/\.pm$//; - eval "package Main; require $module; import $module;"; - warn "Failed to evaluate module $module: $@" if $@; - push @$ra_included_modules, "\%${module}::"; - - # the remaining items are "extra" packages - foreach (@extra_packages) { - s/\.pm$//; - import $_; - warn "Failed to evaluate module $_: $@" if $@; - push @$ra_included_modules, "\%${_}::"; + # Setup the safe compartment for the standalone renderer. + if (exists($ENV{MOJO_MODE})) { + my $ce = new WeBWorK::CourseEnvironment({ + webwork_dir => $ENV{WEBWORK_ROOT}, + }); + + # This safe compartment is used to read the large macro files such as + # PG.pl, PGbasicmacros.pl and PGanswermacros and cache the results so that + # future calls have preloaded versions of these large files. This saves + # approximately 200ms per render. + my $safeCache = new WWSafe; + + my @modules = @{ $ce->{pg}->{modules} }; + my $ra_included_modules = []; + + foreach my $module_packages_ref (@modules) { + my ( $module, @extra_packages ) = @$module_packages_ref; + + # the first item is the main package + $module =~ s/\.pm$//; + eval "package Main; require $module; import $module;"; + warn "Failed to evaluate module $module: $@" if $@; + push @$ra_included_modules, "\%${module}::"; + + # the remaining items are "extra" packages + foreach (@extra_packages) { + s/\.pm$//; + import $_; + warn "Failed to evaluate module $_: $@" if $@; + push @$ra_included_modules, "\%${_}::"; + } } - } - $safeCache->share_from( 'main', $ra_included_modules ); + $safeCache->share_from( 'main', $ra_included_modules ); - my $store_mask = $safeCache->mask(); - $safeCache->mask(Opcode::empty_opset()); - my $safe_cmpt_package_name = $safeCache->root(); - - # these days, the only unrestricted load is from PG.pl -- include it in pre-cache - my $filePath = $WeBWorK::Constants::PG_DIRECTORY . "/macros/PG.pl"; - my $init_subroutine_name = "${safe_cmpt_package_name}::_PG_init"; - - my $errors = ""; - if (-r $filePath ) { - my $rdoResult = $safeCache->rdo($filePath); - $errors .= "\nThere were problems compiling the file:\n $filePath\n $@\n" if $@; - } else { - $errors .= "Can't open file $filePath for reading\n"; - } - $safeCache -> mask($store_mask); + my $store_mask = $safeCache->mask(); + $safeCache->mask(Opcode::empty_opset()); + my $safe_cmpt_package_name = $safeCache->root(); - # intialize PG.pl - my $init_subroutine = eval { \&{$init_subroutine_name} }; - my $macro_file_loaded = ref($init_subroutine) =~ /CODE/; - if ( $macro_file_loaded ) { - &$init_subroutine(); - } - $errors .= "\nUnknown error. Unable to load $filePath\n" if ($errors eq '' and not $macro_file_loaded); - die "Translator.pm [BEGIN errors]: $errors\n" if $errors; + # these days, the only unrestricted load is from PG.pl -- include it in pre-cache + my $filePath = $WeBWorK::Constants::PG_DIRECTORY . "/macros/PG.pl"; + my $init_subroutine_name = "${safe_cmpt_package_name}::_PG_init"; + + my $errors = ""; + if ( -r $filePath ) { + my $rdoResult = $safeCache->rdo($filePath); + $errors .= "\nThere were problems compiling the file:\n $filePath\n $@\n" if $@; + } else { + $errors .= "Can't open file $filePath for reading\n"; + } + $safeCache -> mask($store_mask); - # stash the cache in a global variable - $WeBWorK::Translator::safeCache = $safeCache; + # intialize PG.pl + my $init_subroutine = eval { \&{$init_subroutine_name} }; + my $macro_file_loaded = ref($init_subroutine) =~ /CODE/; + if ( $macro_file_loaded ) { + &$init_subroutine(); + } + $errors .= "\nUnknown error. Unable to load $filePath\n" if ($errors eq '' and not $macro_file_loaded); + die "Translator.pm [BEGIN errors]: $errors\n" if $errors; + + # stash the cache in a global variable + $WeBWorK::Translator::safeCache = $safeCache; + } # allows the use of strict within macro packages. sub be_strict { @@ -208,15 +211,20 @@ sub load_extra_packages{ } =head2 new + Creates the translator object. + =cut sub new { my $class = shift; - # it is safe for all requests to use the safeCache because the perl - # process is forked for each render request (thanks async!) - my $safe_cmpt = $WeBWorK::Translator::safeCache; + + # The standalone renderer caches the safe compartment when the module is compiled. + # It is safe for all requests to use the safeCache because the perl + # process is forked for each render request. (thanks async!) + my $safe_cmpt = exists($ENV{MOJO_MODE}) ? $WeBWorK::Translator::safeCache : new WWSafe; + my $self = { preprocess_code => \&default_preprocess_code, postprocess_code => \&default_postprocess_code, @@ -356,9 +364,10 @@ sub initialize { #$safe_cmpt -> share('$rf_restricted_eval'); use strict; - # $safe_cmpt -> share_from('main', $self->{ra_included_modules} ); - # the above line will get changed when we fix the PG modules thing. heh heh. [FIXED] - # now the default included modules from defaults.config are loaded at BEGIN + # The standalone renderer does this when the module is compiled. + unless (exists($ENV{MOJO_MODE})) { + $safe_cmpt -> share_from('main', $self->{ra_included_modules} ); + } } sub environment{ From ff893d2ae66d2a62d774cdfc132cfa631a0395f7 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Thu, 14 Oct 2021 20:25:46 -0700 Subject: [PATCH 038/134] fix calls to Parser::Value::TeX & too many (cooks) --- macros/contextFraction.pl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/macros/contextFraction.pl b/macros/contextFraction.pl index 2c5585a93a..f44686c3b1 100644 --- a/macros/contextFraction.pl +++ b/macros/contextFraction.pl @@ -618,7 +618,7 @@ sub reduce { # sub string { my $self = shift; - my $string = $self->SUPER::string($self, @_); + my $string = $self->SUPER::string(@_); return $string unless $self->{value}->classMatch('Fraction'); my $precedence = shift; my $frac = $self->context->operators->get('/')->{precedence}; @@ -627,15 +627,17 @@ sub string { } # -# Add parentheses if they are needed by precedence +# Add parentheses if they were there originally, or +# are needed by precedence and we asked for exxxtra parens # sub TeX { my $self = shift; - my $string = $self->SUPER::TeX($self, @_); + my $string = $self->SUPER::TeX(@_); return $string unless $self->{value}->classMatch('Fraction'); my $precedence = shift; my $frac = $self->context->operators->get('/')->{precedence}; - $string = '\left(' . $string . '\right)' if defined $precedence && $precedence > $frac; + $string = '\left(' . $string . '\right)' + if defined $precedence && $precedence > $frac && $self->context->flag('showExtraParens') > 1; return $string; } @@ -898,7 +900,6 @@ sub string { if ($self->getFlagWithAlias("showMixedNumbers","showProperFractions") && CORE::abs($a) > $b) {$n = int($a/$b); $a = CORE::abs($a) % $b; $n .= " " unless $a == 0} $n .= "$a/$b" unless $a == 0 && $n ne ''; - $n = "($n)" if defined $prec && $prec >= 1; return "$n"; } @@ -911,7 +912,6 @@ sub TeX { my $s = ""; ($a,$s) = (-$a,"-") if $a < 0; $n .= ($self->{isHorizontal} ? "$s$a/$b" : "${s}{\\textstyle\\frac{$a}{$b}}") unless $a == 0 && $n ne ''; - $n = "\\left($n\\right)" if defined $prec && $prec >= 1; return "$n"; } From 88c183e3002ec4e39d59cae682f4678a18fec481 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Fri, 15 Oct 2021 08:54:45 -0700 Subject: [PATCH 039/134] forgot to include explicit parens --- macros/contextFraction.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macros/contextFraction.pl b/macros/contextFraction.pl index f44686c3b1..03c980499b 100644 --- a/macros/contextFraction.pl +++ b/macros/contextFraction.pl @@ -636,8 +636,8 @@ sub TeX { return $string unless $self->{value}->classMatch('Fraction'); my $precedence = shift; my $frac = $self->context->operators->get('/')->{precedence}; - $string = '\left(' . $string . '\right)' - if defined $precedence && $precedence > $frac && $self->context->flag('showExtraParens') > 1; + $string = '\left(' . $string . '\right)' if $self->{hadParens} || + (defined $precedence && $precedence > $frac && $self->context->flag('showExtraParens') > 1); return $string; } From 87531fb3ddbb39a8b43968ec25ab58a7b43d9985 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Sat, 16 Oct 2021 09:06:45 -0700 Subject: [PATCH 040/134] don't declare outermost parens --- lib/Parser.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Parser.pm b/lib/Parser.pm index d3a249932c..b2d1affd68 100644 --- a/lib/Parser.pm +++ b/lib/Parser.pm @@ -401,8 +401,8 @@ sub Close { ($top->type eq 'Comma') ? $top->entryType : $top->typeRef, ($type ne 'start') ? ($open,$paren->{close}) : () )}; } else { - $top->{value}{hadParens} = 1; - } + $top->{value}{hadParens} = 1 unless $open eq 'start'; + } $self->pop; $self->push($top); $self->CloseFn() if ($paren->{function} && $self->prev->{type} eq 'fn'); } elsif ($paren->{formInterval} eq $type && $self->top->{value}->length == 2) { From e091ac25e9e898e8d0d2f8f106f00b95bbe9414a Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Wed, 20 Oct 2021 16:17:54 -0700 Subject: [PATCH 041/134] Update macros/contextFraction.pl Co-authored-by: Davide P. Cervone --- macros/contextFraction.pl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/macros/contextFraction.pl b/macros/contextFraction.pl index 03c980499b..2644529566 100644 --- a/macros/contextFraction.pl +++ b/macros/contextFraction.pl @@ -636,8 +636,9 @@ sub TeX { return $string unless $self->{value}->classMatch('Fraction'); my $precedence = shift; my $frac = $self->context->operators->get('/')->{precedence}; + my $noparens = shift; $string = '\left(' . $string . '\right)' if $self->{hadParens} || - (defined $precedence && $precedence > $frac && $self->context->flag('showExtraParens') > 1); + (defined $precedence && $precedence > $frac && !$noparens); return $string; } From bdef90a443faf27cc95fa2453a08f5f969657706 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Fri, 15 Oct 2021 14:54:24 -0700 Subject: [PATCH 042/134] drop custom typeMatch & let SUPER do it instead --- macros/parserAssignment.pl | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/macros/parserAssignment.pl b/macros/parserAssignment.pl index 9dd9840ab8..cb63d563bf 100644 --- a/macros/parserAssignment.pl +++ b/macros/parserAssignment.pl @@ -351,18 +351,6 @@ sub new { return $f; } -sub typeMatch { - my $self = shift; my $other = shift; my $ans = shift; - return 0 unless $self->type eq $other->type; - $other = $other->Package("Formula")->new($self->context,$other) unless $other->isFormula; - my $typeMatch = ($self->createRandomPoints(1))[1]->[0]{data}[1]; - $main::__other__ = sub {($other->createRandomPoints(1))[1]->[0]{data}[1]}; - $other = main::PG_restricted_eval('&$__other__()'); - delete $main::{__other__}; - return 1 unless defined($other); # can't really tell, so don't report type mismatch - $typeMatch->typeMatch($other,$ans); -} - sub cmp_class { my $self = shift; my $value; if ($self->{tree}{rop}{isConstant}) { From 3282d8505790943dbb720a375636173de14bf342 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Thu, 4 Nov 2021 08:32:39 -0700 Subject: [PATCH 043/134] restore typeMatch and block string assignment --- macros/parserAssignment.pl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/macros/parserAssignment.pl b/macros/parserAssignment.pl index cb63d563bf..c3f0feaef7 100644 --- a/macros/parserAssignment.pl +++ b/macros/parserAssignment.pl @@ -348,9 +348,20 @@ sub new { my $self = shift; $class = ref($self) || $self; my $f = $self->SUPER::new(@_); bless $f, $class if $f->type eq 'Assignment'; + my $rhs = $f->getTypicalValue($f)->{data}[1]; + Value->Error('Assignment of strings is not allowed.') if $rhs && $rhs->type eq 'String'; return $f; } +sub typeMatch { + my $self = shift; my $other = shift; my $ans = shift; + return 0 unless $self->type eq $other->type; + my $typeMatch = $self->getTypicalValue($self)->{data}[1]; + $other = $self->getTypicalValue($other,1)->{data}[1]; + return 1 unless defined($other); # can't really tell, so don't report type mismatch + $typeMatch->typeMatch($other,$ans); +} + sub cmp_class { my $self = shift; my $value; if ($self->{tree}{rop}{isConstant}) { From b35a55508d8e891bca1e022e2b0b792684348439 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Tue, 14 Dec 2021 16:16:08 -0500 Subject: [PATCH 044/134] Fix problem with matrix typechecking in answer preview; also fix CSS problem with warning messages (openwebwork/pg#619) --- lib/Value/AnswerChecker.pm | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/Value/AnswerChecker.pm b/lib/Value/AnswerChecker.pm index f5ddb665ff..14eddc4864 100644 --- a/lib/Value/AnswerChecker.pm +++ b/lib/Value/AnswerChecker.pm @@ -590,6 +590,7 @@ sub ans_collect { $entry = $ans->{original_student_ans}; $ans->{student_formula} = $ans->{student_value} = undef unless $entry =~ m/\S/; } + $ans->{typeError} = 0; my $result = $data->[$i][$j]->cmp(@ans_cmp_defaults)->evaluate($entry); $OK &= entryCheck($result,$blank); push(@row,$result->{student_formula}); @@ -616,18 +617,20 @@ sub entryMessage { if ($rows == 1) {$title = "In entry $j"} elsif ($cols == 1) {$title = "In entry $i"} else {$title = "In entry ($i,$j)"} - push(@{$errors},"$title: ". - "$message"); + push(@{$errors}, + "$title: ". + "$message"); } sub entryCheck { my $ans = shift; my $blank = shift; + return 0 if $ans->{typeError}; return 1 if defined($ans->{student_value}); if (!defined($ans->{student_formula})) { $ans->{student_formula} = $ans->{student_ans}; $ans->{student_formula} = $blank unless $ans->{student_formula}; } - return 0 + return 0; } From 1a5eea7679a355e24b344b61e2898f9cbd21b4a3 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Thu, 16 Dec 2021 15:13:28 -0500 Subject: [PATCH 045/134] Allow preview to show matrices with type errors --- lib/Value/AnswerChecker.pm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Value/AnswerChecker.pm b/lib/Value/AnswerChecker.pm index 14eddc4864..e71b49c02b 100644 --- a/lib/Value/AnswerChecker.pm +++ b/lib/Value/AnswerChecker.pm @@ -201,6 +201,7 @@ sub cmp_collect { $ans->{student_value} = $ans->{student_formula}; $ans->{preview_text_string} = $ans->{student_ans}; $ans->{preview_latex_string} = $ans->{student_formula}->TeX; + return 0 if $ans->{typeError}; if (Value::isFormula($ans->{student_formula}) && $ans->{student_formula}->isConstant) { $ans->{student_value} = Parser::Evaluate($ans->{student_formula}); return 0 unless $ans->{student_value}; @@ -593,6 +594,7 @@ sub ans_collect { $ans->{typeError} = 0; my $result = $data->[$i][$j]->cmp(@ans_cmp_defaults)->evaluate($entry); $OK &= entryCheck($result,$blank); + $ans->{typeError} = 1 if $result->{typeError}; push(@row,$result->{student_formula}); entryMessage($result->{ans_message},$errors,$i,$j,$rows,$cols); } @@ -624,8 +626,7 @@ sub entryMessage { sub entryCheck { my $ans = shift; my $blank = shift; - return 0 if $ans->{typeError}; - return 1 if defined($ans->{student_value}); + return 1 if defined($ans->{student_value}) || $ans->{typeError}; if (!defined($ans->{student_formula})) { $ans->{student_formula} = $ans->{student_ans}; $ans->{student_formula} = $blank unless $ans->{student_formula}; From bb87a15996024a4f5094dcd55cbdebaae3c02c6c Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Sun, 26 Dec 2021 14:25:38 -0500 Subject: [PATCH 046/134] Allow paragaph breaks within comments and code blocks --- macros/PGML.pl | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/macros/PGML.pl b/macros/PGML.pl index d0a8756ff7..210d398035 100644 --- a/macros/PGML.pl +++ b/macros/PGML.pl @@ -274,10 +274,14 @@ sub ForceBreak { sub Par { my $self = shift; my $token = shift; - $self->End(undef, shift); - $self->Item("par",$token,{noIndent => 1}); - $self->{atLineStart} = $self->{ignoreNL} = 1; - $self->{indent} = $self->{actualIndent} = 0; + if ($self->{block}{allowPar}) { + $self->Text("\n\n"); + } else { + $self->End(undef, shift); + $self->Item("par",$token,{noIndent => 1}); + $self->{atLineStart} = $self->{ignoreNL} = 1; + $self->{indent} = $self->{actualIndent} = 0; + } } sub Indent { @@ -470,7 +474,7 @@ sub NOOP { "[<" => {type=>'link', parseComments=>1, parseSubstitutions=>1, terminator=>qr/>\]/, terminateMethod=>'terminateGetString', cancelNL=>1, options=>["text","title"]}, - "[%" => {type=>'comment', parseComments=>1, terminator=>qr/%\]/}, + "[%" => {type=>'comment', parseComments=>1, terminator=>qr/%\]/, allowPar=>1}, "[\@" => {type=>'command', parseComments=>1, parseSubstitutions=>1, terminator=>qr/@\]/, terminateMethod=>'terminateGetString', balance=>qr/[\'\"]/, allowStar=>1, allowDblStar=>1, allowTriStar=>1}, @@ -486,7 +490,7 @@ sub NOOP { balance=>$balanceAll, cancelUnbalanced=>1}, "'" => {type=>'balance', terminator=>qr/\'/, terminateMethod=>'terminateBalance'}, '"' => {type=>'balance', terminator=>qr/\"/, terminateMethod=>'terminateBalance'}, - "```" => {type=>'code', terminator=>qr/```/, terminateMethod=>'terminateCode'}, + "```" => {type=>'code', terminator=>qr/```/, terminateMethod=>'terminateCode', allowPar=>1}, ": " => {type=>'pre', parseAll=>1, terminator=>qr/\n+/, terminateMethod=>'terminatePre', combine=>{pre=>"type"}, noIndent=>-1}, ">>" => {type=>'align', parseAll=>1, align=>"right", breakInside=>1, From 0ec597d9a636b46249ce30c61b3c6450d9980f9b Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Wed, 22 Dec 2021 17:19:45 -0600 Subject: [PATCH 047/134] Update the scaffold html markup for bootstrap 5. --- macros/scaffold.pl | 52 ++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/macros/scaffold.pl b/macros/scaffold.pl index 21f1e90054..1b7d2b5cc7 100644 --- a/macros/scaffold.pl +++ b/macros/scaffold.pl @@ -1,12 +1,12 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2020 The WeBWorK Project, http://openwebwork.sf.net/ -# +# Copyright © 2000-2021 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 @@ -42,21 +42,21 @@ =head1 DESCRIPTION the section. For example: Scaffold::Begin(); - + Section::Begin("Part 1: The first part"); BEGIN_TEXT This is the text for part 1. \(1+1\) = \{ans_rule\} END_TEXT ANS(Real(2)->cmp); Section::End(); - + Section::Begin("Part 2: The second part"); BEGIN_TEXT This is text for the second part. \(2*2\) = \{ans_rule\} END_TEXT ANS(Real(4)->cmp); Section::End(); - + Scaffold::End(); You can include whatever code you need to between the @@ -95,7 +95,7 @@ =head1 DESCRIPTION =over -=item C condition >>> +=item C condition >>> This specifies when a section can be opened by the student. The C is either one of the strings C<"always">, @@ -208,7 +208,7 @@ =head1 DESCRIPTION can_open => "when_previous_correct", is_open => "first_incorrect" ); - + # # Sections stay open as the student works through # the problem. @@ -217,7 +217,7 @@ =head1 DESCRIPTION can_open => "when_previous_correct", is_open => "correct_or_first_incorrect" ); - + # # Students work through the problem seeing only # one section at a time, and can't go back to @@ -227,7 +227,7 @@ =head1 DESCRIPTION can_open => "first_incorrect", is_open => "first_incorrect" ); - + # # Students can view and work on any section, # but only the first incorrect one is shown initially. @@ -236,7 +236,7 @@ =head1 DESCRIPTION can_open => "always", is_open => "first_incorrect" ); - + # # Students see all the parts initially, but the # sections close as the student gets them correct. @@ -245,7 +245,7 @@ =head1 DESCRIPTION can_open => "always", is_open => "incorrect" ); - + # # Students see all the parts initially, but the # sections close as the student gets them correct, @@ -274,7 +274,7 @@ =head1 DESCRIPTION sub _scaffold_init { # Load style and javascript for opening and closing the scaffolds. ADD_CSS_FILE("js/apps/Scaffold/scaffold.css"); - ADD_JS_FILE("js/apps/Scaffold/scaffold.js"); + ADD_JS_FILE("js/apps/Scaffold/scaffold.js", 0, { defer => undef }); }; # @@ -614,21 +614,29 @@ sub add_container { splice(@$PG_OUTPUT,0,scalar(@$PG_OUTPUT)) if !($canopen || $iscorrect || $Scaffold::isPTX) || (!$isopen && $Scaffold::isHardcopy); unshift(@$PG_OUTPUT,@{main::MODES( HTML => [ - '
', - '
', - '', - "$number", - '' . $title . '', + '
', + '
', + '
', + '', '
', - '
', - '
' + qq{
}, + '
' ], TeX => ["\\par{\\bf $number $title}\\addtolength{\\leftskip}{15pt}\\par "], PTX => $name ? ["\n", "$name\n"] : ["\n"], )}); push(@$PG_OUTPUT,main::MODES( - HTML => '
', + HTML => '
', TeX => "\\addtolength{\\leftskip}{-15pt}\\par ", PTX => "<\/task>\n", )); From e0987b76974fa439d088552f622651445d8083a8 Mon Sep 17 00:00:00 2001 From: pschan-gh <31397850+pschan-gh@users.noreply.github.com> Date: Thu, 30 Dec 2021 12:05:07 +0800 Subject: [PATCH 048/134] Removed comment lines from Perl POD sections. Changed openwebwork.sf.net to github.com/openwebwork. Examples in draggableProof.pl and draggbleSubsets.pl now use PGML. Added "btn-secondary" class to "Reset" and "Add bucket" buttons. --- lib/DragNDrop.pm | 50 +++---- macros/draggableProof.pl | 282 ++++++++++++++++++------------------- macros/draggableSubsets.pl | 257 +++++++++++++++++---------------- 3 files changed, 289 insertions(+), 300 deletions(-) diff --git a/lib/DragNDrop.pm b/lib/DragNDrop.pm index d4fa06c246..b1c358ff75 100644 --- a/lib/DragNDrop.pm +++ b/lib/DragNDrop.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2021 The WeBWorK Project, http://openwebwork.sf.net/ +# Copyright © 2000-2021 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 @@ -51,13 +51,11 @@ Example: It is imperative that square brackets be used. -############################################################################## OPTIONAL: DragNDrop($answerInputId, $aggregateList, AllowNewBuckets => 1); allows student to create new buckets by clicking on a button. -############################################################################## To add a bucket to an existing pool $bucket_pool, do: @@ -80,7 +78,6 @@ then: An empty array reference, e.g. $bucket_pool->addBucket([]), gives an empty bucket. -############################################################################### OPTIONAL: $bucket_pool->addBucket($indices, label => 'Barrel', removable => 1) @@ -89,7 +86,6 @@ puts the label 'Barrel' at the top of the bucket. With the removable option set to 1, the bucket may be removed by the student via the click of a "Remove" button at the bottom of the bucket. (The first created bucket may never be removed.) -############################################################################### To output the bucket pool to HTML, call: @@ -105,15 +101,13 @@ See draggableProof.pl and draggableSubsets.pl =cut -############################################################################### - use strict; use warnings; package DragNDrop; sub new { - my $self = shift; + my $self = shift; my $class = ref($self) || $self; # 'id' of html tag corresponding to the answer blank. Must be unique to each pool of DragNDrop buckets @@ -127,14 +121,14 @@ sub new { my $defaultBuckets = shift; my %options = ( - AllowNewBuckets => 0, - @_ + AllowNewBuckets => 0, + @_ ); $self = bless { - answerInputId => $answerInputId, - bucketList => [], - aggregateList => $aggregateList, + answerInputId => $answerInputId, + bucketList => [], + aggregateList => $aggregateList, defaultBuckets => $defaultBuckets, %options, }, $class; @@ -148,19 +142,19 @@ sub addBucket { my $indices = shift; my %options = ( - label => "", - removable => 0, - @_ + label => "", + removable => 0, + @_ ); my $bucket = { - indices => $indices, - list => [ map { $self->{aggregateList}->[$_] } @$indices ], + indices => $indices, + list => [ map { $self->{aggregateList}->[$_] } @$indices ], bucket_id => scalar @{ $self->{bucketList} }, - label => $options{label}, + label => $options{label}, removable => $options{removable}, }; - push(@{$self->{bucketList}}, $bucket); + push(@{ $self->{bucketList} }, $bucket); } @@ -171,34 +165,34 @@ sub HTML { $out .= "
"; # buckets from instructor-defined default settings - for (my $i = 0; $i < @{$self->{defaultBuckets}}; $i++) { + for (my $i = 0; $i < @{ $self->{defaultBuckets} }; $i++) { my $defaultBucket = $self->{defaultBuckets}->[$i]; $out .= ""; } # buckets from past answers - for my $bucket ( @{$self->{bucketList}} ) { + for my $bucket (@{ $self->{bucketList} }) { $out .= ""; } $out .= '
'; - $out .= "
reset"; + $out .= "
reset"; if ($self->{AllowNewBuckets} == 1) { - $out .= "add bucket"; + $out .= "add bucket"; } $out .= "
"; @@ -214,9 +208,9 @@ sub TeX { for (my $i = 0; $i < @{ $self->{defaultBuckets} }; $i++) { $out .= "\n"; my $defaultBucket = $self->{defaultBuckets}->[$i]; - if ( @{$defaultBucket->{indices}} > 0 ) { + if (@{ $defaultBucket->{indices} } > 0) { $out .= "\n\\hrule\n\\begin{itemize}"; - for my $j ( @{$defaultBucket->{indices}} ) { + for my $j (@{ $defaultBucket->{indices} }) { $out .= "\n\\item[$j.]\n $self->{aggregateList}->[$j]"; } $out .= "\n\\end{itemize}"; diff --git a/macros/draggableProof.pl b/macros/draggableProof.pl index 6d54149b85..febd5e9f6b 100644 --- a/macros/draggableProof.pl +++ b/macros/draggableProof.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2021 The WeBWorK Project, http://openwebwork.sf.net/ +# Copyright © 2000-2021 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 @@ -33,13 +33,13 @@ =head1 USAGE $draggable = DraggableProof($statements, $extra, Options1 => ..., Options2 => ...); -before BEGIN_TEXT. +before BEGIN_TEXT (or BEGIN_PGML). Then, call: -$draggable->Print +$draggable->Print (or [@ $draggable->Print @]* ) -within the BEGIN_TEXT / END_TEXT environment; +within the BEGIN_TEXT / END_TEXT (or BEGIN_PGML / END_PGML ) environment. $statements, e.g. ["Socrates is a man.", "Socrates is mortal.", ...], is an array reference to the list of statements used in the correct proof. @@ -69,6 +69,7 @@ =head1 EXAMPLE DOCUMENT(); loadMacros( "PGstandard.pl", + "PGML.pl", "MathObjects.pl", "draggableProof.pl" ); @@ -91,9 +92,9 @@ =head1 EXAMPLE $statements, $extra, NumBuckets => 2, # either 1 or 2. - SourceLabel => "Axioms", # label of first bucket if NumBuckets = 2. + SourceLabel => "${BBOLD}Axioms${EBOLD}", # label of first bucket if NumBuckets = 2. # - TargetLabel => "Reasoning", + TargetLabel => "${BBOLD}Reasoning${EBOLD}", # label of second bucket if NumBuckets = 2, # of the only bucket if NumBuckets = 1. # @@ -120,19 +121,15 @@ =head1 EXAMPLE # Default value = 1. ); - Context()->texStrings; - BEGIN_TEXT + BEGIN_PGML - Show that Socrates is mortal by dragging the relevant $BBOLD Axioms $EBOLD - into the $BBOLD Reasoning $EBOLD box in an appropriate order. + Show that Socrates is mortal by dragging the relevant *Axioms* + into the *Reasoning* box in an appropriate order. - $PAR + [@ $draggable->Print @]* - \{ $draggable->Print \} - - END_TEXT - Context()->normalStrings; + END_PGML ANS($draggable->cmp); @@ -142,10 +139,7 @@ =head1 EXAMPLE ################################################################ -loadMacros( -"PGchoicemacros.pl", -"MathObjects.pl", -); +loadMacros("PGchoicemacros.pl", "MathObjects.pl",); sub _draggableProof_init { ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); @@ -158,78 +152,83 @@ sub _draggableProof_init { package draggableProof; sub new { - my $self = shift; + my $self = shift; my $class = ref($self) || $self; - my $proof = shift; - my $extra = shift; + my $proof = shift; + my $extra = shift; my %options = ( - SourceLabel => "Choose from these sentences:", - TargetLabel => "Your Proof:", - NumBuckets => 2, - Levenshtein => 0, - DamerauLevenshtein => 0, - InferenceMatrix => [], - IrrelevancePenalty => 1, - @_ + SourceLabel => "Choose from these sentences:", + TargetLabel => "Your Proof:", + NumBuckets => 2, + Levenshtein => 0, + DamerauLevenshtein => 0, + InferenceMatrix => [], + IrrelevancePenalty => 1, + @_ ); - my $lines = [ @$proof, @$extra ]; - my $numNeeded = scalar(@$proof); - my $numProvided = scalar(@$lines); - my @order = main::shuffle($numProvided); - my @unorder = main::invert(@order); - my $shuffledLines = [ map {$lines->[$_]} @order ]; + my $lines = [ @$proof, @$extra ]; + my $numNeeded = scalar(@$proof); + my $numProvided = scalar(@$lines); + my @order = main::shuffle($numProvided); + my @unorder = main::invert(@order); + my $shuffledLines = [ map { $lines->[$_] } @order ]; my $answerInputId = main::NEW_ANS_NAME() unless $self->{answerInputId}; - my $ans_rule = main::NAMED_HIDDEN_ANS_RULE($answerInputId); + my $ans_rule = main::NAMED_HIDDEN_ANS_RULE($answerInputId); my $dnd; if ($options{NumBuckets} == 2) { - $dnd = new DragNDrop($answerInputId, $shuffledLines, - [ - { - indices => [0..$numProvided-1], - label => $options{'SourceLabel'} - }, - { - indices => [], - label => $options{'TargetLabel'} - } - ], - AllowNewBuckets => 0); - } elsif($options{NumBuckets} == 1) { - $dnd = new DragNDrop($answerInputId, $shuffledLines, - [ - { - indices => [0..$numProvided-1], - label => $options{'TargetLabel'} - } - ], - AllowNewBuckets => 0); + $dnd = new DragNDrop( + $answerInputId, + $shuffledLines, + [ + { + indices => [ 0 .. $numProvided - 1 ], + label => $options{'SourceLabel'} + }, + { + indices => [], + label => $options{'TargetLabel'} + } + ], + AllowNewBuckets => 0 + ); + } elsif ($options{NumBuckets} == 1) { + $dnd = new DragNDrop( + $answerInputId, + $shuffledLines, + [ { + indices => [ 0 .. $numProvided - 1 ], + label => $options{'TargetLabel'} + } ], + AllowNewBuckets => 0 + ); } - my $proof = $options{NumBuckets} == 2 ? main::List( - main::List(@unorder[$numNeeded .. $numProvided - 1]), - main::List(@unorder[0..$numNeeded-1]) - ) : main::List('('.join(',', @unorder[0..$numNeeded-1]).')'); + my $proof = + $options{NumBuckets} == 2 + ? main::List(main::List(@unorder[ $numNeeded .. $numProvided - 1 ]), + main::List(@unorder[ 0 .. $numNeeded - 1 ])) + : main::List('(' . join(',', @unorder[ 0 .. $numNeeded - 1 ]) . ')'); - my $extra = main::Set(@unorder[$numNeeded .. $numProvided - 1]); + my $extra = main::Set(@unorder[ $numNeeded .. $numProvided - 1 ]); my $InferenceMatrix = $options{InferenceMatrix}; $self = bless { - lines => $lines, - shuffledLines => $shuffledLines, - numNeeded => $numNeeded, - numProvided => $numProvided, - order => \@order, - unorder => \@unorder, - proof => $proof, - extra => $extra, - answerInputId => $answerInputId, - dnd => $dnd, - ans_rule => $ans_rule, + lines => $lines, + shuffledLines => $shuffledLines, + numNeeded => $numNeeded, + numProvided => $numProvided, + order => \@order, + unorder => \@unorder, + proof => $proof, + extra => $extra, + answerInputId => $answerInputId, + dnd => $dnd, + ans_rule => $ans_rule, inferenceMatrix => $InferenceMatrix, %options, }, $class; @@ -238,13 +237,13 @@ sub new { if ($previous eq "") { if ($self->{NumBuckets} == 2) { - $dnd->addBucket([0..$numProvided-1], label => $options{'SourceLabel'}); - $dnd->addBucket([], label => $options{'TargetLabel'}); + $dnd->addBucket([ 0 .. $numProvided - 1 ], label => $options{'SourceLabel'}); + $dnd->addBucket([], label => $options{'TargetLabel'}); } elsif ($self->{NumBuckets} == 1) { - $dnd->addBucket([0..$numProvided-1], label => $options{'TargetLabel'}); + $dnd->addBucket([ 0 .. $numProvided - 1 ], label => $options{'TargetLabel'}); } } else { - my @matches = ( $previous =~ /(\([^\(\)]*\)|-?\d+)/g ); + my @matches = ($previous =~ /(\([^\(\)]*\)|-?\d+)/g); if ($self->{NumBuckets} == 2) { my $indices1 = [ split(',', @matches[0] =~ s/\(|\)//gr) ]; $dnd->addBucket($indices1->[0] != -1 ? $indices1 : [], label => $options{'SourceLabel'}); @@ -259,46 +258,47 @@ sub new { return $self; } -sub lines {@{shift->{lines}}} -sub numNeeded {shift->{numNeeded}} -sub numProvided {shift->{numProvided}} -sub order {@{shift->{order}}} -sub unorder {@{shift->{unorder}}} +sub lines { @{ shift->{lines} } } +sub numNeeded { shift->{numNeeded} } +sub numProvided { shift->{numProvided} } +sub order { @{ shift->{order} } } +sub unorder { @{ shift->{unorder} } } sub Levenshtein { my @ar1 = split /$_[2]/, $_[0]; my @ar2 = split /$_[2]/, $_[1]; - my @dist = ([0 .. @ar2]); + my @dist = ([ 0 .. @ar2 ]); $dist[$_][0] = $_ for (1 .. @ar1); for my $i (0 .. $#ar1) { for my $j (0 .. $#ar2) { - $dist[$i+1][$j+1] = main::min($dist[$i][$j+1] + 1, $dist[$i+1][$j] + 1, - $dist[$i][$j] + ($ar1[$i] ne $ar2[$j]) ); + $dist[ $i + 1 ][ $j + 1 ] = + main::min($dist[$i][ $j + 1 ] + 1, $dist[ $i + 1 ][$j] + 1, $dist[$i][$j] + ($ar1[$i] ne $ar2[$j])); } } $dist[-1][-1]; } sub DamerauLevenshtein { + # Damerau–Levenshtein distance with adjacent transpositions # https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance - my $discourse1 = shift; - my $discourse2 = shift; - my $delimiter = shift; + my $discourse1 = shift; + my $discourse2 = shift; + my $delimiter = shift; my $numProvided = shift; my @ar1 = split /$delimiter/, $discourse1; my @ar2 = split /$delimiter/, $discourse2; my @da = (0) x $numProvided; - my @d = (); + my @d = (); my $maxdist = @ar1 + @ar2; for my $i (1 .. @ar1 + 1) { - push(@d, [ (0) x (@ar2 + 2) ] ); + push(@d, [ (0) x (@ar2 + 2) ]); $d[$i][0] = $maxdist; $d[$i][1] = $i - 1; } @@ -311,20 +311,22 @@ sub DamerauLevenshtein { $db = 0; my $k, $l, $cost; for my $j (2 .. @ar2 + 1) { - $k = $da[$ar2[$j - 2]]; + $k = $da[ $ar2[ $j - 2 ] ]; $l = $db; - if ($ar1[$i - 2] == $ar2[$j - 2]) { + if ($ar1[ $i - 2 ] == $ar2[ $j - 2 ]) { $cost = 0; - $db = $j; + $db = $j; } else { $cost = 1; } - $d[$i][$j] = main::min($d[$i-1][$j-1] + $cost, - $d[$i][$j-1] + 1, - $d[$i-1][$j] + 1, - $d[$k-1][$l-1] + ($i - $k - 1) + 1 + ($j - $l - 1)); + $d[$i][$j] = main::min( + $d[ $i - 1 ][ $j - 1 ] + $cost, + $d[$i][ $j - 1 ] + 1, + $d[ $i - 1 ][$j] + 1, + $d[ $k - 1 ][ $l - 1 ] + ($i - $k - 1) + 1 + ($j - $l - 1) + ); } - $da[$ar1[$i - 2]] = $i; + $da[ $ar1[ $i - 2 ] ] = $i; } $d[-1][-1]; } @@ -335,14 +337,11 @@ sub Print { my $ans_rule = $self->{ans_rule}; if ($main::displayMode ne "TeX") { + # HTML mode - return join("\n", - '
', - $ans_rule, - $self->{dnd}->HTML, - '
', - '
', - ); + return + join("\n", '
', $ans_rule, $self->{dnd}->HTML, + '
', '
',); } else { # TeX mode return $self->{dnd}->TeX; @@ -351,14 +350,12 @@ sub Print { sub cmp { my $self = shift; - return $self->{proof} - ->cmp(ordered => 1, removeParens => 1) - ->withPreFilter(sub {$self->prefilter(@_)}) - ->withPostFilter(sub {$self->filter(@_)}); + return $self->{proof}->cmp(ordered => 1, removeParens => 1)->withPreFilter(sub { $self->prefilter(@_) }) + ->withPostFilter(sub { $self->filter(@_) }); } sub prefilter { - my $self = shift; + my $self = shift; my $anshash = shift; my $correctProcessed; @@ -370,7 +367,7 @@ sub prefilter { if ($self->{NumBuckets} == 1) { $correctProcessed = $anshash->{correct_value} =~ s/\(|\)|\s*//gr; } elsif ($self->{NumBuckets} == 2) { - my @matches = ( $anshash->{correct_value} =~ /(\([^\(\)]*\)|-?\d+)/g ); + my @matches = ($anshash->{correct_value} =~ /(\([^\(\)]*\)|-?\d+)/g); $correctProcessed = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; } @@ -380,11 +377,11 @@ sub prefilter { } sub filter { - my $self = shift; + my $self = shift; my $anshash = shift; - my @lines = @{$self->{lines}}; - my @order = @{$self->{order}}; + my @lines = @{ $self->{lines} }; + my @order = @{ $self->{order} }; my $correct_value = $anshash->{correct_value} =~ s/\(|\)|\s*//gr; my $actualAnswer; @@ -392,30 +389,32 @@ sub filter { if ($self->{NumBuckets} == 1) { $actualAnswer = $anshash->{student_value} =~ s/\(|\)|\s*//gr; } elsif ($self->{NumBuckets} == 2) { - my @matches = ( $anshash->{student_value} =~ /(\([^\(\)]*\)|-?\d+)/g ); + my @matches = ($anshash->{student_value} =~ /(\([^\(\)]*\)|-?\d+)/g); $actualAnswer = @matches == 2 ? $matches[1] =~ s/\(|\)|\s*//gr : ''; } if ($self->{Levenshtein} == 1) { - $anshash->{score} = 1 - main::min(1, Levenshtein($correct_value, $actualAnswer, ',')/$self->{numNeeded}); + $anshash->{score} = 1 - main::min(1, Levenshtein($correct_value, $actualAnswer, ',') / $self->{numNeeded}); } elsif ($self->{DamerauLevenshtein} == 1) { my $DLDistance = DamerauLevenshtein($correct_value, $actualAnswer, ',', $self->{numProvided}); - $anshash->{score} = 1 - main::min(1, $DLDistance/($self->{numNeeded})); + $anshash->{score} = + 1 - main::min(1, $DLDistance / ($self->{numNeeded})); } elsif (@{ $self->{inferenceMatrix} } != 0) { - my @unshuffledStudentIndices = map { $self->{order}[$_]} split(',', $actualAnswer); + my @unshuffledStudentIndices = + map { $self->{order}[$_] } split(',', $actualAnswer); my @inferenceMatrix = @{ $self->{inferenceMatrix} }; - my $inferenceScore = 0; - for (my $j = 0; $j < @unshuffledStudentIndices; $j++ ) { + my $inferenceScore = 0; + for (my $j = 0; $j < @unshuffledStudentIndices; $j++) { if ($unshuffledStudentIndices[$j] < $self->{numNeeded}) { - for (my $i = $j - 1; $i >= 0; $i--) { + for (my $i = $j - 1; $i >= 0; $i--) { if ($unshuffledStudentIndices[$i] < $self->{numNeeded}) { $inferenceScore += - $inferenceMatrix[$unshuffledStudentIndices[$i]][$unshuffledStudentIndices[$j]]; + $inferenceMatrix[ $unshuffledStudentIndices[$i] ][ $unshuffledStudentIndices[$j] ]; } } } } my $total = 0; - for my $row ( @inferenceMatrix ) { + for my $row (@inferenceMatrix) { foreach (@$row) { $total += $_; } @@ -423,32 +422,29 @@ sub filter { $anshash->{score} = $inferenceScore / $total; my %invoked = map { $_ => 1 } split(',', $actualAnswer); - foreach ( split(',', $self->{extra}->string =~ s/{|}|\s*//gr ) ) { - if ( exists($invoked{$_}) ) { - $anshash->{score} = main::max(0, $anshash->{score} - ($self->{IrrelevancePenalty}/$total)); + foreach (split(',', $self->{extra}->string =~ s/{|}|\s*//gr)) { + if (exists($invoked{$_})) { + $anshash->{score} = main::max(0, $anshash->{score} - ($self->{IrrelevancePenalty} / $total)); } } } else { - $anshash->{score} = main::List($correct_value) eq main::List($actualAnswer) ? 1 : 0; + $anshash->{score} = + main::List($correct_value) eq main::List($actualAnswer) ? 1 : 0; } - my @correct = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $correct_value); - my @student = map { $_ >= 0 ? $lines[$order[$_]] : '' } split(',', $actualAnswer); + my @correct = + map { $_ >= 0 ? $lines[ $order[$_] ] : '' } split(',', $correct_value); + my @student = + map { $_ >= 0 ? $lines[ $order[$_] ] : '' } split(',', $actualAnswer); $anshash->{non_tex_preview} = 1; - $anshash->{student_ans} = "(see preview)"; - - $anshash->{preview_latex_string} = join('', ( - "
  • ", - join("
  • ",@student), - "
" - )); - - $anshash->{correct_ans_latex_string} = join('', ( - "
  • ", - join("
  • ",@correct), - "
" - )); + $anshash->{student_ans} = "(see preview)"; + + $anshash->{preview_latex_string} = + join('', ("
  • ", join("
  • ", @student), "
")); + + $anshash->{correct_ans_latex_string} = + join('', ("
  • ", join("
  • ", @correct), "
")); $anshash->{correct_value} = $anshash->{original_correct_value}; diff --git a/macros/draggableSubsets.pl b/macros/draggableSubsets.pl index 49cefc9666..0bea037017 100644 --- a/macros/draggableSubsets.pl +++ b/macros/draggableSubsets.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2021 The WeBWorK Project, http://openwebwork.sf.net/ +# Copyright © 2000-2021 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 @@ -33,13 +33,13 @@ =head1 USAGE $draggable = DraggableSubsets($full_set, $ans, Options1 => ..., Options2 => ...); -before BEGIN_TEXT. +before BEGIN_TEXT (or BEGIN_PGML). Then, call: -$draggable->Print +$draggable->Print (or [@ $draggable->Print @]* ) -within the BEGIN_TEXT / END_TEXT environment; +within the BEGIN_TEXT / END_TEXT (or BEGIN_PGML / END_PGML ) environment. $full_set, e.g. ["statement1", "statement2", ...], is an array reference to the list of elements, given as strings, in the original full set. @@ -62,6 +62,7 @@ =head1 EXAMPLE DOCUMENT(); loadMacros( "PGstandard.pl", + "PGML.pl", "MathObjects.pl", "draggableSubsets.pl", ); @@ -69,21 +70,17 @@ =head1 EXAMPLE TEXT(beginproblem()); $D3 = [ - "\(e\)", #0 - "\(r\)", #1 - "\(r^2\)", #2 - "\(s\)", #3 - "\(sr\)", #4 - "\(sr^2\)", #5 + "`e`", #0 + "`r`", #1 + "`r^2`", #2 + "`s`", #3 + "`sr`", #4 + "`sr^2`", #5 ]; $subgroup = "e, s"; - $subsets = [ - [0, 3], - [1, 4], - [2, 5] - ]; + $subsets = [ [0, 3], [1, 4], [2, 5] ]; $draggable = DraggableSubsets( $D3, # full set. Square brackets must be used. @@ -96,58 +93,49 @@ =head1 EXAMPLE label => 'coset 1', # label of the bucket. indices => [ 1, 3, 4, 5 ], # specifies pre-included elements in the bucket via their indices. removable => 0 # specifies whether student may remove bucket. - }, - { - label => 'coset 2', - indices => [ 0 ], - removable => 1 - }, - { - label => 'coset 3', - indices => [ 2 ], - removable => 1 - } - ], - # OrderedSubsets => 0, # means order of subsets does not matter. 1 means otherwise. - # (The order of elements within each subset never matters.) Default value = 0. - # - # AllowNewBuckets => 0, # means no new buckets may be added by student. 1 means otherwise. Default value = 1. - ); - - Context()->texStrings; +}, +{ +label => 'coset 2', +indices => [ 0 ], +removable => 1 +}, +{ +label => 'coset 3', +indices => [ 2 ], +removable => 1 +} +], +# OrderedSubsets => 0, # means order of subsets does not matter. 1 means otherwise. +# (The order of elements within each subset never matters.) Default value = 0. +# +# AllowNewBuckets => 0, # means no new buckets may be added by student. 1 means otherwise. Default value = 1. +); - BEGIN_TEXT +BEGIN_PGML - Let \[ - G=D_3=\lbrace e,r,r^2, s,sr,sr^2 \rbrace - \] - be the Dihedral group of order \(6\), where \(r\) is counter-clockwise rotation by \(2\pi/3\), - and \(s\) is the reflection across the \(x\)-axis. +Let [``G=D_3=\lbrace e,r,r^2, s,sr,sr^2 \rbrace``] +be the Dihedral group of order [`6`], where [`r`] is counter-clockwise rotation by [`2\pi/3`], +and [`s`] is the reflection across the [`x`]-axis. - Partition \(G=D_3\) into $BBOLD right $EBOLD cosets of the subgroup - \(H=\lbrace $subgroup \rbrace\). Give your result by dragging the following elements into separate buckets, - each corresponding to a coset. +Partition [`G=D_3`] into *right* cosets of the subgroup [`H=\lbrace [$subgroup] \rbrace`]. +Give your result by dragging the following elements into separate buckets, +each corresponding to a coset. - $PAR - \{ $draggable->Print \} +[@ $draggable->Print @]* - END_TEXT - Context()->normalStrings; +END_PGML - # Answer Evaluation +# Answer Evaluation - ANS($draggable->cmp); +ANS($draggable->cmp); - ENDDOCUMENT(); +ENDDOCUMENT(); =cut ################################################################ -loadMacros( -"PGchoicemacros.pl", -"MathObjects.pl", -); +loadMacros("PGchoicemacros.pl", "MathObjects.pl",); sub _draggableSubsets_init { ADD_CSS_FILE("https://cdnjs.cloudflare.com/ajax/libs/nestable2/1.6.0/jquery.nestable.min.css", 1); @@ -160,90 +148,96 @@ sub _draggableSubsets_init { package draggableSubsets; sub new { - my $self = shift; + my $self = shift; my $class = ref($self) || $self; # user arguments - my $set = shift; + my $set = shift; my $subsets = shift; my %options = ( - DefaultSubsets => [], - OrderedSubsets => 0, - AllowNewBuckets => 1, - @_ + DefaultSubsets => [], + OrderedSubsets => 0, + AllowNewBuckets => 1, + @_ ); + # end user arguments my $numProvided = scalar(@$set); - my @order = main::shuffle($numProvided); - my @unorder = main::invert(@order); + my @order = main::shuffle($numProvided); + my @unorder = main::invert(@order); - my $shuffledSet = [ map {$set->[$_]} @order ]; + my $shuffledSet = [ map { $set->[$_] } @order ]; - my $defaultBuckets = $options{DefaultSubsets}; + my $defaultBuckets = $options{DefaultSubsets}; my $defaultShuffledBuckets = []; if (@$defaultBuckets) { for my $defaultBucket (@$defaultBuckets) { - my $shuffledIndices = [ map {$unorder[$_]} @{ $defaultBucket->{indices} } ]; + my $shuffledIndices = + [ map { $unorder[$_] } @{ $defaultBucket->{indices} } ]; my $default_shuffled_bucket = { - label => $defaultBucket->{label}, - indices => $shuffledIndices, + label => $defaultBucket->{label}, + indices => $shuffledIndices, removable => $defaultBucket->{removable}, }; push(@$defaultShuffledBuckets, $default_shuffled_bucket); } } else { - push(@$defaultShuffledBuckets, [ { - label => '', - indices => [ 0..$numProvided-1 ] - } ]); + push( + @$defaultShuffledBuckets, + [ { + label => '', + indices => [ 0 .. $numProvided - 1 ] + } ] + ); } my $answerInputId = main::NEW_ANS_NAME() unless $self->{answerInputId}; - my $ans_rule = main::NAMED_HIDDEN_ANS_RULE($answerInputId); - my $dnd = new DragNDrop( - $answerInputId, - $shuffledSet, - $defaultShuffledBuckets, - AllowNewBuckets => $options{AllowNewBuckets}, - ); + my $ans_rule = main::NAMED_HIDDEN_ANS_RULE($answerInputId); + my $dnd = + new DragNDrop($answerInputId, $shuffledSet, $defaultShuffledBuckets, + AllowNewBuckets => $options{AllowNewBuckets},); my $previous = $main::inputs_ref->{$answerInputId} || ''; if ($previous eq '') { - for my $defaultBucket ( @$defaultShuffledBuckets ) { + for my $defaultBucket (@$defaultShuffledBuckets) { $dnd->addBucket($defaultBucket->{indices}, label => $defaultBucket->{label}); } } else { - my @matches = ( $previous =~ /(\([^\(\)]*\)|-?\d+)+/g ); - for(my $i = 0; $i < @matches; $i++) { - my $match = @matches[$i] =~ s/\(|\)//gr; - my $indices = [ split(',', $match) ]; - my $label = $i < @$defaultShuffledBuckets ? $defaultShuffledBuckets->[$i]->{label} : ''; + my @matches = ($previous =~ /(\([^\(\)]*\)|-?\d+)+/g); + for (my $i = 0; $i < @matches; $i++) { + my $match = @matches[$i] =~ s/\(|\)//gr; + my $indices = [ split(',', $match) ]; + my $label = $i < @$defaultShuffledBuckets ? $defaultShuffledBuckets->[$i]->{label} : ''; my $removable = $i < @$defaultShuffledBuckets ? $defaultShuffledBuckets->[$i]->{removable} : 1; - $dnd->addBucket($indices->[0] != -1 ? $indices : [], label => $label, removable => $removable); + $dnd->addBucket( + $indices->[0] != -1 ? $indices : [], + label => $label, + removable => $removable + ); } } my @shuffled_subsets_array = (); - for my $subset ( @$subsets ) { - my @shuffled_subset = map {$unorder[$_]} @$subset; + for my $subset (@$subsets) { + my @shuffled_subset = map { $unorder[$_] } @$subset; push(@shuffled_subsets_array, @$subset != 0 ? main::Set(join(',', @shuffled_subset)) : main::Set()); } my $shuffled_subsets = main::List(@shuffled_subsets_array); $self = bless { - set => $set, - shuffledSet => $shuffledSet, - numProvided => $numProvided, - order => \@order, - unordered => \@unorder, + set => $set, + shuffledSet => $shuffledSet, + numProvided => $numProvided, + order => \@order, + unordered => \@unorder, shuffled_subsets => $shuffled_subsets, - answerInputId => $answerInputId, - dnd => $dnd, - ans_rule => $ans_rule, - OrderedSubsets => $options{OrderedSubsets}, - AllowNewBuckets => $options{AllowNewBuckets}, + answerInputId => $answerInputId, + dnd => $dnd, + ans_rule => $ans_rule, + OrderedSubsets => $options{OrderedSubsets}, + AllowNewBuckets => $options{AllowNewBuckets}, }, $class; return $self; @@ -255,14 +249,11 @@ sub Print { my $ans_rule = $self->{ans_rule}; if ($main::displayMode ne "TeX") { + # HTML mode - return join("\n", - '
', - $ans_rule, - $self->{dnd}->HTML, - '
', - '
', - ); + return + join("\n", '
', $ans_rule, $self->{dnd}->HTML, + '
', '
',); } else { # TeX mode return $self->{dnd}->TeX; @@ -272,21 +263,23 @@ sub Print { sub cmp { my $self = shift; - return $self->{shuffled_subsets} - ->cmp(ordered => $self->{OrderedSubsets}, removeParens => 1, partialCredit => 1) - ->withPreFilter(sub {$self->prefilter(@_)}) - ->withPostFilter(sub {$self->filter(@_)}); + return $self->{shuffled_subsets}->cmp( + ordered => $self->{OrderedSubsets}, + removeParens => 1, + partialCredit => 1 + )->withPreFilter(sub { $self->prefilter(@_) })->withPostFilter(sub { $self->filter(@_) }); } sub prefilter { - my $self = shift; my $anshash = shift; + my $self = shift; + my $anshash = shift; - my @student = ( $anshash->{original_student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); + my @student = ($anshash->{original_student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g); my @studentAnsArray; - for my $match ( @student ) { + for my $match (@student) { if ($match =~ /-1/) { - push(@studentAnsArray, main::Set()); # index -1 corresponds to empty set + push(@studentAnsArray, main::Set()); # index -1 corresponds to empty set } else { push(@studentAnsArray, main::Set($match =~ s/\(|\)//gr)); } @@ -298,23 +291,29 @@ sub prefilter { } sub filter { - my $self = shift; my $anshash = shift; - - my @order = @{ $self->{order} }; - my @student = ( $anshash->{original_student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g ); - my @correct = ( $anshash->{correct_ans} =~ /({[^{}]*}|-?\d+)/g ); - - $anshash->{correct_ans_latex_string} = join (",", map { - "\\{\\text{".join(",", (map { - $_ != -1 ? $self->{shuffledSet}->[$_] : '' - } (split(',', $_ =~ s/{|}//gr)) ))."}\\}" - } @correct); - - $anshash->{preview_latex_string} = join (",", map { - "\\{\\text{".join(",", (map { - $_ != -1 ? $self->{shuffledSet}->[$_] : '' - } (split(',', $_ =~ s/\(|\)//gr)) ))."}\\}" - } @student); + my $self = shift; + my $anshash = shift; + + my @order = @{ $self->{order} }; + my @student = ($anshash->{original_student_ans} =~ /(\([^\(\)]*\)|-?\d+)/g); + my @correct = ($anshash->{correct_ans} =~ /({[^{}]*}|-?\d+)/g); + + $anshash->{correct_ans_latex_string} = join( + ",", + map { + "\\{\\text{" + . join(",", (map { $_ != -1 ? $self->{shuffledSet}->[$_] : '' } (split(',', $_ =~ s/{|}//gr)))) . "}\\}" + } @correct + ); + + $anshash->{preview_latex_string} = join( + ",", + map { + "\\{\\text{" + . join(",", (map { $_ != -1 ? $self->{shuffledSet}->[$_] : '' } (split(',', $_ =~ s/\(|\)//gr)))) + . "}\\}" + } @student + ); $anshash->{student_ans} = "(see preview)"; From f5cb709169355937b1c7874a68afa1d0df6bf152 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Sat, 1 Jan 2022 11:07:46 -0600 Subject: [PATCH 049/134] Add the files from webwork2 htdocs that are used by pg problems to a new htdocs directory here. All of those files have been deleted from the webwork2 repository by a corresponding pull request. Note that a package.json file has been added that installs jsxgraph. This is not needed for webwork2 as jsxgraph has also been added the the package.json file there. So a webwork2 installation step is not added. This will be needed for the standalone renderer and webwork3 though. The parserGraphTool.pl macro has been updated for this change of location. Also the knowl.js and knowlstyle.css files are moved to htdocs/js/apps/Knowl in the pg htdocs directory. --- .gitignore | 1 + htdocs/helpFiles/Entering-Angles.html | 41 + htdocs/helpFiles/Entering-Decimals.html | 27 + htdocs/helpFiles/Entering-Equations.html | 40 + htdocs/helpFiles/Entering-Exponents.html | 26 + htdocs/helpFiles/Entering-Formulas.html | 68 + htdocs/helpFiles/Entering-Formulas10.html | 67 + htdocs/helpFiles/Entering-Fractions.html | 54 + htdocs/helpFiles/Entering-Inequalities.html | 65 + htdocs/helpFiles/Entering-Intervals.html | 60 + htdocs/helpFiles/Entering-Limits.html | 51 + htdocs/helpFiles/Entering-Logarithms.html | 14 + htdocs/helpFiles/Entering-Logarithms10.html | 13 + htdocs/helpFiles/Entering-Numbers.html | 35 + htdocs/helpFiles/Entering-Points.html | 40 + htdocs/helpFiles/Entering-Syntax.html | 137 + htdocs/helpFiles/Entering-Units.html | 74 + htdocs/helpFiles/Entering-Vectors.html | 48 + htdocs/helpFiles/IntervalNotation.html | 57 + htdocs/helpFiles/PDE-notation.html | 95 + htdocs/helpFiles/Syntax.html | 124 + htdocs/helpFiles/Units.html | 69 + .../apps/AppletSupport/AC_RunActiveContent.js | 292 + .../apps/AppletSupport/ww_applet_support.js | 641 + htdocs/js/apps/GraphTool/graphtool.css | 109 + htdocs/js/apps/GraphTool/graphtool.js | 1349 ++ htdocs/js/apps/GraphTool/graphtool.min.js | 1 + .../js/apps/GraphTool/images/CircleTool.svg | 75 + htdocs/js/apps/GraphTool/images/DashTool.svg | 80 + htdocs/js/apps/GraphTool/images/FillTool.svg | 98 + .../images/HorizontalParabolaTool.svg | 112 + htdocs/js/apps/GraphTool/images/LineTool.svg | 80 + .../js/apps/GraphTool/images/SelectTool.svg | 68 + htdocs/js/apps/GraphTool/images/SolidTool.svg | 80 + .../GraphTool/images/VerticalParabolaTool.svg | 112 + htdocs/js/apps/ImageView/imageview.css | 61 + htdocs/js/apps/ImageView/imageview.js | 234 + htdocs/js/apps/InputColor/color.js | 36 + htdocs/js/apps/Knowl/knowl.js | 149 + htdocs/js/apps/Knowl/knowlstyle.css | 108 + .../js/apps/MathQuill/fonts/Symbola-basic.eot | Bin 0 -> 6828 bytes .../js/apps/MathQuill/fonts/Symbola-basic.ttf | Bin 0 -> 6636 bytes .../apps/MathQuill/fonts/Symbola-basic.woff | Bin 0 -> 4732 bytes .../apps/MathQuill/fonts/Symbola-basic.woff2 | Bin 0 -> 3612 bytes htdocs/js/apps/MathQuill/fonts/Symbola.eot | Bin 0 -> 403238 bytes htdocs/js/apps/MathQuill/fonts/Symbola.svg | 2025 ++ htdocs/js/apps/MathQuill/fonts/Symbola.ttf | Bin 0 -> 403144 bytes htdocs/js/apps/MathQuill/fonts/Symbola.woff | Bin 0 -> 204060 bytes htdocs/js/apps/MathQuill/fonts/Symbola.woff2 | Bin 0 -> 147924 bytes htdocs/js/apps/MathQuill/mathquill.css | 507 + htdocs/js/apps/MathQuill/mathquill.js | 5276 +++++ htdocs/js/apps/MathQuill/mathquill.min.js | 12 + htdocs/js/apps/MathQuill/mqeditor.css | 139 + htdocs/js/apps/MathQuill/mqeditor.js | 179 + htdocs/js/apps/MathView/mathview.css | 306 + htdocs/js/apps/MathView/mathview.js | 518 + htdocs/js/apps/MathView/mv_locale_us.js | 348 + htdocs/js/apps/Scaffold/scaffold.css | 80 + htdocs/js/apps/Scaffold/scaffold.js | 18 + htdocs/js/apps/WirisEditor/correct.gif | Bin 0 -> 549 bytes htdocs/js/apps/WirisEditor/editor.gif | Bin 0 -> 1048 bytes htdocs/js/apps/WirisEditor/editor16.png | Bin 0 -> 489 bytes htdocs/js/apps/WirisEditor/editor16b.png | Bin 0 -> 447 bytes htdocs/js/apps/WirisEditor/editor16w.png | Bin 0 -> 407 bytes .../apps/WirisEditor/fieldset-collapsed.png | Bin 0 -> 245 bytes .../js/apps/WirisEditor/fieldset-expanded.png | Bin 0 -> 246 bytes htdocs/js/apps/WirisEditor/incorrect.gif | Bin 0 -> 616 bytes htdocs/js/apps/WirisEditor/integration.ini | 121 + htdocs/js/apps/WirisEditor/loading.gif | Bin 0 -> 673 bytes htdocs/js/apps/WirisEditor/manual.png | Bin 0 -> 661 bytes htdocs/js/apps/WirisEditor/mathml2webwork.js | 9601 ++++++++ htdocs/js/apps/WirisEditor/percent.gif | Bin 0 -> 571 bytes htdocs/js/apps/WirisEditor/popup.html | 1 + htdocs/js/apps/WirisEditor/poweredbywiris.png | Bin 0 -> 1713 bytes htdocs/js/apps/WirisEditor/quizzes.gif | Bin 0 -> 1045 bytes htdocs/js/apps/WirisEditor/quizzes.js | 19383 ++++++++++++++++ htdocs/js/apps/WirisEditor/refresh24b.png | Bin 0 -> 1044 bytes htdocs/js/apps/WirisEditor/router.xml | 12 + htdocs/js/apps/WirisEditor/studio.gif | Bin 0 -> 854 bytes htdocs/js/apps/WirisEditor/studio16.png | Bin 0 -> 604 bytes htdocs/js/apps/WirisEditor/studio16b.png | Bin 0 -> 562 bytes htdocs/js/apps/WirisEditor/studio24.png | Bin 0 -> 958 bytes htdocs/js/apps/WirisEditor/studio24b.png | Bin 0 -> 889 bytes htdocs/js/apps/WirisEditor/uparrow24b.png | Bin 0 -> 800 bytes htdocs/js/apps/WirisEditor/version.txt | 1 + htdocs/js/apps/WirisEditor/warning.png | Bin 0 -> 491 bytes htdocs/js/apps/WirisEditor/warning2.png | Bin 0 -> 493 bytes htdocs/js/apps/WirisEditor/warning3.png | Bin 0 -> 477 bytes htdocs/js/apps/WirisEditor/wiriseditor.js | 72 + htdocs/js/apps/WirisEditor/wirisquizzes.css | 847 + htdocs/package.json | 12 + macros/parserGraphTool.pl | 4 +- 92 files changed, 44351 insertions(+), 2 deletions(-) create mode 100644 htdocs/helpFiles/Entering-Angles.html create mode 100644 htdocs/helpFiles/Entering-Decimals.html create mode 100644 htdocs/helpFiles/Entering-Equations.html create mode 100644 htdocs/helpFiles/Entering-Exponents.html create mode 100644 htdocs/helpFiles/Entering-Formulas.html create mode 100644 htdocs/helpFiles/Entering-Formulas10.html create mode 100644 htdocs/helpFiles/Entering-Fractions.html create mode 100644 htdocs/helpFiles/Entering-Inequalities.html create mode 100644 htdocs/helpFiles/Entering-Intervals.html create mode 100644 htdocs/helpFiles/Entering-Limits.html create mode 100644 htdocs/helpFiles/Entering-Logarithms.html create mode 100644 htdocs/helpFiles/Entering-Logarithms10.html create mode 100644 htdocs/helpFiles/Entering-Numbers.html create mode 100644 htdocs/helpFiles/Entering-Points.html create mode 100644 htdocs/helpFiles/Entering-Syntax.html create mode 100644 htdocs/helpFiles/Entering-Units.html create mode 100644 htdocs/helpFiles/Entering-Vectors.html create mode 100644 htdocs/helpFiles/IntervalNotation.html create mode 100644 htdocs/helpFiles/PDE-notation.html create mode 100644 htdocs/helpFiles/Syntax.html create mode 100644 htdocs/helpFiles/Units.html create mode 100755 htdocs/js/apps/AppletSupport/AC_RunActiveContent.js create mode 100644 htdocs/js/apps/AppletSupport/ww_applet_support.js create mode 100644 htdocs/js/apps/GraphTool/graphtool.css create mode 100644 htdocs/js/apps/GraphTool/graphtool.js create mode 100644 htdocs/js/apps/GraphTool/graphtool.min.js create mode 100644 htdocs/js/apps/GraphTool/images/CircleTool.svg create mode 100644 htdocs/js/apps/GraphTool/images/DashTool.svg create mode 100644 htdocs/js/apps/GraphTool/images/FillTool.svg create mode 100644 htdocs/js/apps/GraphTool/images/HorizontalParabolaTool.svg create mode 100644 htdocs/js/apps/GraphTool/images/LineTool.svg create mode 100644 htdocs/js/apps/GraphTool/images/SelectTool.svg create mode 100644 htdocs/js/apps/GraphTool/images/SolidTool.svg create mode 100644 htdocs/js/apps/GraphTool/images/VerticalParabolaTool.svg create mode 100644 htdocs/js/apps/ImageView/imageview.css create mode 100644 htdocs/js/apps/ImageView/imageview.js create mode 100644 htdocs/js/apps/InputColor/color.js create mode 100644 htdocs/js/apps/Knowl/knowl.js create mode 100644 htdocs/js/apps/Knowl/knowlstyle.css create mode 100644 htdocs/js/apps/MathQuill/fonts/Symbola-basic.eot create mode 100644 htdocs/js/apps/MathQuill/fonts/Symbola-basic.ttf create mode 100644 htdocs/js/apps/MathQuill/fonts/Symbola-basic.woff create mode 100644 htdocs/js/apps/MathQuill/fonts/Symbola-basic.woff2 create mode 100644 htdocs/js/apps/MathQuill/fonts/Symbola.eot create mode 100644 htdocs/js/apps/MathQuill/fonts/Symbola.svg create mode 100644 htdocs/js/apps/MathQuill/fonts/Symbola.ttf create mode 100644 htdocs/js/apps/MathQuill/fonts/Symbola.woff create mode 100644 htdocs/js/apps/MathQuill/fonts/Symbola.woff2 create mode 100644 htdocs/js/apps/MathQuill/mathquill.css create mode 100644 htdocs/js/apps/MathQuill/mathquill.js create mode 100644 htdocs/js/apps/MathQuill/mathquill.min.js create mode 100644 htdocs/js/apps/MathQuill/mqeditor.css create mode 100644 htdocs/js/apps/MathQuill/mqeditor.js create mode 100644 htdocs/js/apps/MathView/mathview.css create mode 100644 htdocs/js/apps/MathView/mathview.js create mode 100644 htdocs/js/apps/MathView/mv_locale_us.js create mode 100644 htdocs/js/apps/Scaffold/scaffold.css create mode 100644 htdocs/js/apps/Scaffold/scaffold.js create mode 100755 htdocs/js/apps/WirisEditor/correct.gif create mode 100755 htdocs/js/apps/WirisEditor/editor.gif create mode 100755 htdocs/js/apps/WirisEditor/editor16.png create mode 100755 htdocs/js/apps/WirisEditor/editor16b.png create mode 100755 htdocs/js/apps/WirisEditor/editor16w.png create mode 100755 htdocs/js/apps/WirisEditor/fieldset-collapsed.png create mode 100755 htdocs/js/apps/WirisEditor/fieldset-expanded.png create mode 100755 htdocs/js/apps/WirisEditor/incorrect.gif create mode 100755 htdocs/js/apps/WirisEditor/integration.ini create mode 100755 htdocs/js/apps/WirisEditor/loading.gif create mode 100755 htdocs/js/apps/WirisEditor/manual.png create mode 100644 htdocs/js/apps/WirisEditor/mathml2webwork.js create mode 100755 htdocs/js/apps/WirisEditor/percent.gif create mode 100755 htdocs/js/apps/WirisEditor/popup.html create mode 100755 htdocs/js/apps/WirisEditor/poweredbywiris.png create mode 100755 htdocs/js/apps/WirisEditor/quizzes.gif create mode 100755 htdocs/js/apps/WirisEditor/quizzes.js create mode 100755 htdocs/js/apps/WirisEditor/refresh24b.png create mode 100755 htdocs/js/apps/WirisEditor/router.xml create mode 100755 htdocs/js/apps/WirisEditor/studio.gif create mode 100755 htdocs/js/apps/WirisEditor/studio16.png create mode 100755 htdocs/js/apps/WirisEditor/studio16b.png create mode 100755 htdocs/js/apps/WirisEditor/studio24.png create mode 100755 htdocs/js/apps/WirisEditor/studio24b.png create mode 100755 htdocs/js/apps/WirisEditor/uparrow24b.png create mode 100755 htdocs/js/apps/WirisEditor/version.txt create mode 100755 htdocs/js/apps/WirisEditor/warning.png create mode 100755 htdocs/js/apps/WirisEditor/warning2.png create mode 100755 htdocs/js/apps/WirisEditor/warning3.png create mode 100755 htdocs/js/apps/WirisEditor/wiriseditor.js create mode 100755 htdocs/js/apps/WirisEditor/wirisquizzes.css create mode 100644 htdocs/package.json diff --git a/.gitignore b/.gitignore index 0a2e6dad2e..ed2cb50b4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ lib/chromatic/color conf/*.yml +htdocs/package-lock.json diff --git a/htdocs/helpFiles/Entering-Angles.html b/htdocs/helpFiles/Entering-Angles.html new file mode 100644 index 0000000000..4649183652 --- /dev/null +++ b/htdocs/helpFiles/Entering-Angles.html @@ -0,0 +1,41 @@ + +
+Entering Angles +
+ +
    + +
  • Angles in radians without units are the default: +
    +For an angle of 60 degrees, enter it in radians as   pi/3   or   1.04719..., but not 60
    +By default, trig functions are evaluated in radians, so cos(pi/3) = 1/2, but cos(60) = -0.9524 since it is radians. You must convert degrees to radians before applying a trig function to an angle. +
    +
  • + +
  • Occasionally, units are required on angles: +
    +If asked for units on an angle, enter, for example,
    +pi/6 rad   (including rad)
    +30 deg   (including deg) +
    +
  • + +
  • Examples of constants available: +
    pi,   e = e^1
    +
  • + +
  • Sometimes decimals are not allowed: +
    Sometimes   pi/6   is allowed, but   0.524   is not
    +
  • + +
  • Sometimes trig functions are not allowed: +
    +Sometimes   0.866025403784   is allowed, but   cos(pi/6)   is not +
    +
  • + +
  • Link to a list of all available functions
  • + + +
+ diff --git a/htdocs/helpFiles/Entering-Decimals.html b/htdocs/helpFiles/Entering-Decimals.html new file mode 100644 index 0000000000..8f089c4ad5 --- /dev/null +++ b/htdocs/helpFiles/Entering-Decimals.html @@ -0,0 +1,27 @@ + +
+Entering decimals +
+ +
    + +
  • In general, give at least 5 decimal places. +
    Typically, if your answer is correct to 5 decimal places it will be marked correct, although the number of decimal places required may vary from problem to problem. When in doubt, give more decimal places.
    +
  • + +
  • If there is more than one correct answer, enter your answers as a comma separated list. +
    +For example, if your answers are -3/2, 4/3, 2pi, e^3, 5 enter them as +-1.5, 1.3333333, 6.2831853, 20.0855369, 5 +
    + +
  • Sometimes, fractions and certain operations are not allowed. +
    Usually, the operations that are not allowed include addition +, subtraction -, multiplication *, division /, and exponentiation ^ (or **). When these operations are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
  • Sometimes, certain functions are not allowed. +
    Usually, the functions that are not allowed include square root sqrt( ), absolute value | | (or abs( )), as well as other named functions such as sin( ), ln( ), etc. When these functions are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
+ diff --git a/htdocs/helpFiles/Entering-Equations.html b/htdocs/helpFiles/Entering-Equations.html new file mode 100644 index 0000000000..e09249c9d4 --- /dev/null +++ b/htdocs/helpFiles/Entering-Equations.html @@ -0,0 +1,40 @@ +
+Entering Equations +
+ +
    + +
  • Equations must have an equals sign and use the correct variable names: +
    +y = 5x+2   will be incorrect if the answer is   w = 5y+2 +
    +
  • + +
  • Examples of valid equations that are equivalent: +
    +32 = 5*x + 2   is the same as   30 = 5x   or   x = 6
    +y = (x-1)^2 + 3   is the same as   y - 3 = (x-1)^2
    +x^2 + xy + y^2 = 13x   is the same as   y*(y+x) = 13x - x^2
    +
    +
  • + +
  • If there is no equation that solves the question: +
    +Enter   NONE   or   DNE   (this may vary from problem to problem) +
    +
  • + +
  • Examples of constants used in equations: +
    pi, e = e^1
    +
  • + +
  • Functions may be used in equations, but may not be applied across the equals sign: +
    +sqrt(x) = sqrt(5)   is valid, but   sqrt(x=5)   is not +
    +
    +Link to a list of all available functions +
    +
  • + +
diff --git a/htdocs/helpFiles/Entering-Exponents.html b/htdocs/helpFiles/Entering-Exponents.html new file mode 100644 index 0000000000..5533c2d7f6 --- /dev/null +++ b/htdocs/helpFiles/Entering-Exponents.html @@ -0,0 +1,26 @@ +
+Entering exponents +
+ +
    + +
  • Both ^ and ** are used for exponentiation. +
    For example, x^2 and x**2 are the same, as are e^(-x/2) and 1/(e**(x/2))
    +
  • + +
  • Square roots have a named function, but other roots do not and should be entered using fractional exponents. +
    +For example, the square root of 2 can be entered as sqrt(2), 2^(1/2), or 2**(1/2), but the cube root of 2 must be entered as 2^(1/3) or 2**(1/3). The parentheses in 2^(1/3) are required, since 2^1/3 will be interpreted as (2^1)/3 = 2/3. +
    +
  • + +
  • Sometimes, fractional exponents and certain operations are not allowed. +
    Usually, the operations that are not allowed include addition +, subtraction -, multiplication *, division /. When these operations are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
  • Sometimes, certain functions are not allowed. +
    Usually, the functions that are not allowed include square root sqrt( ), absolute value | | (or abs( )), as well as other named functions such as sin( ), ln( ), etc. When these functions are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
+ diff --git a/htdocs/helpFiles/Entering-Formulas.html b/htdocs/helpFiles/Entering-Formulas.html new file mode 100644 index 0000000000..220e60e61c --- /dev/null +++ b/htdocs/helpFiles/Entering-Formulas.html @@ -0,0 +1,68 @@ + +
+Entering Formulas +
+ +
    + +
  • Link to a list of all available functions

  • + +
  • Formulas must use the correct variable(s): +
    +For example, a function of time   t   could be   -16t^2 + 12, while   -16x^2 + 12   would be incorrect. +
    +
  • + +
  • Examples of valid formulas: +
    +5*sin((pi*x)/2)   or   5 sin(pi x/2)
    +e^(-x)   or   e**(-x)   or   1/(e^x)
    +abs(5y)   or   |5y|
    +sqrt(9 - z^2)   or   (9 - z^2)^(1/2)
    +24   or   4!   or   4 * 3 * 2 * 1
    +pi   or   4 arctan(1)   or   4 atan(1)   or   4 tan^(-1)(1)
    +
    +
  • + +
  • Entering logarithms: +
    In this question, use ln(x) or log(x) +for natural log, +and logten(x) or log10(x) for +the base 10 logarithm. Enter log base b as ln(x)/ln(b). +
    +
  • + +
  • Examples of constants used in formulas: +
    pi, e = e^1
    +
  • + +
  • Examples of operations used in formulas: +
    Addition +, subtraction -, multiplication *, division /, exponentiation ^ (or **), factorial ! +
    +
  • + +
  • Examples of functions used in formulas: +
    +sqrt(x) = x^(1/2), abs(x) = | x |
    +2^x, e^x, ln(x), log10(x)
    +sin(x), cos(x), tan(x), csc(x), sec(x), cot(x)
    +arcsin(x) = asin(x) = sin^(-1)(x)
    +arccos(x) = acos(x) = cos^(-1)(x)
    +arctan(x) = atan(x) = tan^(-1)(x)
    +
    + +
  • Sometimes formulas must be simplified: +
    +For example,   6x + 5 - 2x   should be simplified to   4x + 5 +
    +
  • + +
  • Sometimes, certain operations are not allowed. +
    Usually, the operations that are not allowed include addition +, subtraction -, multiplication *, division /, and exponentiation ^ (or **). When these operations are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
  • Sometimes, certain functions are not allowed. +
    Usually, the functions that are not allowed include square root sqrt( ), absolute value | | (or abs( )), as well as other named functions such as sin( ), ln( ), etc. When these functions are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
diff --git a/htdocs/helpFiles/Entering-Formulas10.html b/htdocs/helpFiles/Entering-Formulas10.html new file mode 100644 index 0000000000..5ae280074e --- /dev/null +++ b/htdocs/helpFiles/Entering-Formulas10.html @@ -0,0 +1,67 @@ + +
+Entering Formulas +
+ +
    + +
  • Link to a list of all available functions

  • + +
  • Formulas must use the correct variable(s): +
    +For example, a function of time   t   could be   -16t^2 + 12, while   -16x^2 + 12   would be incorrect. +
    +
  • + +
  • Examples of valid formulas: +
    +5*sin((pi*x)/2)   or   5 sin(pi x/2)
    +e^(-x)   or   e**(-x)   or   1/(e^x)
    +abs(5y)   or   |5y|
    +sqrt(9 - z^2)   or   (9 - z^2)^(1/2)
    +24   or   4!   or   4 * 3 * 2 * 1
    +pi   or   4 arctan(1)   or   4 atan(1)   or   4 tan^(-1)(1)
    +
    +
  • + +
  • Entering logarithms: +
    In this question, use ln(x) for natural log, +and log(x), logten(x) or log10(x) for +the base 10 logarithm. Enter log base b as ln(x)/ln(b). +
    +
  • + +
  • Examples of constants used in formulas: +
    pi, e = e^1
    +
  • + +
  • Examples of operations used in formulas: +
    Addition +, subtraction -, multiplication *, division /, exponentiation ^ (or **), factorial ! +
    +
  • + +
  • Examples of functions used in formulas: +
    +sqrt(x) = x^(1/2), abs(x) = | x |
    +2^x, e^x, ln(x), log10(x)
    +sin(x), cos(x), tan(x), csc(x), sec(x), cot(x)
    +arcsin(x) = asin(x) = sin^(-1)(x)
    +arccos(x) = acos(x) = cos^(-1)(x)
    +arctan(x) = atan(x) = tan^(-1)(x)
    +
    + +
  • Sometimes formulas must be simplified: +
    +For example,   6x + 5 - 2x   should be simplified to   4x + 5 +
    +
  • + +
  • Sometimes, certain operations are not allowed. +
    Usually, the operations that are not allowed include addition +, subtraction -, multiplication *, division /, and exponentiation ^ (or **). When these operations are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
  • Sometimes, certain functions are not allowed. +
    Usually, the functions that are not allowed include square root sqrt( ), absolute value | | (or abs( )), as well as other named functions such as sin( ), ln( ), etc. When these functions are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
diff --git a/htdocs/helpFiles/Entering-Fractions.html b/htdocs/helpFiles/Entering-Fractions.html new file mode 100644 index 0000000000..6aaa6b0638 --- /dev/null +++ b/htdocs/helpFiles/Entering-Fractions.html @@ -0,0 +1,54 @@ +
+Entering fractions +
+ +
    + +
  • Examples of fractions, which are of the form a / b for non-decimal numbers a and b that have no common factors, include: +
    +5/2, -1/3, pi/3, 4, sqrt(2)/2 +
    +
  • + +
  • Examples of fractions that can be simplified include: +
    +15/6, (3-4)/3, 2*pi/6, (16/2)/2 +
    +
  • + +
  • Sometimes decimals are not allowed: +
    +Allowed:    5/2, -1/3, pi/3, 4, sqrt(2)/2, 2^(1/2)
    +Not allowed:    2.5, -0.33333, 3.14159/3, 0.707106/2, 2^(0.5) +
    +
  • + +
  • Sometimes a mixed fraction is required: +
    Enter 1 2/3 (for 1 and 2/3) with a space between the 1 and the 2 instead of 5/3
    +
  • + +
  • Sometimes, you must make an integer into a fraction: +
    Enter 4/1 instead of 4
    +
  • + +
  • If there is more than one correct answer, enter your answers as a comma separated list. +
    +For example, if your answers are -3/2, 4/3, 2pi, e^3, 5 enter +3/2, 4/3, 2*pi, e^3, 5 +
    + +
  • If there are no solutions: +
    +Enter   NONE   or   DNE   (this may vary from problem to problem) +
    +
  • + +
  • Sometimes, certain operations are not allowed. +
    Usually, the operations that are not allowed include addition +, subtraction -, multiplication *, and exponentiation ^ (or **). When these operations are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
  • Sometimes, certain functions are not allowed. +
    Usually, the functions that are not allowed include square root sqrt( ), absolute value | | (or abs( )), as well as other named functions such as sin( ), ln( ), etc. When these functions are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
diff --git a/htdocs/helpFiles/Entering-Inequalities.html b/htdocs/helpFiles/Entering-Inequalities.html new file mode 100644 index 0000000000..a5d2047060 --- /dev/null +++ b/htdocs/helpFiles/Entering-Inequalities.html @@ -0,0 +1,65 @@ +
+Entering inequalities +
+ +
    + +
  • Types of operators: +
    + + + + + + + +
    <    less than
    <=    less than or equal to (  =<   might also work)
    =    equals
    !=    not equal to (uses exclamation point)
    >    greater than
    >=    greater than or equal to (  =>   might also work)
    +
    +
  • + +
  • Special symbols: +
    +infinity   or   inf   means positive infinity
    +-infinity   or   -inf   means negative infinity
    +R   means all real numbers
    +R   is the same as   -inf < x < inf   or   (-inf,inf)
    +{2,4,5}   using curly braces denotes a finite set
    +NONE   or a pair of curly braces   {}   means the empty set
    +U   denotes the union of intervals +
    +
  • + +
  • Entering answers using inequality or interval notation: +
    + + + + + + + + + + + + + + + +
    Inequality
    Notation
    * Interval
    Notation
    Remarks
    x<2(-infinity,2)Use rounded parentheses (   or   ) at infinite endpoints
    x>2(2,infinity) 
    x<=2(-infinity,2] 
    x>=2[2,infinity) 
    0<x<=2(0,2] 
    0<x and x<2(0,2)and   is special
    x<0 or x>2(-inf,0)U(2,inf)or   is special
    U   denotes union
    x=0 or x=2{0,2}finite sets are allowed using curly braces   {a,b,c}
    x<3 or x>3(-inf,3)U(3,inf)
    x != 3
    R-{3}
    set differences are allowed
    +
    +* Some questions may not allow interval notation to be used +
    +
  • + +
  • Tips for entering inequalities and intervals: +
    +If an interval includes an endpoint, use square brackets: [   or   ]

    +If an interval excludes an endpoint or an endpoint is infinite, use rounded parentheses: (   or   )

    +Use curly braces to enclose finite sets and commas to separate elements the set: { -3, pi, 2/5, 0.75 }

    +All sets should be expressed in their simplest form in interval notation with no overlapping intervals. For example,   [2,4]U[3,5]   is not equivalent to   [2,5]

    +If you are asked to find the range of a function y = f(x), your inequality should be in terms of the variable y +
    +
  • + +
diff --git a/htdocs/helpFiles/Entering-Intervals.html b/htdocs/helpFiles/Entering-Intervals.html new file mode 100644 index 0000000000..4c9e3f7e46 --- /dev/null +++ b/htdocs/helpFiles/Entering-Intervals.html @@ -0,0 +1,60 @@ +

+Using Interval Notation +

+ +
    + +
  • If an endpoint is included, then use [ or ]. +If not, then use ( or ). For example, the interval +from -3 to 7 that includes 7 but not -3 is expressed (-3,7]. + + +
    +
    + +
  • For infinite intervals, use Inf +for (infinity) and/or +-Inf for -∞ (-Infinity). For +example, the infinite interval containing all points greater than or +equal to 6 is expressed [6,Inf). + + +
    +
    + +
  • If the set includes more than one interval, they are joined using the union +symbol U. For example, the set consisting of all points in (-3,7] together with all points in [-8,-5) is expressed [-8,-5)U(-3,7]. + +
    +
    + +
  • If the answer is the empty set, you can specify that by using + braces with nothing inside: { } + +
    + +
    + +
  • You can use R as a shorthand for all real numbers. + So, it is equivalent to entering (-Inf, Inf). + +
    +
    + +
  • You can use set difference notation. So, for all real numbers + except 3, you can use R-{3} or + (-Inf, 3)U(3,Inf) (they are the same). Similarly, + [1,10)-{3,4} is the same as [1,3)U(3,4)U(4,10). + + +
    +
    + + +
  • WeBWorK will not interpret [2,4]U[3,5] as equivalent + to [2,5], unless a problem tells you otherwise. +All sets should be expressed in their simplest interval notation form, with no +overlapping intervals. + +
+ diff --git a/htdocs/helpFiles/Entering-Limits.html b/htdocs/helpFiles/Entering-Limits.html new file mode 100644 index 0000000000..74c70c7fb5 --- /dev/null +++ b/htdocs/helpFiles/Entering-Limits.html @@ -0,0 +1,51 @@ +
+Entering Limits +
+ +
    + +
  • Limits whose values are numbers: +
    +For example, limx → ∞ arctan(x) = π/2, so you would enter   pi/2 +
    +
  • + +
  • Limits whose values are infinite: +
    +Enter   infinity,   inf,   -infinity,   -inf +
    +
  • + +
  • Limits that don't exist: +
    +Enter   DNE   or   NONE   (this may vary from question to question) +
    +
  • + +
  • Limits whose value is a function: +
    +Enter the function using appropriate syntax, for example:
    +sqrt(x) = x^(1/2), abs(x) = | x |
    +2^x, e^x, ln(x), log10(x)
    +sin(x), cos(x), tan(x), csc(x), sec(x), cot(x)
    +arcsin(x) = asin(x) = sin^(-1)(x)
    +arccos(x) = acos(x) = cos^(-1)(x)
    +arctan(x) = atan(x) = tan^(-1)(x)
    +
    +
  • + +
  • Sometimes answers must be simplified: +
    +For example,   6x+5-2x+7   should be simplified to   4x+12 +
    +
  • + +
  • Sometimes, certain operations are not allowed. +
    Usually, the operations that are not allowed include addition +, subtraction -, multiplication *, division /, and exponentiation ^ (or **). When these operations are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
  • Sometimes, certain functions are not allowed. +
    Usually, the functions that are not allowed include square root sqrt( ), absolute value | | (or abs( )), as well as other named functions such as sin( ), ln( ), etc. When these functions are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
diff --git a/htdocs/helpFiles/Entering-Logarithms.html b/htdocs/helpFiles/Entering-Logarithms.html new file mode 100644 index 0000000000..7a16eb68b3 --- /dev/null +++ b/htdocs/helpFiles/Entering-Logarithms.html @@ -0,0 +1,14 @@ +
+

Help Entering Logarithms

+
+
    +
  • Entering natural logarithm: In this question, use ln(x) or log(x). +

  • +
  • Entering base 10 logarithm: In this question, use log10(x) or logten(x).

  • +
  • Entering logarithms base b:   ln(x)/ln(b)   or   log(x)/log(b)
    WeBWorK does not recognize logarithms to other bases, so you must use the change of base formula for logarithms to enter your answer. For example, enter log base 2 of x as

    ln(x)/ln(2)    or    log(x)/log(2)
  • +
  • Put parentheses around the arguments to logs:
    ln(2x+8)   and   ln2x+8 = ln(2)*x+8   are very different.
  • +
  • Sometimes logarithms must be simplified or expanded:
    For example, the required answer may be   ln(6) + ln(x)   or   ln(6x)
  • +
  • Sometimes, certain operations are not allowed.
    Usually, the operations that are not allowed include addition +, subtraction -, multiplication *, division /, and exponentiation ^ (or **). When these operations are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
  • +
  • Sometimes, certain functions are not allowed.
    Usually, the functions that are not allowed include square root sqrt( ), absolute value | | (or abs( )), as well as other named functions such as sin( ), ln( ), etc. When these functions are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
  • +
  • Link to a list of all available functions

  • +
diff --git a/htdocs/helpFiles/Entering-Logarithms10.html b/htdocs/helpFiles/Entering-Logarithms10.html new file mode 100644 index 0000000000..23aa222873 --- /dev/null +++ b/htdocs/helpFiles/Entering-Logarithms10.html @@ -0,0 +1,13 @@ +
+

Help Entering Logarithms

+
+
    +
  • Entering natural logarithm: In this question, use ln(x)

  • +
  • Entering base 10 logarithm: In this question, use log(x), log10(x), or logten(x).

  • +
  • Entering logarithms base b:   ln(x)/ln(b)   or   log(x)/log(b)
    WeBWorK does not recognize logarithms to other bases, so you must use the change of base formula for logarithms to enter your answer. For example, enter log base 2 of x as

    ln(x)/ln(2)    or    log(x)/log(2)
  • +
  • Put parentheses around the arguments to logs:
    ln(2x+8)   and   ln2x+8 = ln(2)*x+8   are very different.
  • +
  • Sometimes logarithms must be simplified or expanded:
    For example, the required answer may be   ln(6) + ln(x)   or   ln(6x)
  • +
  • Sometimes, certain operations are not allowed.
    Usually, the operations that are not allowed include addition +, subtraction -, multiplication *, division /, and exponentiation ^ (or **). When these operations are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
  • +
  • Sometimes, certain functions are not allowed.
    Usually, the functions that are not allowed include square root sqrt( ), absolute value | | (or abs( )), as well as other named functions such as sin( ), ln( ), etc. When these functions are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
  • +
  • Link to a list of all available functions

  • +
diff --git a/htdocs/helpFiles/Entering-Numbers.html b/htdocs/helpFiles/Entering-Numbers.html new file mode 100644 index 0000000000..37539df6ed --- /dev/null +++ b/htdocs/helpFiles/Entering-Numbers.html @@ -0,0 +1,35 @@ +
+Entering numbers +
+ +
    + +
  • Examples of (real) numbers include: +
    4, 5/2, -1/3, pi/3, e^3, 3.1415926535, sqrt(2) = 2^(1/2), ln(2), sin(2pi/3)
    +
  • + +
  • If there is more than one correct answer, enter your answers as a comma separated list. +
    +For example, enter   -1.5, 4/3, 2pi, e^3, 5
    +Do not use commas in large numbers: enter   4321   (not   4,321) +
    + +
  • If there are no solutions: +
    +Enter   NONE   or   DNE   (this may vary from problem to problem) +
    +
  • + +
  • If your answer is a decimal, give at least 5 decimal places. +
    Typically, if your answer is correct to 5 decimal places it will be marked correct, although the number of decimal places required may vary from problem to problem. When in doubt, give more decimal places.
    +
  • + +
  • Sometimes, fractions and certain operations are not allowed. +
    Usually, the operations that are not allowed include addition +, subtraction -, multiplication *, division /, and exponentiation ^ (or **). When these operations are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
  • Sometimes, certain functions are not allowed. +
    Usually, the functions that are not allowed include square root sqrt( ), absolute value | | (or abs( )), as well as other named functions such as sin( ), ln( ), etc. When these functions are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
    +
  • + +
diff --git a/htdocs/helpFiles/Entering-Points.html b/htdocs/helpFiles/Entering-Points.html new file mode 100644 index 0000000000..d80f25431f --- /dev/null +++ b/htdocs/helpFiles/Entering-Points.html @@ -0,0 +1,40 @@ +
+Entering Points +
+ +
    + +
  • A point must use parentheses and commas: +
    +(4.5, 3/7)   is a valid point in 2 dimensions
    +(pi,e,2)   is a valid point in 3 dimensions +
    +
  • + +
  • If the answer is more than one point: +
    +Enter your answer as a comma-separated list of points, for example:    +(4,3), (5,10) +
    +
  • + +
  • If there are no solutions: +
    +Enter   NONE   or   DNE   (this may vary from problem to problem) +
    +
  • + +
  • Examples of constants used in points: +
    pi, e = e^1
    +
  • + +
  • Functions may be used in each coordinate of a point, but may not be applied across the parentheses or commas: +
    +(sqrt(2),sqrt(5))   is valid, but   (sqrt(2,5))   is not +
    +
    +Link to a list of all available functions +
    +
  • + +
diff --git a/htdocs/helpFiles/Entering-Syntax.html b/htdocs/helpFiles/Entering-Syntax.html new file mode 100644 index 0000000000..f6154508db --- /dev/null +++ b/htdocs/helpFiles/Entering-Syntax.html @@ -0,0 +1,137 @@ +

Syntax for entering answers to WeBWorK

+ + +

Mathematical Symbols Available In WeBWorK

+ +
  • + Addition +
  • - Subtraction +
  • * Multiplication can also be indicated by a space or juxtaposition, e.g. 2x, 2 x or 2*x, also 2(3+4). +
  • / Division +
  • ^ or ** You can use either ^ or ** for exponentiation, e.g. 3^2 or 3**2 +
  • Parentheses: () - You can also use square brackets, [ ], and braces, { }, for grouping, e.g. [1+2]/[3(4+5)] +
+

Syntax for entering expressions

+
  • Be careful entering expressions just as you would be careful entering expressions in a calculator. + +
  • Use the "Preview Button" to see exactly how your entry +looks. E.g. to tell the difference between 1+2/3*4 and [1+2]/[3*4] +click the "Preview Button". +
  • Sometimes using the * symbol to indicate mutiplication makes +things easier to read. For example (1+2)*(3+4) and (1+2)(3+4) are both +valid. So are 3*4 and 3 4 (3 space 4, not 34) but using a * makes +things clearer. +
  • Use ('s and )'s to make your meaning clear. You can also use ['s and ]'s and {'s and }'s. +
  • Don't enter 2/4+5 (which is 5.5) when you really want 2/(4+5) (which is 2/9). +
  • Don't enter 2/3*4 (which is 8/3) when you really want 2/(3*4) (which is 2/12). +
  • Entering big quotients with square brackets, e.g. [1+2+3+4]/[5+6+7+8], is a good practice. +
  • Be careful when entering functions. It's always good practice +to use parentheses when entering functions. Write sin(t) instead of +sint or sin t even though WeBWorK is smart enough to usually accept sin t or even sint. For example, sin 2t is interpreted as sin(2)t, i.e. (sin(2))*t so be careful. + +
  • You can enter sin^2(t) as a short cut although mathematically +speaking sin^2(t) is shorthand for (sin(t))^2(the square of sin of t). +(You can enter it as sin(t)^2 or even sint^2, but don't try such things +unless you really understand the precedence of operations. The +"sin" operation has highest precedence, so it is performed first, using +the next token (i.e. t) as an argument. Then the result is squared.) +You can always use the Preview button to see a typeset version of what +you entered and check whether what you wrote was what you +meant. :-) +
  • For example 2+3sin^2(4x) will work and is equivalent to +2+3(sin(4x))^2 or 2+3sin(4x)^2. Why does the last expression work? +Because things in parentheses are always done first [ i.e. (4x)], next +all functions, such as sin, are evaluated [giving sin(4x)], next all +exponents are taken [giving sin(4x)^2], next all multiplications and +divisions are performed in order from left to right [giving +3sin(4x)^2], and finally all additions and subtractions are performed +[giving 2+3sin(4x)^2]. +
  • Is -5^2 positive or negative? It's negative. This is because +the square operation is done before the negative sign is applied. Use +(-5)^2 if you want to square negative 5. +
  • When in doubt use parentheses!!! :-) +
  • The complete rules for the precedence of operations, in addition to the above, are +
    • Multiplications and divisions are performed left to right: 2/3*4 = (2/3)*4 = 8/3. + +
    • Additions and subtractions are performed left to right: 1-2+3 = (1-2)+3 = 2. +
    • Exponents are taken right to left: 2^3^4 = 2^(3^4) = 2^81 = a big number. +
    +
  • Use the "Preview Button" to see exactly how your entry +looks. E.g. to tell the difference between 1+2/3*4 and [1+2]/[3*4] +click the "Preview Button". +
+

Mathematical Constants Available In WeBWorK

+
  • pi This gives 3.14159265358979, e.g. cos(pi) is -1 +
  • e This gives 2.71828182845905, e.g. ln(e*2) is 1 + ln(2) +
+ +

Scientific Notation Available In WeBWorK

+
  • 2.1E2 is the same as 210 +
  • 2.1E-2 is the same as .021 +
+

Mathematical Functions Available In WeBWorK

+

Note that sometimes one or more of these functions is disabled for a WeBWorK problem because the +instructor wants you to calculate the answer by some means other than just using the function. +

+
  • abs( ) The absolute value +
  • cos( ) Note: cos( ) uses radian measure + +
  • sin( ) Note: sin( ) uses radian measure +
  • tan( ) Note: tan( ) uses radian measure +
  • sec( ) Note: sec( ) uses radian measure +
  • cot( ) Note: cot( ) uses radian measure +
  • csc( ) Note: csc( ) uses radian measure +
  • exp( ) The same function as e^x +
  • log( ) This is usually the natural log but your professor may have redined this as log to the base 10 +
  • ln( ) The natural log +
  • logten( ) The log to the base 10 + +
  • arcsin( ) +
  • asin( ) or sin^-1() Another name for arcsin +
  • arccos( ) +
  • acos( ) or cos^-1() Another name for arccos +
  • arctan( ) +
  • atan( ) or tan^-1() Another name for arctan +
  • arccot( ) +
  • acot( ) or cot^-1() Another name for arccot +
  • arcsec( ) + +
  • asec( ) or sec^-1() Another name for arcsec +
  • arccsc( ) +
  • acsc( ) or csc^-1() Another name for arccsc +
  • sinh( ) +
  • cosh( ) +
  • tanh( ) +
  • sech( ) +
  • csch( ) +
  • coth( ) + +
  • arcsinh( ) +
  • asinh( ) or sinh^-1() Another name for arcsinh +
  • arccosh( ) +
  • acosh( ) or cosh^-1()Another name for arccosh +
  • arctanh( ) +
  • atanh( ) or tanh^-1()Another name for arctanh +
  • arcsech( ) +
  • asech( ) or sech^-1()Another name for arcsech +
  • arccsch( ) + +
  • acsch( ) or csch^-1() Another name for arccsch +
  • arccoth( ) +
  • acoth( ) or coth^-1() Another name for arccoth +
  • sqrt( ) +
  • n! (n factorial -- defined for nÕ0  +
  • These functions may not always be available for every problem. +
    • sgn( ) The sign function, either -1, 0, or 1 + +
    • step( ) The step function (0 if x<0 , 1 if xÕ0 ) +
    • fact(n) The factorial function n! (defined only for nonnegative integers) +
    • P(n,k) = n*(n-1)*(n-2)...(n-k+1) the number of ordered sequences of k elements chosen from n elements +
    • C(n,k) = "n choose k" the number of unordered sequences of k elements chosen from n elements +
    +
+ +For more information: + +http://webwork.maa.org/wiki/Available_Functions + + diff --git a/htdocs/helpFiles/Entering-Units.html b/htdocs/helpFiles/Entering-Units.html new file mode 100644 index 0000000000..851655873c --- /dev/null +++ b/htdocs/helpFiles/Entering-Units.html @@ -0,0 +1,74 @@ + +

Units Available in WeBWorK

+

+Some WeBWorK problems ask for answers with units. Below is a list of basic units +and how they need to be abbreviated in WeBWorK answers. In some problems, you +may need to combine units (e.g, velocity might be in ft/s for feet per +second). +

+ +
Unit Abbreviation +
Time +
Seconds s + +
Minutes min +
Hours hr +
Days day +
Years yr +
Milliseconds ms + + +
Distance +
Feet ft +
Inches in +
Miles mi +
Meters m + +
Centimeters cm +
Millimeters mm +
Kilometers km +
Angstroms A +
Light years light-year + + +
Mass +
Grams g +
Kilograms kg +
Slugs slug + +
Volume +
Liters L + +
Cubic Centimeters cc +
Milliliters ml + +
Force +
Newtons N +
Dynes dyne + +
Pounds lb +
Tons ton + +
Work/Energy +
Joules J +
kilo Joule kJ + +
ergs erg +
foot pounds lbf +
calories cal +
kilo calories kcal +
electron volts eV + +
kilo Watt hours kWh + +
Misc +
Amperes amp +
Moles mol +
Degrees Centrigrade degC + +
Degrees Fahrenheit degF +
Degrees Kelvin degK +
Angle degrees deg +
Angle radians rad + +
diff --git a/htdocs/helpFiles/Entering-Vectors.html b/htdocs/helpFiles/Entering-Vectors.html new file mode 100644 index 0000000000..c546eff58d --- /dev/null +++ b/htdocs/helpFiles/Entering-Vectors.html @@ -0,0 +1,48 @@ +
+Entering Vectors +
+ +
    + +
  • Predefined vectors i, j, and k +
    +i   is the same as   <1,0,0>
    +j   is the same as   <0,1,0>
    +k   is the same as   <0,0,1>
    +
    +
  • + +
  • A vector may be entered using angle brackets and commas, or adding multiples of i, j, and k: +
    +<4.5, 3/7>   and   4.5i + 3/7j   are valid vectors in 2 dimensions
    +<pi,e,2>   and   pi i + e j + 2 k   are valid vectors in 3 dimensions +
    +
  • + +
  • If the answer is more than one vector: +
    +Enter your answer as a comma-separated list of vectors, for example:    +<4,3>, <5,10> +
    +
  • + +
  • If there are no solutions: +
    +Enter   NONE   or   DNE   (this may vary from problem to problem) +
    +
  • + +
  • Examples of constants used in vectors: +
    pi, e = e^1
    +
  • + +
  • Functions may be used in each coordinate of a vector, but may not be applied across the parentheses or commas: +
    +<sqrt(2),sqrt(5)>   is valid, but   <sqrt(2,5)>   is not +
    +
    +Link to a list of all available functions +
    +
  • + +
diff --git a/htdocs/helpFiles/IntervalNotation.html b/htdocs/helpFiles/IntervalNotation.html new file mode 100644 index 0000000000..4907b837bf --- /dev/null +++ b/htdocs/helpFiles/IntervalNotation.html @@ -0,0 +1,57 @@ + +

+Using Interval Notation +

+ +
    + +
  • If an endpoint is included, then use [ or ]. +If not, then use ( or ). For example, the interval +from -3 to 7 that includes 7 but not -3 is expressed (-3,7]. + +
    +
    + +
  • For infinite intervals, use Inf +for (infinity) and/or +-Inf for -∞ (-Infinity). For +example, the infinite interval containing all points greater than or +equal to 6 is expressed [6,Inf). + +
    +
    + +
  • If the set includes more than one interval, they are joined using the union +symbol U. For example, the set consisting of all points in (-3,7] together with all points in [-8,-5) is expressed [-8,-5)U(-3,7]. + +
    +
    + +
  • If the answer is the empty set, you can specify that by using + braces with nothing inside: { } + +
    +
    + +
  • You can use R as a shorthand for all real numbers. + So, it is equivalent to entering (-Inf, Inf). + +
    +
    + +
  • You can use set difference notation. So, for all real numbers + except 3, you can use R-{3} or + (-Inf, 3)U(3,Inf) (they are the same). Similarly, + [1,10)-{3,4} is the same as [1,3)U(3,4)U(4,10). + +
    +
    + + +
  • WeBWorK will not interpret [2,4]U[3,5] as equivalent + to [2,5], unless a problem tells you otherwise. +All sets should be expressed in their simplest interval notation form, with no +overlapping intervals. + +
+ diff --git a/htdocs/helpFiles/PDE-notation.html b/htdocs/helpFiles/PDE-notation.html new file mode 100644 index 0000000000..d11425d511 --- /dev/null +++ b/htdocs/helpFiles/PDE-notation.html @@ -0,0 +1,95 @@ + + +

Entering Partial Derivatives

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Partial DerivativeEnter into WeBWork
\(\frac{\partial u}{\partial x}\) ux
\(\frac{\partial u}{\partial t}\)ut
\(\frac{\partial u}{\partial y}\)uy
\(\frac{\partial^2 u}{\partial x^2}\)uxx
\(\frac{\partial^2 u}{\partial t^2}\)utt
\(\frac{\partial^2 u}{\partial y^2}\)uyy +
+
+To answer questions that require you to input a PDE you will also need to know the form +of the PDE WeBWorK is expecting. In particular you will need to use the same letters +for constants that WeBWorK is expecting. Below is a table of the PDE's +you may encounter. +
+

Models

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ModelPDE
heat equation\(k \frac{\partial^2 u}{\partial x^2} = \frac{\partial u}{\partial t}\)
heat equation with lateral heat transfer - \(u_m\) is the temperature of the surrounding medium and will be given, h is the constant from Newton's $ + \(k \frac{\partial^2 u}{\partial x^2}-h (u-um) = \frac{\partial u}{\partial t}\)
wave equation\(a^2 \frac{\partial^2 u}{\partial x^2} = \frac{\partial^2 u}{\partial t^2}\))
wave equation with damping\(a^2 \frac{\partial^2 u}{\partial x^2} = \frac{\partial^2 u}{\partial t^2}+c \frac{\partial u}{\partial t}\)
wave equation with an external force (f(x,t) will specified in the problem\(a^2 \frac{\partial^2 u}{\partial x^2} + f(x,t) = \frac{\partial^2 u}{\partial t^2}\)
Laplace's equation\(\frac{\partial^2 u}{\partial x^2}+\frac{\partial^2 u}{\partial y^2} = 0\)
+
+For problems with proportional quantities the constant of proportionality is c. +
+
+

Entering Boundary and Initial Conditions

+
+There are three types of boundary conditions we will consider: +
+1. Ends held at a constant temperature \(u_0\) (Dirichlet condition): \(u(L,t) = u_0\) +
+2. Ends insulated (Neumann condition): \(\frac{\partial u}{\partial x}\big\vert_{x=L} = 0\) +
+3. Heat transfer through the ends into a medium held at constant temperature \(u_m\) +
+
+ +Suppose that we want to enter the boundary condition \(\frac{\partial u}{\partial x}\big\vert_{x=0}=0\). +You will be given two answer blanks: the first is to input the partial derivative and the point, ux(0,t), +and the second will be for the right hand side. In WeBWorK notation the boundary condition would be given as ux(0,t) = 0. + diff --git a/htdocs/helpFiles/Syntax.html b/htdocs/helpFiles/Syntax.html new file mode 100644 index 0000000000..03f8d323e9 --- /dev/null +++ b/htdocs/helpFiles/Syntax.html @@ -0,0 +1,124 @@ +

Syntax for entering answers to WeBWorK

+ + +

Mathematical Symbols Available In WeBWorK

+
  • + Addition +
  • - Subtraction +
  • * Multiplication can also be indicated by a space or juxtaposition, e.g. 2x, 2 x or 2*x, also 2(3+4). +
  • / Division +
  • ^ or ** You can use either ^ or ** for exponentiation, e.g. 3^2 or 3**2 +
  • Parentheses: () - You can also use square brackets, [ ], and braces, { }, for grouping, e.g. [1+2]/[3(4+5)] +
+

Syntax for entering expressions

+
  • Be careful entering expressions just as you would be careful entering expressions in a calculator. +
  • Use the "Preview Button" to see exactly how your entry +looks. E.g. to tell the difference between 1+2/3*4 and [1+2]/[3*4] +click the "Preview Button". +
  • Sometimes using the * symbol to indicate mutiplication makes +things easier to read. For example (1+2)*(3+4) and (1+2)(3+4) are both +valid. So are 3*4 and 3 4 (3 space 4, not 34) but using a * makes +things clearer. +
  • Use ('s and )'s to make your meaning clear. You can also use ['s and ]'s and {'s and }'s. +
  • Don't enter 2/4+5 (which is 5.5) when you really want 2/(4+5) (which is 2/9). +
  • Don't enter 2/3*4 (which is 8/3) when you really want 2/(3*4) (which is 2/12). +
  • Entering big quotients with square brackets, e.g. [1+2+3+4]/[5+6+7+8], is a good practice. +
  • Be careful when entering functions. It's always good practice +to use parentheses when entering functions. Write sin(t) instead of +sint or sin t even though WeBWorK is smart enough to usually accept sin t or even sint. For example, sin 2t is interpreted as sin(2)t, i.e. (sin(2))*t so be careful. +
  • You can enter sin^2(t) as a short cut although mathematically +speaking sin^2(t) is shorthand for (sin(t))^2(the square of sin of t). +(You can enter it as sin(t)^2 or even sint^2, but don't try such things +unless you really understand the precedence of operations. The +"sin" operation has highest precedence, so it is performed first, using +the next token (i.e. t) as an argument. Then the result is squared.) +You can always use the Preview button to see a typeset version of what +you entered and check whether what you wrote was what you +meant. :-) +
  • For example 2+3sin^2(4x) will work and is equivalent to +2+3(sin(4x))^2 or 2+3sin(4x)^2. Why does the last expression work? +Because things in parentheses are always done first [ i.e. (4x)], next +all functions, such as sin, are evaluated [giving sin(4x)], next all +exponents are taken [giving sin(4x)^2], next all multiplications and +divisions are performed in order from left to right [giving +3sin(4x)^2], and finally all additions and subtractions are performed +[giving 2+3sin(4x)^2]. +
  • Is -5^2 positive or negative? It's negative. This is because +the square operation is done before the negative sign is applied. Use +(-5)^2 if you want to square negative 5. +
  • When in doubt use parentheses!!! :-) +
  • The complete rules for the precedence of operations, in addition to the above, are +
    • Multiplications and divisions are performed left to right: 2/3*4 = (2/3)*4 = 8/3. +
    • Additions and subtractions are performed left to right: 1-2+3 = (1-2)+3 = 2. +
    • Exponents are taken right to left: 2^3^4 = 2^(3^4) = 2^81 = a big number. +
    +
  • Use the "Preview Button" to see exactly how your entry +looks. E.g. to tell the difference between 1+2/3*4 and [1+2]/[3*4] +click the "Preview Button". +
+

Mathematical Constants Available In WeBWorK

+
  • pi This gives 3.14159265358979, e.g. cos(pi) is -1 +
  • e This gives 2.71828182845905, e.g. ln(e*2) is 1 + ln(2) +
+

Scientific Notation Available In WeBWorK

+
  • 2.1E2 is the same as 210 +
  • 2.1E-2 is the same as .021 +
+

Mathematical Functions Available In WeBWorK

+

Note that sometimes one or more of these functions is disabled for a WeBWorK problem because the +instructor wants you to calculate the answer by some means other than just using the function. +

+
  • abs( ) The absolute value +
  • cos( ) Note: cos( ) uses radian measure +
  • sin( ) Note: sin( ) uses radian measure +
  • tan( ) Note: tan( ) uses radian measure +
  • sec( ) Note: sec( ) uses radian measure +
  • cot( ) Note: cot( ) uses radian measure +
  • csc( ) Note: csc( ) uses radian measure +
  • exp( ) The same function as e^x +
  • log( ) This is usually the natural log but your professor may have redined this as log to the base 10 +
  • ln( ) The natural log +
  • logten( ) The log to the base 10 +
  • arcsin( ) +
  • asin( ) or sin^-1() Another name for arcsin +
  • arccos( ) +
  • acos( ) or cos^-1() Another name for arccos +
  • arctan( ) +
  • atan( ) or tan^-1() Another name for arctan +
  • arccot( ) +
  • acot( ) or cot^-1() Another name for arccot +
  • arcsec( ) +
  • asec( ) or sec^-1() Another name for arcsec +
  • arccsc( ) +
  • acsc( ) or csc^-1() Another name for arccsc +
  • sinh( ) +
  • cosh( ) +
  • tanh( ) +
  • sech( ) +
  • csch( ) +
  • coth( ) +
  • arcsinh( ) +
  • asinh( ) or sinh^-1() Another name for arcsinh +
  • arccosh( ) +
  • acosh( ) or cosh^-1()Another name for arccosh +
  • arctanh( ) +
  • atanh( ) or tanh^-1()Another name for arctanh +
  • arcsech( ) +
  • asech( ) or sech^-1()Another name for arcsech +
  • arccsch( ) +
  • acsch( ) or csch^-1() Another name for arccsch +
  • arccoth( ) +
  • acoth( ) or coth^-1() Another name for arccoth +
  • sqrt( ) +
  • n! (n factorial -- defined for nÕ0  +
  • These functions may not always be available for every problem. +
    • sgn( ) The sign function, either -1, 0, or 1 +
    • step( ) The step function (0 if x<0 , 1 if xÕ0 ) +
    • fact(n) The factorial function n! (defined only for nonnegative integers) +
    • P(n,k) = n*(n-1)*(n-2)...(n-k+1) the number of ordered sequences of k elements chosen from n elements +
    • C(n,k) = "n choose k" the number of unordered sequences of k elements chosen from n elements +
    +
+ +For more information: + +http://webwork.maa.org/wiki/Available_Functions diff --git a/htdocs/helpFiles/Units.html b/htdocs/helpFiles/Units.html new file mode 100644 index 0000000000..eb2c8b7aa2 --- /dev/null +++ b/htdocs/helpFiles/Units.html @@ -0,0 +1,69 @@ + +

Units Available in WeBWorK

+

+Some WeBWorK problems ask for answers with units. Below is a list of basic units +and how they need to be abbreviated in WeBWorK answers. In some problems, you +may need to combine units (e.g, velocity might be in ft/s for feet per +second). +

+ +
Unit Abbreviation +
Time +
Seconds s +
Minutes min +
Hours hr +
Days day +
Years yr +
Milliseconds ms + +
Distance +
Feet ft +
Inches in +
Miles mi +
Meters m +
Centimeters cm +
Millimeters mm +
Kilometers km +
Angstroms A +
Light years light-year + +
Mass +
Grams g +
Kilograms kg +
Slugs slug + +
Volume +
Liters L +
Cubic Centimeters cc +
Milliliters ml + +
Force +
Newtons N +
Dynes dyne +
Pounds lb +
Tons ton + +
Work/Energy +
Joules J +
kilo Joule kJ +
ergs erg +
foot pounds lbf +
calories cal +
kilo calories kcal +
electron volts eV +
kilo Watt hours kWh + +
Misc +
Amperes amp +
Moles mol +
Degrees Centrigrade degC +
Degrees Fahrenheit degF +
Degrees Kelvin degK +
Angle degrees deg +
Angle radians rad + +
+ +More Units at http://webwork.maa.org/wiki/Units + +
diff --git a/htdocs/js/apps/AppletSupport/AC_RunActiveContent.js b/htdocs/js/apps/AppletSupport/AC_RunActiveContent.js new file mode 100755 index 0000000000..30cddb9dbb --- /dev/null +++ b/htdocs/js/apps/AppletSupport/AC_RunActiveContent.js @@ -0,0 +1,292 @@ +//v1.7 +// Flash Player Version Detection +// Detect Client Browser type +// Copyright 2005-2007 Adobe Systems Incorporated. All rights reserved. +var isIE = (navigator.appVersion.indexOf("MSIE") != -1) ? true : false; +var isWin = (navigator.appVersion.toLowerCase().indexOf("win") != -1) ? true : false; +var isOpera = (navigator.userAgent.indexOf("Opera") != -1) ? true : false; + +function ControlVersion() +{ + var version; + var axo; + var e; + + // NOTE : new ActiveXObject(strFoo) throws an exception if strFoo isn't in the registry + + try { + // version will be set for 7.X or greater players + axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7"); + version = axo.GetVariable("$version"); + } catch (e) { + } + + if (!version) + { + try { + // version will be set for 6.X players only + axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6"); + + // installed player is some revision of 6.0 + // GetVariable("$version") crashes for versions 6.0.22 through 6.0.29, + // so we have to be careful. + + // default to the first public version + version = "WIN 6,0,21,0"; + + // throws if AllowScripAccess does not exist (introduced in 6.0r47) + axo.AllowScriptAccess = "always"; + + // safe to call for 6.0r47 or greater + version = axo.GetVariable("$version"); + + } catch (e) { + } + } + + if (!version) + { + try { + // version will be set for 4.X or 5.X player + axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.3"); + version = axo.GetVariable("$version"); + } catch (e) { + } + } + + if (!version) + { + try { + // version will be set for 3.X player + axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.3"); + version = "WIN 3,0,18,0"; + } catch (e) { + } + } + + if (!version) + { + try { + // version will be set for 2.X player + axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"); + version = "WIN 2,0,0,11"; + } catch (e) { + version = -1; + } + } + + return version; +} + +// JavaScript helper required to detect Flash Player PlugIn version information +function GetSwfVer(){ + // NS/Opera version >= 3 check for Flash plugin in plugin array + var flashVer = -1; + + if (navigator.plugins != null && navigator.plugins.length > 0) { + if (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]) { + var swVer2 = navigator.plugins["Shockwave Flash 2.0"] ? " 2.0" : ""; + var flashDescription = navigator.plugins["Shockwave Flash" + swVer2].description; + var descArray = flashDescription.split(" "); + var tempArrayMajor = descArray[2].split("."); + var versionMajor = tempArrayMajor[0]; + var versionMinor = tempArrayMajor[1]; + var versionRevision = descArray[3]; + if (versionRevision == "") { + versionRevision = descArray[4]; + } + if (versionRevision[0] == "d") { + versionRevision = versionRevision.substring(1); + } else if (versionRevision[0] == "r") { + versionRevision = versionRevision.substring(1); + if (versionRevision.indexOf("d") > 0) { + versionRevision = versionRevision.substring(0, versionRevision.indexOf("d")); + } + } + var flashVer = versionMajor + "." + versionMinor + "." + versionRevision; + } + } + // MSN/WebTV 2.6 supports Flash 4 + else if (navigator.userAgent.toLowerCase().indexOf("webtv/2.6") != -1) flashVer = 4; + // WebTV 2.5 supports Flash 3 + else if (navigator.userAgent.toLowerCase().indexOf("webtv/2.5") != -1) flashVer = 3; + // older WebTV supports Flash 2 + else if (navigator.userAgent.toLowerCase().indexOf("webtv") != -1) flashVer = 2; + else if ( isIE && isWin && !isOpera ) { + flashVer = ControlVersion(); + } + return flashVer; +} + +// When called with reqMajorVer, reqMinorVer, reqRevision returns true if that version or greater is available +function DetectFlashVer(reqMajorVer, reqMinorVer, reqRevision) +{ + versionStr = GetSwfVer(); + if (versionStr == -1 ) { + return false; + } else if (versionStr != 0) { + if(isIE && isWin && !isOpera) { + // Given "WIN 2,0,0,11" + tempArray = versionStr.split(" "); // ["WIN", "2,0,0,11"] + tempString = tempArray[1]; // "2,0,0,11" + versionArray = tempString.split(","); // ['2', '0', '0', '11'] + } else { + versionArray = versionStr.split("."); + } + var versionMajor = versionArray[0]; + var versionMinor = versionArray[1]; + var versionRevision = versionArray[2]; + + // is the major.revision >= requested major.revision AND the minor version >= requested minor + if (versionMajor > parseFloat(reqMajorVer)) { + return true; + } else if (versionMajor == parseFloat(reqMajorVer)) { + if (versionMinor > parseFloat(reqMinorVer)) + return true; + else if (versionMinor == parseFloat(reqMinorVer)) { + if (versionRevision >= parseFloat(reqRevision)) + return true; + } + } + return false; + } +} + +function AC_AddExtension(src, ext) +{ + if (src.indexOf('?') != -1) + return src.replace(/\?/, ext+'?'); + else + return src + ext; +} + +function AC_Generateobj(objAttrs, params, embedAttrs) +{ + var str = ''; + if (isIE && isWin && !isOpera) + { + str += ' '; + } + str += ''; + } + else + { + str += '\n"; + for(var i = 0; i < elementList.length; ++i) { + str += " " + i + " " + elementList[i].type + + " | " + elementList[i].name + + " = " + elementList[i].value + + " <" + elementList[i].id + ">\n"; + } + elementList = document.problemMainForm.getElementsByTagName("textarea"); + for (var i = 0; i < elementList.length; ++i) { + str = str + " " + i + " " + elementList[i].type + + " | " + elementList[i].name + + " = " + elementList[i].value + + " <" + elementList[i].id + ">\n"; + } + alert(str); +} + +// Determine whether an XML string has been base64 encoded. +function base64Q(str) { + if (!str) { + // The empty string is not a base64 string. + return 0; + } else if (str.match(/[<>]+/)) { + // base64 can't contain < or > and xml strings contain lots of them + return 0; + } else { + // Its probably a non-empty base64 string. + return 1; + } +} + +// Set the state stored on the HTML page +function setHTMLAppletState(appletName, newState) { + if (typeof(newState) === 'undefined') newState = "restart_applet"; + var stateInput = ww_applet_list[appletName].stateInput; + getQE(stateInput).value = newState; + getQE("previous_" + stateInput).value = newState; +} + +// Get Question Element in problemMainForm by name +function getQE(name1) { + var obj = document.getElementById(name1); + if (!obj) {obj = document.problemMainForm[name1]} + + if (!obj || obj.name != name1) { + var msg = "Can't find element " + name1; + if (jsDebugMode == 1) { + debug_add(msg + "\n ( Place listQuestionElements() at end of document in order to get all form elements! )\n" ); + } else { + alert(msg); listQuestionElements(); + }; + } else { + return obj; + } +} + +function getQuestionElement(name1) { + return getQE(name1); +} + +// WW_Applet class definition + +function ww_applet(appletName) { + this.appletName = appletName; + this.type = ''; + this.code = ''; + this.codebase = ''; + this.base64State = ''; + this.initialState = ''; + this.configuration = ''; + this.getStateAlias = ''; + this.setStateAlias = ''; + this.setConfigAlias = ''; + this.getConfigAlias = ''; + this.initializeActionAlias = ''; + this.submitActionAlias = ''; + this.submitActionScript = ''; + this.answerBoxAlias = ''; + this.maxInitializationAttempts = 5; + this.debugMode = 0; + this.isReady = 0; + this.reportsLoaded = 0; + this.onInit = 0; +}; + +// Make sure that the applet has this function available +ww_applet.prototype.methodDefined = function(methodName) { + var appletName = this.appletName; + var applet = getApplet(appletName); + if (!methodName) { + // methodName is not defined + return false; + } + try { + if (typeof(applet[methodName]) == "function") { + this.debug_add("Method " + methodName + " is defined in " + appletName ); + return true; + } else { + this.debug_add("Method " + methodName + " is not defined in " + appletName); + throw("undefined applet method"); + } + } catch(e) { + this.debug_add("Error in accessing " + methodName + " in applet " + appletName + "\n *Error: " + e); + } + return false; +}; + +// CONFIGURATIONS +// Configurations are "permanent" +ww_applet.prototype.setConfig = function () { + var appletName = this.appletName; + var applet = getApplet(appletName); + var setConfigAlias = this.setConfigAlias; + + try { + if (this.methodDefined(this.setConfigAlias)) { + applet[setConfigAlias](this.configuration); + this.debug_add(" Configuring applet: Calling " + appletName + "." + + setConfigAlias + "( " + this.configuration + " )"); + } else { + this.debug_add(" Configuring applet: Unable to execute command |" + + setConfigAlias + "| in the applet " + appletName + " with data ( \"" + this.configuration + "\" ) " ); + } + } catch(e) { + alert("Error in configuring applet " + appletName + " using command " + setConfigAlias + " : " + e); + } +}; + +// Gets the configuration from the applet. Used for debugging purposes. +ww_applet.prototype.getConfig = function() { + var appletName = this.appletName; + var applet = getApplet(appletName); + var getConfigAlias = this.getConfigAlias; + + try { + if (this.methodDefined(getConfigAlias)) { + alert(applet[getConfigAlias]()); + } else { + this.debug_add(" Unable to execute " + appletName + "." + getConfigAlias + "( " + this.configuration + " )"); + } + } catch(e) { + alert(" Error in getting configuration from applet " + appletName + " " + e); + } +}; + +// STATE: +// State can vary as the applet is manipulated. It is reset from the questions _state values. +ww_applet.prototype.setState = function(state) { + var appletName = this.appletName; + var applet = getApplet(appletName); + var setStateAlias = this.setStateAlias; + console.log("Into setState for applet " + appletName); + this.debug_add("\n++++++++++++++++++++++++++++++++++++++++\nBegin process of setting state for applet " + appletName); + + + // Obtain the state which will be sent to the applet and if it is encoded place it in plain xml text. + // Communication with the applet is in plain text, not in base64 code. + + if (state) { + this.debug_add("Obtain state from calling parameter:\n " + state.substring(0, 200) + "\n"); + } else { + this.debug_add("Obtain state from " + this.stateInput); + + // Hidden answer box preserving applet state + var ww_preserve_applet_state = getQE(this.stateInput); + state = ww_preserve_applet_state.value; + this.debug_add("Immediately on grabbing state from HTML cache state is " + (state.substring(0, 200) ) + "..."); + } + + if (base64Q(state)) { + state = Base64.decode(state); + this.debug_add("Decodes to: " + state.substring(0, 200)); + if (this.debugMode >= 1) { + // Decode text for the text area box + ww_preserve_applet_state.value = state; + + } + this.debug_add("Decoded to " + ww_preserve_applet_state.value); + } + + // Handle the exceptional cases: + // If the state is blank, undefined, or explicitly defined as restart_applet, + // then we will not simply be restoring the state of the applet from HTML "memory". + // + // 1. For a restart we wipe the HTML state cache so that we won't restart again. + // 2. In the other "empty" cases we attempt to replace the state with the contents of the + // initialState variable. + + // Exceptional cases + if (state.match(/^restart_applet<\/xml>/) || + state.match(/^\s*$/) || + state.match(/^\s*<\/xml>/)) { + this.debug_add("Beginning handling exceptional cases when the state is not simply restored " + + "from the HTML cache. State is: " + state.substring(0, 100)); + + if (typeof(this.initialState) == "undefined") { this.initialState = ""; } + debug_add("Restart_applet has been called. the value of the initialState is " + this.initialState); + if (this.initialState.match(/^\s*<\/xml>/) || this.initialState.match(/^\s*$/)) { + // If the initial state is empty + debug_add("The applet " + appletName + + " has been restarted. There was no non-empty initialState value. \n" + + "Nothing is sent to the applet.\n Done setting state"); + if (state.match(/^restart_applet<\/xml>/)) { + alert("The applet is being restarted with empty initialState"); + } + // So that the submit action will not be overridden by restart_applet. + setHTMLAppletState(appletName, ""); + + // Don't call the setStateAlias function. + // Quit because we know we will not transmitting any starting data to the applet + console.log("Out of setState for applet " + appletName); + return; + } else { + state = this.initialState; + if (base64Q(state)) state = Base64.decode(state); + + debug_add("The applet " + appletName + "has been set to its virgin state value." + state.substring(0, 200)); + if (state.match(/^restart_applet<\/xml>/)) { + alert("The applet is being reset to its initialState."); + } + // Store the state in the HTML variables just for safetey + setHTMLAppletState(appletName, this.initialState); + + // If there was a viable state in the initialState variable we can + // now continue as if we had found a valid state in the HTML cache. + } + this.debug_add("Completed handling the exceptional cases."); + } + + if (state.match(/\ -- Applet state was not reset"); + } + + this.debug_add("Done setting state"); + if (this.debugMode >= 2) { console.log("DebugText:\n" + debugText); debugText = ""; } + console.log("Out of setState for applet " + appletName); +}; + +ww_applet.prototype.getState = function () { + var state = "foobar"; + var appletName = this.appletName; + var applet = getApplet(appletName); + var getStateAlias = this.getStateAlias; + console.log("Into getState for applet " + appletName); + this.debug_add(" Begin getState from applet " + appletName ); + + try { + if (this.methodDefined(getStateAlias)) { + // There may be no state function + state = applet[getStateAlias](); // Get state in xml format + this.debug_add(" state has type " + typeof(state)); + // Geogebra returns an object type instead of a string type + state = String(state); + // This insures that we can view the state as a string + this.debug_add(" state converted to type " + typeof(state)); + } else { + this.debug_add(" Applet does not have a getState method named: " + getStateAlias + "."); + state ="undefined_state"; + } + + } catch (e) { + alert("Error in getting state from applet " + appletName + " " + e); + } + + // Replace state by encoded version unless in debug mode + if (this.debugMode == 0) { + if (!base64Q(state)) state = Base64.encode(state); + }; + + this.debug_add(" state is\n " + state.substring(0, 20) + "\n"); // state should still be in plain text + + // Answer box preserving applet state (jsDebugMode: textarea, otherwise: hidden) + var ww_preserve_applet_state = getQE(this.stateInput); + // Place state in input item (jsDebugMode: textarea, otherwise: hidden) + ww_preserve_applet_state.value = state; + this.debug_add("State stored in answer box " + this.stateInput + " and getState is finished."); + console.log("Out of getState for applet " + appletName); +}; + +// Sets debug mode in the applet +// Applet's method must be called debug +ww_applet.prototype.setDebug = function(debugMode) { + var appletName = this.appletName; + var applet = getApplet(appletName); + debugMode = jsDebugMode || debugMode ; + + try{ + if (this.methodDefined("debug")) { + // Set the applet's debug functions on. + applet.debug(debugMode); + } else { + this.debug_add(" Unable to set debug state in applet " + appletName + "."); + } + } catch(e) { + alert("Unable to set debug mode for applet " + appletName); + } +}; + +// INITIALIZE +ww_applet.prototype.initializeAction = function () { + this.setState(); +}; + +ww_applet.prototype.submitAction = function () { + var appletName = this.appletName; + console.log("Into submitAction for " + appletName); + // Don't do anything if the applet is hidden. + if (!ww_applet_list[appletName].visible) return; + this.debug_add("submitAction"); + + // Hidden HTML input element preserving applet state + var ww_preserve_applet_state = getQE(this.stateInput); + var saved_state = ww_preserve_applet_state.value; + + // Check to see if we want to restart the applet + if (saved_state.match(/^restart_applet<\/xml>/)) { + this.debug_add("Restarting the applet " + appletName); + // Replace the saved state with restart_applet + setHTMLAppletState(appletName); + if (this.debugMode >= 2) { console.log("DebugText:\n" + debugText); debugText = ""; } + return; + } + // If we are not restarting the applet save the state and submit + this.debug_add("Not restarting."); + this.debug_add("Begin submit action for applet " + appletName); + var applet = getApplet(appletName); + if (!this.isReady) { + alert(appletName + " is not ready. " + + "The isReady flag is false which is strange since we are resubmitting this page. " + + "There should have been plenty of time for the applet to load."); + this.initializeAction(); + } + + this.debug_add("About to get state"); + + // Have ww_applet retrieve state from applet and store in HTML cache + this.getState(); + + this.debug_add("Submit Action Script " + this.submitActionScript + "\n"); + eval(this.submitActionScript); + + this.debug_add("Completed submitAction(" + this.submitActionScript + ") \nfor applet " + appletName + "\n"); + + // Because the state has not always been perfectly preserved when storing the state in text + // area boxes we take a "belt && suspenders" approach by converting the value of the text + // area state cache to base64 form. + + saved_state = ww_preserve_applet_state.value; + this.debug_add("Saved state looks like before encoding " + saved_state.substring(0, 200)); + if (!base64Q(saved_state)) { + // Preserve html entities untranslated! Yeah!!!!!!! + // FIXME -- this is not a perfect fix -- things are confused for a while when + // you switch from debug to non debug modes + saved_state = Base64.encode(saved_state); + } + + // On submit the value of ww_preserve_applet_state.value is always in Base64. + ww_preserve_applet_state.value = saved_state; + this.debug_add("just before submitting saved state looks like " + ww_preserve_applet_state.value.substring(0, 200)); + + if (this.debugMode >= 2) { console.log("DebugText:\n" + debugText); debugText = ""; } +}; + +// This function returns 0 unless: +// applet has already been flagged as ready +// applet.config is defined (or alias for .config) +// applet.setState is defined +// applet.isActive is defined and returns 1; +// applet reported that it is loaded by calling applet_loaded() +ww_applet.prototype.checkLoaded = function() { + var ready = 0; + var appletName = this.appletName; + var applet = getApplet(appletName); + + // Memorize readiness in non-debug mode + if (this.debugMode == 0 && this.isReady) return 1; + + this.debug_add("*Test 4 methods to see if the applet " + appletName + " has been loaded:\n"); + + try { + if (this.methodDefined(this.setConfigAlias)) ready = 1; + } catch(e) { + this.debug_add("*Unable to find setConfig command in applet " + appletName + "\n" + e); + } + + try { + if (this.methodDefined(this.setStateAlias)) ready = 1; + } catch(e) { + this.debug_add("*Unable to setState command in applet " + appletName + "\n" + e); + } + + if (typeof(this.reportsLoaded) != "undefined" && this.reportsLoaded != 0) { + this.debug_add(" *" + appletName + " applet self reports that it has completed loading. "); + ready = 1; + } + + // The return value of the isActive() method, when defined, overrides the other indications + // that the applet is ready. + if (this.methodDefined("isActive")) { + if (applet.isActive()) { + // This could be zero if applet is loaded, but it is loading auxiliary data. + this.debug_add("*Applet " + appletName + " signals it is active.\n"); + ready = 1; + } else { + this.debug_add("*Applet " + appletName + " signals it is not active. -- \n it may still be loading data.\n"); + ready = 0; + } + } + this.isReady = ready; + return(ready); +}; + +ww_applet.prototype.debug_add = function(str) { + if (this.debugMode >= 2) { + debugText += "\n" +str; + } +}; + +ww_applet.prototype.safe_applet_initialize = function(i) { + var appletName = this.appletName; + console.log("Into safe_applet_initialize for applet " + appletName + " i = " + i); + var failed_attempts_allowed = 3; + + --i; + + // Check whether the applet is has already loaded + this.debug_add("* Try to initialize applet " + appletName + ". Count down: " + i + ".\n" ); + this.debug_add("Entering checkLoaded subroutine"); + var applet_loaded = this.checkLoaded(); + if ((applet_loaded != 0) && (applet_loaded != 1)) { + alert("Error: The applet_loaded variable has not been defined. " + applet_loaded); + } + this.debug_add("Returning from checkLoaded subroutine with result " + applet_loaded); + + // If applet has not loaded try again, or announce that the applet can't be loaded + + if (applet_loaded == 0 && i > 0) { + // Wait until applet is loaded + this.debug_add("*Applet " + appletName + " is not yet ready try again\n"); + if (this.debugMode >= 2) { console.log("DebugText:\n" + debugText ); debugText = ""; } + setTimeout(function() { ww_applet_list[appletName].safe_applet_initialize(i); }, TIMEOUT); + // Warn about loading after failed_attempts_allowed failed attempts or if there is only one attempt left. + if (i <= 1 || i < ww_applet_list[appletName].maxInitializationAttempts-failed_attempts_allowed) { + console.log("Oops, applet is not ready. " + (i-1) + " tries left") + }; + console.log("Out of safe_applet_initialize for applet " + appletName); + return; + } else if (applet_loaded == 0 && i <= 0) { + // Its possible that the isActive() response of the applet is not working properly. + console.log("*We haven't been able to verify that the applet " + appletName + + " is loaded. We'll try to use it anyway but it might not work.\n"); + i = 1; + applet_loaded = 1; // FIXME -- give a choice as to whether to continue or not + this.isReady = 1; + console.log("Out of safe_applet_initialize for applet " + appletName); + return; + } + + // If the applet is loaded try to configure it. + if (applet_loaded) { + this.debug_add(" applet is ready = " + applet_loaded); + + this.debug_add("*Applet " + appletName + " initialization completed\n with " + i + + " possible attempts remaining. \n" + + "------------------------------\n"); + if (this.debugMode >= 2) { console.log("DebugText:\n" + debugText ); debugText = ""; } + // In-line handler -- configure and initialize + try { + this.setDebug(this.debugMode ? 1 : 0); + } catch(e2) { + var msg = "*Unable set debug in " + appletName + " \n " + e2; + if (this.debugMode >= 2) { this.debug_add(msg); } else { alert(msg) }; + } + try{ + // For applets that define their own configuration + this.setConfig(); + } catch(e4) { + var msg = "*Unable to configure " + appletName + " \n " +e4; + if (this.debugMode >= 2) { this.debug_add(msg); } else { alert(msg) }; + } + + try{ + // This is often the setState action. + this.initializeAction(); + } catch(e) { + var msg = "*Unable to perform an explicit initialization action (e.g. setState) on applet " + + appletName + " because \n " + e; + if (this.debugMode >= 2) { this.debug_add(msg); } else { alert(msg); } + } + } else { + alert("Error: applet " + appletName + " has not been loaded"); + this.debug_add("*Error: timed out waiting for applet " + appletName + " to load"); + if (this.debugMode >= 2) { console.log(" in safe applet initialize: " + debugText ); debugText = ""; + } + } + console.log("Out of safe_applet_initialize for applet " + appletName); + return; +}; + +// Initialize applet support and the applets. +function initializeAppletSupport() { + // Be careful that this function is only executed once. + if (typeof initializeAppletSupport.hasRun == 'undefined') initializeAppletSupport.hasRun = true; + else return; + + console.log("Into initializeAppletSupport"); + + // This should be the only ggbOnInit method defined. Unfortunately some older problems define a + // ggbOnInit so we check for that here. Those problems should be updated, and newly written + // problems should not define a javascript function by that name. + // This caches the ggbOnInit from the problem, and calls it in the ggbOnInit function defined + // here. This will only work if there is only one of these old problems on the page. + var ggbOnInitFromProblem = window.ggbOnInit ? window.ggbOnInit : null; + + window.ggbOnInit = function(appletName) { + if (ggbOnInitFromProblem) { + console.log("Calling cached ggbOnInit from problem."); + ggbOnInitFromProblem(appletName); + } + if (appletName in ww_applet_list && ww_applet_list[appletName].onInit && + ww_applet_list[appletName].onInit != 'ggbOnInit') { + if (window[ww_applet_list[appletName].onInit] && + typeof(window[ww_applet_list[appletName].onInit]) == 'function') { + console.log("Calling onInit function for " + appletName + " in ggbOnInit."); + window[ww_applet_list[appletName].onInit](appletName); + } else { + console.log("Calling onInit code for " + appletName + " in ggbOnInit."); + eval(ww_applet_list[appletName].onInit); + } + } + }; + + // Called from the submit event listener defined below. + function submitAction() { + console.log("Submit button pushed. Calling submit action routines."); + if (jsDebugMode == 0) { + debugText = "Call submitAction() function on each applet.\n"; + } + + for (var appletName in ww_applet_list) { + ww_applet_list[appletName].submitAction(); + } + if (jsDebugMode == 1) { debug_add("\nDone calling submitAction() on each applet.\n"); } + if (jsDebugMode == 1) { console.log("DebugText:\n" + debugText); debugText = ""; }; + console.log("Done calling submit action routines"); + } + + // Connect the form submitAction handler. + if (document.problemMainForm) document.problemMainForm.addEventListener('submit', submitAction); + if (document.gwquiz) document.gwquiz.addEventListener('submit', submitAction); + + for (var appletName in ww_applet_list) { + if (!ww_applet_list[appletName].onInit) { + console.log("Applet " + appletName + " has no onInit function. Initializing with safe_applet_initialize."); + var maxInitializationAttempts = ww_applet_list[appletName].maxInitializationAttempts; + this.debug_add("Initializing " + appletName); + ww_applet_list[appletName].safe_applet_initialize(maxInitializationAttempts); + } else { + // If onInit is defined, then the onInit function will handle the initialization. + console.log("Applet " + appletName + " has onInit function. No further initialization required."); + } + } + this.debug_add("End of applet initialization"); + console.log("Out of initializeAppletSupport"); +} + +window.addEventListener('load', initializeAppletSupport); diff --git a/htdocs/js/apps/GraphTool/graphtool.css b/htdocs/js/apps/GraphTool/graphtool.css new file mode 100644 index 0000000000..4b3578ae73 --- /dev/null +++ b/htdocs/js/apps/GraphTool/graphtool.css @@ -0,0 +1,109 @@ +.graphtool-container { + width: 400px; +} + +.graphtool-container .graphtool-graph { + width: 400px; + height: 400px; +} + +.graphtool-answer-container { + width: 100%; +} + +.graphtool-answer-container .graphtool-graph { + margin: auto; + width: 200px; + height: 200px; +} + +@media only screen and (max-width: 600px) { + .graphtool-container { + width: 300px; + } + .graphtool-container .graphtool-graph { + width: 300px; + height: 300px; + } + .graphtool-answer-container .graphtool-graph { + width: 150px; + height: 150px; + } +} + +.gt-toolbar-container { + margin: 8px 0 8px 0; + text-align: right; + width: 100%; + padding: 0; +} + +.gt-toolbar-container .gt-button { + box-sizing: border-box; + height: 34px; + text-align: center; + padding: 1px; + margin: 2px; + border-radius: 4px; +} + +.gt-toolbar-container .gt-tool-button { + width: 34px; + background-position: 0; +} + +.gt-toolbar-container .gt-tool-button:disabled{ + box-shadow: inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05); + outline: 0; + opacity: unset; +} + +.gt-toolbar-container .gt-solid-dash-box { + display: inline-block; + box-sizing: border-box; + height: 34px; + width: 34px; + padding: 0; + margin: 2px; + vertical-align: middle; +} + +.gt-toolbar-container .gt-solid-tool { + display: block; + height: 16px; + margin: 0 0 2px 0; + border-radius: 4px 4px 0 0; + background-image: url("images/SolidTool.svg"); +} + +.gt-toolbar-container .gt-dashed-tool { + display: block; + height: 16px; + margin: 2px 0 0 0; + border-radius: 0 0 4px 4px; + background-image: url("images/DashTool.svg"); +} + +.gt-tool-button.gt-select-tool { + background-image: url("images/SelectTool.svg"); +} + +.gt-tool-button.gt-line-tool { + background-image: url("images/LineTool.svg"); +} + +.gt-tool-button.gt-circle-tool { + background-image: url("images/CircleTool.svg"); +} + +.gt-tool-button.gt-vertical-parabola-tool { + background-image: url("images/VerticalParabolaTool.svg"); +} + +.gt-tool-button.gt-horizontal-parabola-tool { + background-image: url("images/HorizontalParabolaTool.svg"); +} + +.gt-tool-button.gt-fill-tool { + background-image: url("images/FillTool.svg"); +} diff --git a/htdocs/js/apps/GraphTool/graphtool.js b/htdocs/js/apps/GraphTool/graphtool.js new file mode 100644 index 0000000000..75bdab74a9 --- /dev/null +++ b/htdocs/js/apps/GraphTool/graphtool.js @@ -0,0 +1,1349 @@ +"use strict" + +// Polyfill for IE11. +if (!Object.values) Object.values = function(o) { + return Object.keys(o).map(function(i) { return o[i]; }); +}; + +function graphTool(containerId, options) { + // Do nothing if the graph has already been created. + if (document.getElementById(containerId + "_graph")) return; + + var graphContainer = $('#' + containerId); + if (graphContainer.css('width') == '0px') { + setTimeout(function() { graphTool(containerId, options); }, 100); + return; + } + + var gt = {}; + + // Semantic color control + + // dark blue + // > 13:1 with white + gt.curveColor = '#0000a6' + + // blue + // > 9:1 with white + gt.focusCurveColor = '#0000f5' + + // fillColor must use 6-digit hex + // medium purple + // 3:1 with white + // 4.5:1 with #0000a6 + // > 3:1 with #0000f5 + gt.fillColor = '#a384e5' + + // strict contrast ratios are less important for these colors + gt.pointColor = 'orange' + gt.pointHighlightColor = 'yellow' + gt.underConstructionColor = 'orange' + + gt.snapSizeX = options.snapSizeX ? options.snapSizeX : 1; + gt.snapSizeY = options.snapSizeY ? options.snapSizeY : 1; + gt.isStatic = 'isStatic' in options ? options.isStatic : false; + var availableTools = options.availableTools ? options.availableTools : [ + "LineTool", + "CircleTool", + "VerticalParabolaTool", + "HorizontalParabolaTool", + "FillTool", + "SolidDashTool" + ]; + + // These are the icons used for the fill tool and fill graph object. + gt.fillIcon = "data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' id='SVGRoot' version='1.1' viewBox='0 0 32 32' height='32px' width='32px'%3E%3Cdefs id='defs815' /%3E%3Cmetadata id='metadata818'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3Cdc:title%3E%3C/dc:title%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cg id='layer1'%3E%3Cpath id='path1382' d='m 13.466084,10.267728 -4.9000003,8.4 4.9000003,4.9 8.4,-4.9 z' style='opacity:1;fill:" + gt.fillColor.replace(/#/, '%23') + ";fill-opacity:1;stroke:%23000000;stroke-width:1.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none' /%3E%3Cpath id='path1384' d='M 16.266084,15.780798 V 6.273173' style='fill:none;stroke:%23000000;stroke-width:1.38;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Cpath id='path1405' d='m 20,16 c 0,0 2,-1 3,0 1,0 1,1 2,2 0,1 0,2 0,3 0,1 0,2 0,2 0,0 -1,0 -1,0 -1,-1 -1,-1 -1,-2 0,-1 0,-1 -1,-2 0,-1 0,-2 -1,-2 -1,-1 -2,-1 -1,-1 z' style='fill:%230900ff;fill-opacity:1;stroke:%23000000;stroke-width:0.7px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1' /%3E%3C/g%3E%3C/svg%3E"; + + gt.fillIconFocused = "data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' id='SVGRoot' version='1.1' viewBox='0 0 32 32' height='32px' width='32px'%3E%3Cdefs id='defs815' /%3E%3Cmetadata id='metadata818'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3Cdc:title%3E%3C/dc:title%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cg id='layer1'%3E%3Cpath id='path1382' d='m 13.466084,10.267728 -4.9000003,8.4 4.9000003,4.9 8.4,-4.9 z' style='opacity:1;fill:" + gt.pointHighlightColor.replace(/#/, '%23') + ";fill-opacity:1;stroke:%23000000;stroke-width:1.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none' /%3E%3Cpath id='path1384' d='M 16.266084,15.780798 V 6.273173' style='fill:none;stroke:%23000000;stroke-width:1.38;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Cpath id='path1405' d='m 20,16 c 0,0 2,-1 3,0 1,0 1,1 2,2 0,1 0,2 0,3 0,1 0,2 0,2 0,0 -1,0 -1,0 -1,-1 -1,-1 -1,-2 0,-1 0,-1 -1,-2 0,-1 0,-2 -1,-2 -1,-1 -2,-1 -1,-1 z' style='fill:%230900ff;fill-opacity:1;stroke:%23000000;stroke-width:0.7px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1' /%3E%3C/g%3E%3C/svg%3E"; + + if ('htmlInputId' in options) gt.html_input = document.getElementById(options.htmlInputId); + var cfgOptions = { + showCopyright: false, + //minimizeReflow: "all", + pan: { enabled: false }, + zoom: { enabled: false }, + showNavigation: false, + boundingBox: [-10, 10, 10, -10], + defaultAxes: {}, + axis: { + ticks: { + label: { highlight: false }, + insertTicks: false, + ticksDistance: 2, + minorTicks: 1, + minorHeight: 6, + majorHeight: 6, + tickEndings: [1, 1] + }, + highlight: false, + firstArrow: { size: 7 }, + lastArrow: { size: 7 }, + straightFirst: false, + straightLast: false + }, + grid: { gridX: gt.snapSizeX, gridY: gt.snapSizeY }, + }; + + // Merge options that are set by the problem. + if ('JSXGraphOptions' in options) $.extend(true, cfgOptions, cfgOptions, options.JSXGraphOptions); + + function setupBoard() { + gt.board = JXG.JSXGraph.initBoard(containerId + "_graph", cfgOptions); + gt.board.suspendUpdate(); + + // Move the axes defining points to the end so that the arrows go to the board edges. + var bbox = gt.board.getBoundingBox(); + gt.board.defaultAxes.x.point1.setPosition(JXG.COORDS_BY_USER, [bbox[0], 0]); + gt.board.defaultAxes.x.point2.setPosition(JXG.COORDS_BY_USER, [bbox[2], 0]); + gt.board.defaultAxes.y.point1.setPosition(JXG.COORDS_BY_USER, [0, bbox[3]]); + gt.board.defaultAxes.y.point2.setPosition(JXG.COORDS_BY_USER, [0, bbox[1]]); + + gt.board.create('text', [ + function() { return gt.board.getBoundingBox()[2] - 3 / gt.board.unitX; }, + function() { return 1.5 / gt.board.unitY; }, + function() { return '\\(x\\)'; } + ], { + anchorX: 'right', anchorY: 'bottom', highlight: false, + color: 'black', fixed: true, useMathJax: true + }); + gt.board.create('text', [ + function() { return 4.5 / gt.board.unitX; }, + function() { return gt.board.getBoundingBox()[1] + 2.5 / gt.board.unitY; }, + function() { return '\\(y\\)'; } + ], { + anchorX: 'left', anchorY: 'top', highlight: false, + color: 'black', fixed: true, useMathJax: true + }); + gt.current_pos_text = gt.board.create('text', [ + function() { return gt.board.getBoundingBox()[2] - 5 / gt.board.unitX; }, + function() { return gt.board.getBoundingBox()[3] + 5 / gt.board.unitY; }, ""], + { anchorX: 'right', anchorY: 'bottom', fixed: true }); + // Overwrite the popup infobox for points. + gt.board.highlightInfobox = function (x, y, el) { return gt.board.highlightCustomInfobox('', el); } + + if (!gt.isStatic) { + gt.board.on('move', function(e) { + var coords = gt.getMouseCoords(e); + if (gt.activeTool.updateHighlights(coords)) return; + if (!gt.selectedObj || !gt.selectedObj.updateTextCoords(coords)) + gt.setTextCoords(coords.usrCoords[1], coords.usrCoords[2]); + }); + + $(document).on('keydown.ToolDeactivate', function(e) { + if (e.key === 'Escape') gt.selectTool.activate(); + }); + } + + $(window).resize(function(e) { + if (gt.board.canvasWidth != graphDiv.width() || gt.board.canvasHeight != graphDiv.height()) + { + gt.board.resizeContainer(graphDiv.width(), graphDiv.height(), true); + gt.graphedObjs.forEach(function(object) { object.onResize(); }); + gt.staticObjs.forEach(function(object) { object.onResize(); }); + } + }); + + gt.drawSolid = true; + gt.graphedObjs = []; + gt.staticObjs = []; + gt.selectedObj = null; + + gt.board.unsuspendUpdate(); + } + + // Some utility functions. + gt.snapRound = function(x, snap) { + return Math.round(Math.round(x / snap) * snap * 100000) / 100000; + }; + + gt.setTextCoords = function(x, y) { + gt.current_pos_text.setText( + "(" + gt.snapRound(x, gt.snapSizeX) + ", " + gt.snapRound(y, gt.snapSizeY) + ")" + ); + }; + + gt.updateText = function() { + gt.html_input.value = gt.graphedObjs.reduce( + function(val, obj) { + return val + (val.length ? "," : "") + "{" + obj.stringify() + "}"; + }, ""); + }; + + gt.getMouseCoords = function(e) { + var i; + if (e[JXG.touchProperty]) { i = 0; } + + var cPos = gt.board.getCoordsTopLeftCorner(), + absPos = JXG.getPosition(e, i), + dx = absPos[0] - cPos[0], + dy = absPos[1] - cPos[1]; + + return new JXG.Coords(JXG.COORDS_BY_SCREEN, [dx, dy], gt.board); + }; + + gt.sign = function(x) { + x = +x; + if (Math.abs(x) < JXG.Math.eps) { return 0; } + return x > 0 ? 1 : -1; + }; + + gt.pointRegexp = /\( *(-?[0-9]*(?:\.[0-9]*)?), *(-?[0-9]*(?:\.[0-9]*)?) *\)/g; + + // Prevent paired points from being moved into the same position. This + // prevents lines and circles from being made degenerate. + gt.pairedPointDrag = function(e) { + if (this.X() == this.paired_point.X() && this.Y() == this.paired_point.Y()) { + var coords = gt.getMouseCoords(e); + var x_trans = coords.usrCoords[1] - this.paired_point.X(), + y_trans = coords.usrCoords[2] - this.paired_point.Y(); + if (y_trans > Math.abs(x_trans)) + this.setPosition(JXG.COORDS_BY_USER, [this.X(), this.Y() + gt.snapSizeY]); + else if (x_trans > Math.abs(y_trans)) + this.setPosition(JXG.COORDS_BY_USER, [this.X() + gt.snapSizeX, this.Y()]); + else if (x_trans < -Math.abs(y_trans)) + this.setPosition(JXG.COORDS_BY_USER, [this.X() - gt.snapSizeX, this.Y()]); + else + this.setPosition(JXG.COORDS_BY_USER, [this.X(), this.Y() - gt.snapSizeY]); + } + gt.updateObjects(); + gt.updateText(); + }; + + // Prevent paired points from being moved onto the same horizontal or + // vertical line. This prevents parabolas from being made degenerate. + gt.pairedPointDragRestricted = function(e) { + var coords = gt.getMouseCoords(e); + var new_x = this.X(), new_y = this.Y(); + if (this.X() == this.paired_point.X()) + { + if (coords.usrCoords[1] > this.paired_point.X()) new_x += gt.snapSizeX; + else new_x -= gt.snapSizeX; + } + if (this.Y() == this.paired_point.Y()) + { + if (coords.usrCoords[2] > this.paired_point.Y()) new_y += gt.snapSizeX; + else new_y -= gt.snapSizeX; + } + if (this.X() == this.paired_point.X() || this.Y() == this.paired_point.Y()) + this.setPosition(JXG.COORDS_BY_USER, [new_x, new_y]); + gt.updateObjects(); + gt.updateText(); + }; + + gt.createPoint = function(x, y, paired_point, restrict) { + var point = gt.board.create('point', [x, y], + { size: 2, snapToGrid: true, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false }); + point.on('down', function() { gt.board.containerObj.style.cursor = 'none'; }); + point.on('up', function() { gt.board.containerObj.style.cursor = 'auto'; }); + if (typeof(paired_point) !== 'undefined') { + point.paired_point = paired_point; + paired_point.paired_point = point; + paired_point.on('drag', restrict ? gt.pairedPointDragRestricted : gt.pairedPointDrag); + point.on('drag', restrict ? gt.pairedPointDragRestricted : gt.pairedPointDrag); + } + return point; + }; + + gt.updateObjects = function() { + gt.graphedObjs.forEach(function(obj) { obj.update(); }); + gt.staticObjs.forEach(function(obj) { obj.update(); }); + }; + + // Generic graph object class from which all the specific graph objects + // derive. + function GraphObject(jsxGraphObject) { + this.baseObj = jsxGraphObject; + this.baseObj.gtGraphObject = this; + this.definingPts = {}; + }; + GraphObject.prototype.blur = function() { + Object.values(this.definingPts).forEach(function(obj) { + obj.setAttribute({ visible: false }); + }); + this.baseObj.setAttribute({ strokeColor: gt.curveColor, strokeWidth: 2 }); + }; + GraphObject.prototype.focus = function() { + Object.values(this.definingPts).forEach(function(obj) { + obj.setAttribute({ + visible: true, strokeColor: gt.focusCurveColor, strokeWidth: 1, size: 3, + fillColor: gt.pointColor, highlightStrokeColor: gt.focusCurveColor, + highlightFillColor: gt.pointHighlightColor + }); + }); + this.baseObj.setAttribute({ strokeColor: gt.focusCurveColor, strokeWidth: 3 }); + gt.drawSolid = this.baseObj.getAttribute('dash') == 0; + if ('solidButton' in gt) gt.solidButton.prop('disabled', gt.drawSolid); + if ('dashedButton' in gt) gt.dashedButton.prop('disabled', !gt.drawSolid); + }; + GraphObject.prototype.update = function() { }; + GraphObject.prototype.fillCmp = function(point) { return 1; }; + GraphObject.prototype.remove = function() { + Object.values(this.definingPts).forEach(function(obj) { + gt.board.removeObject(obj); + }); + gt.board.removeObject(this.baseObj); + }; + GraphObject.prototype.setSolid = function(solid) { + this.baseObj.setAttribute({ dash: solid ? 0 : 2 }); + }; + GraphObject.prototype.stringify = function() { return ""; }; + GraphObject.prototype.id = function() { return this.baseObj.id; }; + GraphObject.prototype.on = function(e, handler, context) { this.baseObj.on(e, handler, context); }; + GraphObject.prototype.off = function(e, handler) { this.baseObj.off(e, handler); }; + GraphObject.prototype.onResize = function() { }; + GraphObject.prototype.updateTextCoords = function(coords) { + return !Object.keys(this.definingPts).every(function(point) { + if (this[point].hasPoint(coords.scrCoords[1], coords.scrCoords[2])) { + gt.setTextCoords(this[point].X(), this[point].Y()); + return false; + } + return true; + }, this.definingPts); + }; + GraphObject.restore = function(string) { + var data = string.match(/^(.*?),(.*)/); + if (data.length < 3) return false; + var obj = false; + Object.keys(gt.graphObjectTypes).every(function(type) { + if (data[1] == gt.graphObjectTypes[type].strId) { + obj = gt.graphObjectTypes[type].restore(data[2]); + return false; + } + return true; + }); + if (obj !== false) obj.blur(); + return obj; + }; + + // Line graph object + function Line(point1, point2, solid, color) { + GraphObject.call(this, gt.board.create('line', [point1, point2], { + fixed: true, highlight: false, strokeColor: color ? color : gt.underConstructionColor, + dash: solid ? 0 : 2 + })); + this.definingPts.point1 = point1; + this.definingPts.point2 = point2; + }; + Line.prototype = Object.create(GraphObject.prototype); + Object.defineProperty(Line.prototype, 'constructor', + { value: Line, enumerable: false, writable: true }); + Line.prototype.stringify = function() { + return [ + Line.strId, this.baseObj.getAttribute('dash') == 0 ? 'solid' : 'dashed', + "(" + gt.snapRound(this.definingPts.point1.X(), gt.snapSizeX) + "," + + gt.snapRound(this.definingPts.point1.Y(), gt.snapSizeY) + ")", + "(" + gt.snapRound(this.definingPts.point2.X(), gt.snapSizeX) + "," + + gt.snapRound(this.definingPts.point2.Y(), gt.snapSizeY) + ")" + ].join(","); + }; + Line.prototype.fillCmp = function(point) { + return gt.sign(JXG.Math.innerProduct(point, this.baseObj.stdform)); + }; + Line.strId = "line"; + Line.restore = function(string) { + var pointData; + var points = []; + while (pointData = gt.pointRegexp.exec(string)) + { points.push(pointData.slice(1, 3)); } + if (points.length < 2) return false; + var point1 = gt.createPoint(parseFloat(points[0][0]), parseFloat(points[0][1])); + var point2 = gt.createPoint(parseFloat(points[1][0]), parseFloat(points[1][1]), point1); + return new gt.graphObjectTypes.line(point1, point2, /solid/.test(string), gt.curveColor); + }; + + // Circle graph object + function Circle(center, point, solid, color) { + GraphObject.call(this, gt.board.create('circle', [center, point], { + fixed: true, highlight: false, strokeColor: color ? color : gt.underConstructionColor, + dash: solid ? 0 : 2 + })); + this.definingPts.center = center; + this.definingPts.point = point; + }; + Circle.prototype = Object.create(GraphObject.prototype); + Object.defineProperty(Circle.prototype, 'constructor', + { value: Circle, enumerable: false, writable: true }); + Circle.prototype.stringify = function() { + return [ + Circle.strId, (this.baseObj.getAttribute('dash') == 0 ? 'solid' : 'dashed'), + "(" + gt.snapRound(this.definingPts.center.X(), gt.snapSizeX) + "," + + gt.snapRound(this.definingPts.center.Y(), gt.snapSizeY) + ")", + "(" + gt.snapRound(this.definingPts.point.X(), gt.snapSizeX) + "," + + gt.snapRound(this.definingPts.point.Y(), gt.snapSizeY) + ")" + ].join(","); + }; + Circle.prototype.fillCmp = function(point) { + return gt.sign(this.baseObj.stdform[3] * + (point[1] * point[1] + point[2] * point[2]) + + JXG.Math.innerProduct(point, this.baseObj.stdform)); + }; + Circle.strId = "circle"; + Circle.restore = function(string) { + var pointData; + var points = []; + while (pointData = gt.pointRegexp.exec(string)) + { points.push(pointData.slice(1, 3)); } + if (points.length < 2) return false; + var center = gt.createPoint(parseFloat(points[0][0]), parseFloat(points[0][1])); + var point = gt.createPoint(parseFloat(points[1][0]), parseFloat(points[1][1]), center); + return new gt.graphObjectTypes.circle(center, point, /solid/.test(string), gt.curveColor); + }; + + // Parabola graph object. + // The underlying jsxgraph object is really a curve. The problem with the + // jsxgraph parabola object is that it can not be created from the vertex + // and a point on the graph of the parabola. + function aVal(vertex, point, vertical) { + return vertical ? + (point.Y() - vertex.Y()) / Math.pow(point.X() - vertex.X(), 2) : + (point.X() - vertex.X()) / Math.pow(point.Y() - vertex.Y(), 2); + } + + function createParabola(vertex, point, vertical, solid, color) { + if (vertical) return gt.board.create('curve', [ + // x coordinate of point on curve + function(x) { return x; }, + // y coordinate of point on curve + function(x) { + return aVal(vertex, point, vertical) * + Math.pow(x - vertex.X(), 2) + vertex.Y(); + }, + // domain minimum + function() { return gt.board.getBoundingBox()[0]; }, + // domain maximum + function() { return gt.board.getBoundingBox()[2]; } + ], { + strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.underConstructionColor, + dash: solid ? 0 : 2 + }); + else return gt.board.create('curve', [ + // x coordinate of point on curve + function(x) { + return aVal(vertex, point, vertical) * + Math.pow(x - vertex.Y(), 2) + vertex.X(); + }, + // y coordinate of point on curve + function(x) { return x; }, + // domain minimum + function() { return gt.board.getBoundingBox()[3]; }, + // domain maximum + function() { return gt.board.getBoundingBox()[1]; } + ], { + strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.underConstructionColor, + dash: solid ? 0 : 2 + }); + } + + function Parabola(vertex, point, vertical, solid, color) { + GraphObject.call(this, createParabola(vertex, point, vertical, solid, color)); + this.definingPts.vertex = vertex; + this.definingPts.point = point; + this.vertical = vertical; + } + Parabola.prototype = Object.create(GraphObject.prototype); + Object.defineProperty(Parabola.prototype, 'constructor', + { value: Parabola, enumerable: false, writable: true }); + Parabola.prototype.stringify = function() { + return [ + Parabola.strId, this.baseObj.getAttribute('dash') == 0 ? 'solid' : 'dashed', + this.vertical ? 'vertical' : 'horizontal', + "(" + gt.snapRound(this.definingPts.vertex.X(), gt.snapSizeX) + "," + + gt.snapRound(this.definingPts.vertex.Y(), gt.snapSizeY) + ")", + "(" + gt.snapRound(this.definingPts.point.X(), gt.snapSizeX) + "," + + gt.snapRound(this.definingPts.point.Y(), gt.snapSizeY) + ")" + ].join(","); + }; + Parabola.prototype.fillCmp = function(point) { + if (this.vertical) + return gt.sign(point[2] - this.baseObj.Y(point[1])); + else + return gt.sign(point[1] - this.baseObj.X(point[2])); + }; + Parabola.strId = "parabola"; + Parabola.restore = function(string) { + var pointData; + var points = []; + while (pointData = gt.pointRegexp.exec(string)) + { points.push(pointData.slice(1, 3)); } + if (points.length < 2) return false; + var vertex = gt.createPoint(parseFloat(points[0][0]), parseFloat(points[0][1])); + var point = gt.createPoint(parseFloat(points[1][0]), parseFloat(points[1][1]), vertex, true); + return new gt.graphObjectTypes.parabola(vertex, point, /vertical/.test(string), /solid/.test(string), gt.curveColor); + }; + + // Fill graph object + function Fill(point) { + point.setAttribute({ visible: false }); + GraphObject.call(this, point); + this.focused = true; + this.definingPts.point = point; + this.updateTimeout = 0; + this.update(); + var this_obj = this; + // The snapToGrid option does not allow centering an image on a point. + // The following implements a snap to grid method that does allow that. + this.definingPts.icon = gt.board.create('image', + [ + function() { return this_obj.focused ? gt.fillIconFocused : gt.fillIcon; }, + [point.X() - 12 / gt.board.unitX, point.Y() - 12 / gt.board.unitY], + [function() { return 24 / gt.board.unitX; }, function() { return 24 / gt.board.unitY; }] + ], + { withLabel: false, highlight: false, layer: 9, name: 'FillIcon' }); + this.definingPts.icon.gtGraphObject = this; + this.definingPts.icon.point = point; + this.isStatic = gt.isStatic; + if (!gt.isStatic) + { + this.on('down', function() { gt.board.containerObj.style.cursor = 'none'; }); + this.on('up', function() { gt.board.containerObj.style.cursor = 'auto'; }); + this.on('drag', function(e) { + var coords = gt.getMouseCoords(e); + var x = gt.snapRound(coords.usrCoords[1], gt.snapSizeX), + y = gt.snapRound(coords.usrCoords[2], gt.snapSizeY); + this.setPosition(JXG.COORDS_BY_USER, + [x - 12 / gt.board.unitX, y - 12 / gt.board.unitY]); + this.point.setPosition(JXG.COORDS_BY_USER, [x, y]); + this_obj.update(); + gt.updateText(); + }); + } + } + Fill.prototype = Object.create(GraphObject.prototype); + Object.defineProperty(Fill.prototype, 'constructor', + { value: Fill, enumerable: false, writable: true }); + // The fill object has a non-standard focus object. So focus/blur and + // on/off methods need to be overridden. + Fill.prototype.blur = function() { + this.focused = false; + this.definingPts.icon.setAttribute({ fixed: true }); + }; + Fill.prototype.focus = function() { + this.focused = true; + this.definingPts.icon.setAttribute({ fixed: false }); + }; + Fill.prototype.on = function(e, handler, context) { this.definingPts.icon.on(e, handler, context); }; + Fill.prototype.off = function(e, handler) { this.definingPts.icon.off(e, handler); }; + Fill.prototype.remove = function() { + if ('fillObj' in this) gt.board.removeObject(this.fillObj); + GraphObject.prototype.remove.call(this); + }; + Fill.prototype.update = function() { + if (this.isStatic) return; + if (this.updateTimeout) clearTimeout(this.updateTimeout); + var this_obj = this; + this.updateTimeout = setTimeout(function() { + this_obj.updateTimeout = 0; + if ('fillObj' in this_obj) { + gt.board.removeObject(this_obj.fillObj); + delete this_obj.fillObj; + } + + var centerPt = this_obj.definingPts.point.coords.usrCoords; + var allObjects = gt.graphedObjs.concat(gt.staticObjs); + + // Determine which side of each object needs to be shaded. If the point + // is on a graphed object, then don't fill. + var a_vals = Array(allObjects.length); + for (var i = 0; i < allObjects.length; ++i) { + a_vals[i] = allObjects[i].fillCmp(centerPt); + if (a_vals[i] == 0) return; + } + + var canvas = document.createElement('canvas'); + canvas.width = gt.board.canvasWidth; + canvas.height = gt.board.canvasHeight; + var context = canvas.getContext('2d'); + var colorLayerData = context.getImageData(0, 0, canvas.width, canvas.height); + + var fillPixel = function(pixelPos) { + colorLayerData.data[pixelPos] = Number('0x' + gt.fillColor.slice(1, 3)); + colorLayerData.data[pixelPos + 1] = Number('0x' + gt.fillColor.slice(3, 5)); + colorLayerData.data[pixelPos + 2] = Number('0x' + gt.fillColor.slice(5)); + colorLayerData.data[pixelPos + 3] = 255; + }; + + var isFillPixel = function(x, y) { + var curPixel = [1.0, (x - gt.board.origin.scrCoords[1]) / gt.board.unitX, + (gt.board.origin.scrCoords[2] - y) / gt.board.unitY]; + for (var i = 0; i < allObjects.length; ++i) { + if (allObjects[i].fillCmp(curPixel) != a_vals[i]) + return false; + } + return true; + }; + + for (var j = 0; j < canvas.width; ++j) { + for (var k = 0; k < canvas.height; ++k) { + if (isFillPixel(j, k)) fillPixel((k * canvas.width + j) * 4); + } + } + + context.putImageData(colorLayerData, 0, 0); + var dataURL = canvas.toDataURL('image/png'); + canvas.remove(); + + var boundingBox = gt.board.getBoundingBox(); + this_obj.fillObj = gt.board.create('image', [ + dataURL, + [boundingBox[0], boundingBox[3]], + [boundingBox[2] - boundingBox[0], boundingBox[1] - boundingBox[3]] + ], { withLabel: false, highlight: false, fixed: true, layer: 0 }); + + }, 100); + }; + Fill.prototype.onResize = function() { + this.definingPts.icon.setPosition(JXG.COORDS_BY_USER, + [this.definingPts.point.X() - 12 / gt.board.unitX, + this.definingPts.point.Y() - 12 / gt.board.unitY]) + gt.board.update(); + }; + Fill.prototype.updateTextCoords = function(coords) { + if (this.definingPts.point.hasPoint(coords.scrCoords[1], coords.scrCoords[2])) { + gt.setTextCoords(this.definingPts.point.X(), this.definingPts.point.Y()); + return true; + } + return false; + }; + Fill.prototype.stringify = function() { + return [ + Fill.strId, + "(" + gt.snapRound(this.baseObj.X(), gt.snapSizeX) + "," + + gt.snapRound(this.baseObj.Y(), gt.snapSizeY) + ")" + ].join(","); + }; + Fill.strId = "fill"; + Fill.restore = function(string) { + var pointData; + var points = []; + while (pointData = gt.pointRegexp.exec(string)) + { points.push(pointData.slice(1, 3)); } + if (!points.length) return false; + return new gt.graphObjectTypes.fill(gt.createPoint(parseFloat(points[0][0]), parseFloat(points[0][1]))); + }; + + gt.graphObjectTypes = {}; + gt.graphObjectTypes[Line.strId] = Line; + gt.graphObjectTypes[Parabola.strId] = Parabola; + gt.graphObjectTypes[Circle.strId] = Circle; + gt.graphObjectTypes[Fill.strId] = Fill; + + // Load any custom graph objects. + if ('customGraphObjects' in options) { + Object.keys(options.customGraphObjects).forEach(function(name) { + var graphObject = this[name]; + var parentObject = 'parent' in graphObject ? + (graphObject.parent ? gt.graphObjectTypes[graphObject.parent] : null) : GraphObject; + var customGraphObject; + if (parentObject) { + customGraphObject = function() { + if ('preInit' in graphObject) + parentObject.call(this, graphObject.preInit.apply(this, + [gt].concat(Array.prototype.slice.call(arguments)))); + else + parentObject.apply(this, arguments); + if ('postInit' in graphObject) + graphObject.postInit.apply(this, + [gt].concat(Array.prototype.slice.call(arguments))); + }; + customGraphObject.prototype = Object.create(parentObject.prototype); + Object.defineProperty(customGraphObject.prototype, 'constructor', + { value: customGraphObject, enumerable: false, writable: true }); + } else { + customGraphObject = function() { + graphObject.preInit.apply(this, [gt].concat(Array.prototype.slice.call(arguments))); + }; + } + if ('blur' in graphObject) { + customGraphObject.prototype.blur = function() { + if (graphObject.blur.call(this, gt) && parentObject) { + parentObject.prototype.blur.call(this); + } + }; + } + if ('focus' in graphObject) { + customGraphObject.prototype.focus = function() { + if (graphObject.focus.call(this, gt) && parentObject) { + parentObject.prototype.focus.call(this); + } + }; + } + if ('update' in graphObject) { + customGraphObject.prototype.update = function() { + graphObject.update.call(this, gt) + }; + } + if ('onResize' in graphObject) { + customGraphObject.prototype.onResize = function() { + graphObject.onResize.call(this, gt); + }; + } + if ('updateTextCoords' in graphObject) { + customGraphObject.prototype.updateTextCoords = function(coords) { + return graphObject.updateTextCoords.call(this, gt, coords); + }; + } + if ('fillCmp' in graphObject) { + customGraphObject.prototype.fillCmp = function(point) { + return graphObject.fillCmp.call(this, gt, point) + }; + } + if ('remove' in graphObject) { + customGraphObject.prototype.remove = function() { + graphObject.remove.call(this, gt); + if (parentObject) parentObject.prototype.remove.call(this); + }; + } + if ('setSolid' in graphObject) { + customGraphObject.prototype.setSolid = function(solid) { + graphObject.setSolid.call(this, gt, solid) + }; + } + if ('on' in graphObject) { + customGraphObject.prototype.on = function(e, handler, context) { + graphObject.on.call(this, e, handler, context) + }; + } + if ('off' in graphObject) { + customGraphObject.prototype.off = function(e, handler) { + graphObject.off.call(this, e, handler) + }; + } + if ('stringify' in graphObject) { + customGraphObject.prototype.stringify = function() { + return [customGraphObject.strId, graphObject.stringify.call(this, gt)].join(","); + }; + } + if ('restore' in graphObject) { + customGraphObject.restore = function(string) { + return graphObject.restore.call(this, gt, string); + }; + } else if (parentObject) + customGraphObject.restore = parentObject.restore; + + if ('helperMethods' in graphObject) { + Object.keys(graphObject.helperMethods).forEach(function(method) { + customGraphObject[method] = function() { + return graphObject.helperMethods[method].apply(this, + [gt].concat(Array.prototype.slice.call(arguments))); + }; + }); + } + customGraphObject.strId = name; + gt.graphObjectTypes[customGraphObject.strId] = customGraphObject; + }, options.customGraphObjects); + } + + // Generic tool class from which all the graphing tools derive. Most of + // the methods, if overridden, must call the corresponding generic method. + // At this point the updateHighlights method is the only one that this + // doesn't need to be done with. + function GenericTool(container, name, tooltip) { + this.button = $(""); + var this_tool = this; + this.button.on('click', function () { this_tool.activate(); }); + container.append(this.button); + this.hlObjs = {}; + } + GenericTool.prototype.activate = function() { + gt.activeTool.deactivate(); + gt.activeTool = this; + this.button.blur(); + this.button.prop('disabled', true); + if (gt.selectedObj) { gt.selectedObj.blur(); } + gt.selectedObj = null; + }; + GenericTool.prototype.finish = function() { + gt.updateObjects(); + gt.updateText(); + gt.board.update(); + gt.selectTool.activate(); + }; + GenericTool.prototype.updateHighlights = function(coords) { return false; }; + GenericTool.prototype.removeHighlights = function() { + Object.keys(this.hlObjs).forEach(function(obj) { + gt.board.removeObject(this[obj]); + delete this[obj]; + }, this.hlObjs); + }; + GenericTool.prototype.deactivate = function() { + this.button.prop('disabled', false); + this.removeHighlights(); + }; + + // Select tool + function SelectTool(container) { GenericTool.call(this, container, "select", "Object Selection Tool"); } + SelectTool.prototype = Object.create(GenericTool.prototype); + Object.defineProperty(SelectTool.prototype, 'constructor', + { value: SelectTool, enumerable: false, writable: true }); + SelectTool.prototype.selectionChanged = function(e) { + if (gt.selectedObj) + { + if (gt.selectedObj.id() != this.gtGraphObject.id()) + { + // Don't allow the selection of a new object if the pointer + // is in the vicinity of one of the currently selected + // object's defining points. + var coords = gt.getMouseCoords(e); + var points = Object.values(gt.selectedObj.definingPts); + for (var i = 0; i < points.length; ++i) + { + if (points[i].X() == gt.snapRound(coords.usrCoords[1], gt.snapSizeX) && + points[i].Y() == gt.snapRound(coords.usrCoords[2], gt.snapSizeY)) + return; + } + gt.selectedObj.blur(); + } + else return; + } + gt.selectedObj = this.gtGraphObject; + gt.selectedObj.focus(); + }; + SelectTool.prototype.activate = function(initialize) { + // Cache the currently selected object to re-select after the GenericTool + // activate method de-selects it. + var selectedObj = gt.selectedObj; + GenericTool.prototype.activate.call(this); + if (selectedObj) gt.selectedObj = selectedObj; + // If only one object has been graphed, select it. + if (!initialize && gt.graphedObjs.length == 1) { + gt.selectedObj = gt.graphedObjs[0]; + } + if (gt.selectedObj) { gt.selectedObj.focus(); } + gt.graphedObjs.forEach(function(obj) { obj.on('down', this.selectionChanged); }, this); + }; + SelectTool.prototype.deactivate = function() { + gt.graphedObjs.forEach(function(obj) { obj.off('down', this.selectionChanged); }, this); + GenericTool.prototype.deactivate.call(this); + }; + + // Line graphing tool + function LineTool(container, iconName, tooltip) { + GenericTool.call(this, container, iconName ? iconName : "line", tooltip ? tooltip : "Line Tool"); + } + LineTool.prototype = Object.create(GenericTool.prototype); + Object.defineProperty(LineTool.prototype, 'constructor', + { value: LineTool, enumerable: false, writable: true }); + LineTool.prototype.updateHighlights = function(coords) { + if ('hl_line' in this.hlObjs) this.hlObjs.hl_line.setAttribute({ dash: gt.drawSolid ? 0 : 2 }); + if (typeof(coords) === 'undefined') return false; + if ('point1' in this && gt.snapRound(coords.usrCoords[1], gt.snapSizeX) == this.point1.X() && + gt.snapRound(coords.usrCoords[2], gt.snapSizeY) == this.point1.Y()) + return false; + if (!('hl_point' in this.hlObjs)) { + this.hlObjs.hl_point = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], { + size: 2, color: gt.underConstructionColor, fixed: true, snapToGrid: true, + snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false + }); + if ('point1' in this) + this.hlObjs.hl_line = gt.board.create('line', [this.point1, this.hlObjs.hl_point], { + fixed: true, strokeColor: gt.underConstructionColor, highlight: false, + dash: gt.drawSolid ? 0 : 2 + }); + } + else + this.hlObjs.hl_point.setPosition(JXG.COORDS_BY_USER, [coords.usrCoords[1], coords.usrCoords[2]]); + + gt.setTextCoords(this.hlObjs.hl_point.X(), this.hlObjs.hl_point.Y()); + gt.board.update(); + return true; + }; + LineTool.prototype.deactivate = function() { + gt.board.off('up'); + if ('point1' in this) gt.board.removeObject(this.point1); + delete this.point1; + gt.board.containerObj.style.cursor = 'auto'; + GenericTool.prototype.deactivate.call(this); + }; + LineTool.prototype.activate = function() { + GenericTool.prototype.activate.call(this); + gt.board.containerObj.style.cursor = 'none'; + var this_tool = this; + gt.board.on('up', function(e) { + var coords = gt.getMouseCoords(e); + // Don't allow the point to be created off the board. + if (!gt.board.hasPoint(coords.usrCoords[1], coords.usrCoords[2])) return; + gt.board.off('up'); + this_tool.point1 = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], + { size: 2, snapToGrid: true, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false }); + this_tool.point1.setAttribute({ fixed: true }); + this_tool.removeHighlights(); + + gt.board.on('up', function(e) { + var coords = gt.getMouseCoords(e); + + // Don't allow the second point to be created on top of the first or off the board + if ((this_tool.point1.X() == gt.snapRound(coords.usrCoords[1], gt.snapSizeX) && + this_tool.point1.Y() == gt.snapRound(coords.usrCoords[2], gt.snapSizeY)) || + !gt.board.hasPoint(coords.usrCoords[1], coords.usrCoords[2])) + return; + gt.board.off('up'); + + this_tool.point1.setAttribute({ fixed: false }); + this_tool.point1.on('down', function() { gt.board.containerObj.style.cursor = 'none'; }); + this_tool.point1.on('up', function() { gt.board.containerObj.style.cursor = 'auto'; }); + + gt.selectedObj = new gt.graphObjectTypes.line(this_tool.point1, + gt.createPoint(coords.usrCoords[1], coords.usrCoords[2], this_tool.point1), + gt.drawSolid); + gt.graphedObjs.push(gt.selectedObj); + delete this_tool.point1; + + this_tool.finish(); + }); + + gt.board.update(); + }); + }; + + // Circle graphing tool + function CircleTool(container, iconName, tooltip) { + GenericTool.call(this, container, iconName ? iconName : "circle", tooltip ? tooltip : "Circle Tool"); + } + CircleTool.prototype = Object.create(GenericTool.prototype); + Object.defineProperty(CircleTool.prototype, 'constructor', + { value: CircleTool, enumerable: false, writable: true }); + CircleTool.prototype.updateHighlights = function(coords) { + if ('hl_circle' in this.hlObjs) this.hlObjs.hl_circle.setAttribute({ dash: gt.drawSolid ? 0 : 2 }); + if (typeof(coords) === 'undefined') return false; + if ('center' in this && gt.snapRound(coords.usrCoords[1], gt.snapSizeX) == this.center.X() && + gt.snapRound(coords.usrCoords[2], gt.snapSizeY) == this.center.Y()) + return false; + if (!('hl_point' in this.hlObjs)) { + this.hlObjs.hl_point = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], { + size: 2, color: gt.underConstructionColor, fixed: true, snapToGrid: true, + snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false + }); + if ('center' in this) + this.hlObjs.hl_circle = gt.board.create('circle', [this.center, this.hlObjs.hl_point], { + fixed: true, strokeColor: gt.underConstructionColor, highlight: false, + dash: gt.drawSolid ? 0 : 2 + }); + } + else + this.hlObjs.hl_point.setPosition(JXG.COORDS_BY_USER, [coords.usrCoords[1], coords.usrCoords[2]]); + + gt.setTextCoords(this.hlObjs.hl_point.X(), this.hlObjs.hl_point.Y()); + gt.board.update(); + return true; + }; + CircleTool.prototype.deactivate = function() { + gt.board.off('up'); + if ('center' in this) gt.board.removeObject(this.center); + delete this.center; + gt.board.containerObj.style.cursor = 'auto'; + GenericTool.prototype.deactivate.call(this); + }; + CircleTool.prototype.activate = function() { + GenericTool.prototype.activate.call(this); + gt.board.containerObj.style.cursor = 'none'; + var this_tool = this; + gt.board.on('up', function(e) { + var coords = gt.getMouseCoords(e); + // Don't allow the point to be created off the board. + if (!gt.board.hasPoint(coords.usrCoords[1], coords.usrCoords[2])) return; + gt.board.off('up'); + this_tool.center = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], + { size: 2, snapToGrid: true, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false }); + this_tool.center.setAttribute({ fixed: true }); + this_tool.removeHighlights(); + + gt.board.on('up', function(e) { + var coords = gt.getMouseCoords(e); + + // Don't allow the second point to be created on top of the center or off the board + if ((this_tool.center.X() == gt.snapRound(coords.usrCoords[1], gt.snapSizeX) && + this_tool.center.Y() == gt.snapRound(coords.usrCoords[2], gt.snapSizeY)) || + !gt.board.hasPoint(coords.usrCoords[1], coords.usrCoords[2])) + return; + gt.board.off('up'); + + this_tool.center.setAttribute({ fixed: false }); + this_tool.center.on('down', function() { gt.board.containerObj.style.cursor = 'none'; }); + this_tool.center.on('up', function() { gt.board.containerObj.style.cursor = 'auto'; }); + + gt.selectedObj = new gt.graphObjectTypes.circle(this_tool.center, + gt.createPoint(coords.usrCoords[1], coords.usrCoords[2], this_tool.center), + gt.drawSolid); + gt.graphedObjs.push(gt.selectedObj); + delete this_tool.center; + + this_tool.finish(); + }); + + gt.board.update(); + }); + }; + + // Parabola graphing tool + function ParabolaTool(container, vertical, iconName, tooltip) { + GenericTool.call(this, container, + iconName ? iconName : (vertical ? "vertical-parabola" : "horizontal-parabola"), + tooltip ? tooltip : (vertical ? "Vertical Parabola Tool" : "Horizontal Parabola Tool")); + this.vertical = vertical; + } + ParabolaTool.prototype = Object.create(GenericTool.prototype); + Object.defineProperty(ParabolaTool.prototype, 'constructor', + { value: ParabolaTool, enumerable: false, writable: true }); + ParabolaTool.prototype.updateHighlights = function(coords) { + if ('hl_parabola' in this.hlObjs) this.hlObjs.hl_parabola.setAttribute({ dash: gt.drawSolid ? 0 : 2 }); + if (typeof(coords) === 'undefined') return false; + if ('vertex' in this && + (gt.snapRound(coords.usrCoords[1], gt.snapSizeX) == this.vertex.X() || + gt.snapRound(coords.usrCoords[2], gt.snapSizeY) == this.vertex.Y())) + return false; + if (!('hl_point' in this.hlObjs)) { + this.hlObjs.hl_point = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], { + size: 2, color: gt.underConstructionColor, fixed: true, snapToGrid: true, + snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, + highlight: false, withLabel: false + }); + if ('vertex' in this) + this.hlObjs.hl_parabola = createParabola(this.vertex, this.hlObjs.hl_point, this.vertical, + gt.drawSolid, gt.underConstructionColor); + } + else + this.hlObjs.hl_point.setPosition(JXG.COORDS_BY_USER, [coords.usrCoords[1], coords.usrCoords[2]]); + + gt.setTextCoords(this.hlObjs.hl_point.X(), this.hlObjs.hl_point.Y()); + gt.board.update(); + return true; + }; + ParabolaTool.prototype.deactivate = function() { + gt.board.off('up'); + if ('vertex' in this) gt.board.removeObject(this.vertex); + delete this.vertex; + gt.board.containerObj.style.cursor = 'auto'; + GenericTool.prototype.deactivate.call(this); + }; + ParabolaTool.prototype.activate = function() { + GenericTool.prototype.activate.call(this); + gt.board.containerObj.style.cursor = 'none'; + var this_tool = this; + gt.board.on('up', function(e) { + var coords = gt.getMouseCoords(e); + // Don't allow the point to be created off the board. + if (!gt.board.hasPoint(coords.usrCoords[1], coords.usrCoords[2])) return; + gt.board.off('up'); + this_tool.vertex = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], + { size: 2, snapToGrid: true, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false }); + this_tool.vertex.setAttribute({ fixed: true }); + this_tool.removeHighlights(); + + gt.board.on('up', function(e) { + var coords = gt.getMouseCoords(e); + + // Don't allow the second point to be created on the same + // horizontal or vertical line as the vertex or off the board. + if ((this_tool.vertex.X() == gt.snapRound(coords.usrCoords[1], gt.snapSizeX) || + this_tool.vertex.Y() == gt.snapRound(coords.usrCoords[2], gt.snapSizeY)) || + !gt.board.hasPoint(coords.usrCoords[1], coords.usrCoords[2])) + return; + + gt.board.off('up'); + + this_tool.vertex.setAttribute({ fixed: false }); + this_tool.vertex.on('down', function() { gt.board.containerObj.style.cursor = 'none'; }); + this_tool.vertex.on('up', function() { gt.board.containerObj.style.cursor = 'auto'; }); + + gt.selectedObj = new gt.graphObjectTypes.parabola(this_tool.vertex, + gt.createPoint(coords.usrCoords[1], coords.usrCoords[2], this_tool.vertex, true), + this_tool.vertical, gt.drawSolid); + gt.graphedObjs.push(gt.selectedObj); + delete this_tool.vertex; + + this_tool.finish(); + }); + + gt.board.update(); + }); + }; + + function VerticalParabolaTool(container, iconName, tooltip) { + ParabolaTool.call(this, container, true, iconName, tooltip); + } + VerticalParabolaTool.prototype = Object.create(ParabolaTool.prototype); + Object.defineProperty(VerticalParabolaTool.prototype, 'constructor', + { value: VerticalParabolaTool, enumerable: false, writable: true }); + + function HorizontalParabolaTool(container, iconName, tooltip) { + ParabolaTool.call(this, container, false, iconName, tooltip); + } + HorizontalParabolaTool.prototype = Object.create(ParabolaTool.prototype); + Object.defineProperty(HorizontalParabolaTool.prototype, 'constructor', + { value: HorizontalParabolaTool, enumerable: false, writable: true }); + + // Fill tool + function FillTool(container, iconName, tooltip) { + GenericTool.call(this, container, iconName ? iconName : "fill", tooltip ? tooltip : "Region Shading Tool"); + } + FillTool.prototype = Object.create(GenericTool.prototype); + Object.defineProperty(FillTool.prototype, 'constructor', + { value: FillTool, enumerable: false, writable: true }); + FillTool.prototype.updateHighlights = function(coords) { + if (typeof(coords) === 'undefined') return false; + if (!('hl_point' in this.hlObjs)) { + this.hlObjs.hl_point = gt.board.create('image', [ + gt.fillIcon, [ + gt.snapRound(coords.usrCoords[1], gt.snapSizeX) - 12 / gt.board.unitX, + gt.snapRound(coords.usrCoords[2], gt.snapSizeY) - 12 / gt.board.unitY + ], [24 / gt.board.unitX, 24 / gt.board.unitY] + ], { withLabel: false, highlight: false, layer: 9 }); + } + else + this.hlObjs.hl_point.setPosition(JXG.COORDS_BY_USER, [ + gt.snapRound(coords.usrCoords[1], gt.snapSizeX) - 12 / gt.board.unitX, + gt.snapRound(coords.usrCoords[2], gt.snapSizeY) - 12 / gt.board.unitY + ]); + + gt.setTextCoords(coords.usrCoords[1], coords.usrCoords[2]); + gt.board.update(); + return true; + }; + FillTool.prototype.deactivate = function() { + gt.board.off('up'); + gt.board.containerObj.style.cursor = 'auto'; + GenericTool.prototype.deactivate.call(this); + }; + FillTool.prototype.activate = function() { + GenericTool.prototype.activate.call(this); + gt.board.containerObj.style.cursor = 'none'; + gt.board.on('up', function(e) { + gt.board.off('up'); + var coords = gt.getMouseCoords(e); + + // Don't allow the fill to be created off the board + if (!gt.board.hasPoint(coords.usrCoords[1], coords.usrCoords[2])) return; + gt.board.off('up'); + + gt.selectedObj = new gt.graphObjectTypes.fill(gt.createPoint(coords.usrCoords[1], coords.usrCoords[2])); + gt.graphedObjs.push(gt.selectedObj); + + gt.updateText(); + gt.board.update(); + gt.selectTool.activate(); + }); + }; + + // Draw objects solid or dashed. Makes the currently selected object (if + // any) solid or dashed, and anything drawn while the tool is selected will + // be drawn solid or dashed. + function toggleSolidity(e) { + this.blur(); + if ('solidButton' in gt) gt.solidButton.prop('disabled', e.data.solid); + if ('dashedButton' in gt) gt.dashedButton.prop('disabled', !e.data.solid); + if (gt.selectedObj) + { + gt.selectedObj.setSolid(e.data.solid); + gt.updateText(); + } + gt.drawSolid = e.data.solid; + gt.activeTool.updateHighlights(); + } + + // Delete the selected object. + function deleteObject() { + this.blur(); + if (!gt.selectedObj) return; + + var modal = $(''); + modal.modal('show'); + $('.modal-backdrop').css('opacity', '0.2'); + modal.on('hidden', function() { modal.remove(); }); + + $('#gt-confirm-delete').on('click', function() { + for (var i = 0; i < gt.graphedObjs.length; ++i) { + if (gt.graphedObjs[i].id() === gt.selectedObj.id()) { + gt.graphedObjs[i].remove(); + gt.graphedObjs.splice(i, 1); + break; + } + } + gt.selectedObj = null; + gt.updateObjects(); + gt.updateText(); + modal.modal('hide'); + }); + } + + // Remove all graphed objects. + function clearGraph() { + this.blur(); + if (gt.graphedObjs.length == 0) return; + + var modal = $(''); + modal.modal('show'); + $('.modal-backdrop').css('opacity', '0.2'); + modal.on('hidden', function() { modal.remove(); }); + + $('#gt-confirm-clear').on('click', function() { + gt.graphedObjs.forEach(function(obj) { obj.remove(); }); + gt.graphedObjs = []; + gt.selectedObj = null; + gt.selectTool.activate(); + gt.html_input.value = ""; + modal.modal('hide'); + }); + } + + function SolidDashTool(container) { + var solidDashBox = $("
"); + // The draw solid button is active by default. + gt.solidButton = + $("") + .on('click', { solid: true }, toggleSolidity); + solidDashBox.append(gt.solidButton); + gt.dashedButton = + $("") + .on('click', { solid: false }, toggleSolidity); + solidDashBox.append(gt.dashedButton); + container.append(solidDashBox); + } + + gt.toolTypes = { + LineTool: LineTool, + CircleTool: CircleTool, + VerticalParabolaTool: VerticalParabolaTool, + HorizontalParabolaTool: HorizontalParabolaTool, + FillTool: FillTool, + SolidDashTool: SolidDashTool + }; + + // Create the tools and html elements. + var graphDiv = $("
"); + graphContainer.append(graphDiv); + + if (!gt.isStatic) { + var buttonBox = $("
"); + gt.selectTool = new SelectTool(buttonBox); + + // Load any custom tools. + if ('customTools' in options) { + Object.keys(options.customTools).forEach(function(tool) { + var toolObject = this[tool]; + var parentTool = 'parent' in toolObject ? + (toolObject.parent ? gt.toolTypes[toolObject.parent] : null) : GenericTool; + var customTool; + if (parentTool) { + customTool = function(container) { + parentTool.call(this, container, toolObject.iconName, toolObject.tooltip); + if ('initialize' in toolObject) toolObject.initialize.call(this, gt, container); + }; + customTool.prototype = Object.create(parentTool.prototype); + Object.defineProperty(customTool.prototype, 'constructor', + { value: customTool, enumerable: false, writable: true }); + } else { + customTool = function() { + toolObject.initialize.call(this, gt, container); + }; + } + if ('activate' in toolObject) { + customTool.prototype.activate = function() { + parentTool.prototype.activate.call(this); + toolObject.activate.call(this, gt); + }; + } + if ('deactivate' in toolObject) { + customTool.prototype.deactivate = function() { + toolObject.deactivate.call(this, gt); + parentTool.prototype.deactivate.call(this); + }; + } + if ('updateHighlights' in toolObject) { + customTool.prototype.updateHighlights = function(coords) { + return toolObject.updateHighlights.call(this, gt, coords); + }; + } + if ('removeHighlights' in toolObject) { + customTool.prototype.removeHighlights = function() { + toolObject.removeHighlights.call(this, gt); + parentTool.prototype.removeHighlights.call(this); + }; + } + if ('helperMethods' in toolObject) { + Object.keys(toolObject.helperMethods).forEach(function(method) { + customTool[method] = function() { + return toolObject.helperMethods[method].apply(this, + [gt].concat(Array.prototype.slice.call(arguments))); + }; + }); + } + gt.toolTypes[tool] = customTool; + }, options.customTools); + } + + availableTools.forEach(function(tool) { + if (tool in gt.toolTypes) { + new gt.toolTypes[tool](buttonBox); + } else + console.log("Unknown tool: " + tool); + }); + + buttonBox.append($("") + .on('click', deleteObject)); + buttonBox.append($("") + .on('click', clearGraph)); + + graphContainer.append(buttonBox); + + $('.gt-button[data-toggle="tooltip"]').tooltip({ trigger: 'hover', placement: 'bottom', delay: { show: 1000, hide: 0 } }); + } + + setupBoard(); + + // Restore data from previous attempts if available + function restoreObjects(data, objectsAreStatic) { + gt.board.suspendUpdate(); + var tmpIsStatic = gt.isStatic; + gt.isStatic = objectsAreStatic; + var objectRegexp = /{(.*?)}/g; + var objectData; + while (objectData = objectRegexp.exec(data)) { + var obj = GraphObject.restore(objectData[1]); + if (obj !== false) + { + if (objectsAreStatic) gt.staticObjs.push(obj) + else gt.graphedObjs.push(obj); + } + } + gt.isStatic = tmpIsStatic; + gt.updateObjects(); + gt.board.unsuspendUpdate(); + } + if ('html_input' in gt) restoreObjects(gt.html_input.value, false); + if ('staticObjects' in options && typeof(options.staticObjects) === 'string' && options.staticObjects.length) + restoreObjects(options.staticObjects, true); + if (!gt.isStatic) { + gt.updateText(); + gt.activeTool = gt.selectTool; + gt.activeTool.activate(true); + } +} diff --git a/htdocs/js/apps/GraphTool/graphtool.min.js b/htdocs/js/apps/GraphTool/graphtool.min.js new file mode 100644 index 0000000000..c5b0b07802 --- /dev/null +++ b/htdocs/js/apps/GraphTool/graphtool.min.js @@ -0,0 +1 @@ +"use strict";function graphTool(o,t){if(!document.getElementById(o+"_graph")){var e=$("#"+o);if("0px"!=e.css("width")){var b={curveColor:"#0000a6",focusCurveColor:"#0000f5",fillColor:"#a384e5",pointColor:"orange",pointHighlightColor:"yellow",underConstructionColor:"orange"};b.snapSizeX=t.snapSizeX?t.snapSizeX:1,b.snapSizeY=t.snapSizeY?t.snapSizeY:1,b.isStatic="isStatic"in t&&t.isStatic;var i=t.availableTools?t.availableTools:["LineTool","CircleTool","VerticalParabolaTool","HorizontalParabolaTool","FillTool","SolidDashTool"];b.fillIcon="data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' id='SVGRoot' version='1.1' viewBox='0 0 32 32' height='32px' width='32px'%3E%3Cdefs id='defs815' /%3E%3Cmetadata id='metadata818'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3Cdc:title%3E%3C/dc:title%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cg id='layer1'%3E%3Cpath id='path1382' d='m 13.466084,10.267728 -4.9000003,8.4 4.9000003,4.9 8.4,-4.9 z' style='opacity:1;fill:"+b.fillColor.replace(/#/,"%23")+";fill-opacity:1;stroke:%23000000;stroke-width:1.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none' /%3E%3Cpath id='path1384' d='M 16.266084,15.780798 V 6.273173' style='fill:none;stroke:%23000000;stroke-width:1.38;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Cpath id='path1405' d='m 20,16 c 0,0 2,-1 3,0 1,0 1,1 2,2 0,1 0,2 0,3 0,1 0,2 0,2 0,0 -1,0 -1,0 -1,-1 -1,-1 -1,-2 0,-1 0,-1 -1,-2 0,-1 0,-2 -1,-2 -1,-1 -2,-1 -1,-1 z' style='fill:%230900ff;fill-opacity:1;stroke:%23000000;stroke-width:0.7px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1' /%3E%3C/g%3E%3C/svg%3E",b.fillIconFocused="data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' id='SVGRoot' version='1.1' viewBox='0 0 32 32' height='32px' width='32px'%3E%3Cdefs id='defs815' /%3E%3Cmetadata id='metadata818'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3Cdc:title%3E%3C/dc:title%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cg id='layer1'%3E%3Cpath id='path1382' d='m 13.466084,10.267728 -4.9000003,8.4 4.9000003,4.9 8.4,-4.9 z' style='opacity:1;fill:"+b.pointHighlightColor.replace(/#/,"%23")+";fill-opacity:1;stroke:%23000000;stroke-width:1.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none' /%3E%3Cpath id='path1384' d='M 16.266084,15.780798 V 6.273173' style='fill:none;stroke:%23000000;stroke-width:1.38;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1' /%3E%3Cpath id='path1405' d='m 20,16 c 0,0 2,-1 3,0 1,0 1,1 2,2 0,1 0,2 0,3 0,1 0,2 0,2 0,0 -1,0 -1,0 -1,-1 -1,-1 -1,-2 0,-1 0,-1 -1,-2 0,-1 0,-2 -1,-2 -1,-1 -2,-1 -1,-1 z' style='fill:%230900ff;fill-opacity:1;stroke:%23000000;stroke-width:0.7px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1' /%3E%3C/g%3E%3C/svg%3E","htmlInputId"in t&&(b.html_input=document.getElementById(t.htmlInputId));var r={showCopyright:!1,pan:{enabled:!1},zoom:{enabled:!1},showNavigation:!1,boundingBox:[-10,10,10,-10],defaultAxes:{},axis:{ticks:{label:{highlight:!1},insertTicks:!1,ticksDistance:2,minorTicks:1,minorHeight:6,majorHeight:6,tickEndings:[1,1]},highlight:!1,firstArrow:{size:7},lastArrow:{size:7},straightFirst:!1,straightLast:!1},grid:{gridX:b.snapSizeX,gridY:b.snapSizeY}};"JSXGraphOptions"in t&&$.extend(!0,r,r,t.JSXGraphOptions),b.snapRound=function(t,o){return Math.round(Math.round(t/o)*o*1e5)/1e5},b.setTextCoords=function(t,o){b.current_pos_text.setText("("+b.snapRound(t,b.snapSizeX)+", "+b.snapRound(o,b.snapSizeY)+")")},b.updateText=function(){b.html_input.value=b.graphedObjs.reduce(function(t,o){return t+(t.length?",":"")+"{"+o.stringify()+"}"},"")},b.getMouseCoords=function(t){var o;t[JXG.touchProperty]&&(o=0);var e=b.board.getCoordsTopLeftCorner(),i=JXG.getPosition(t,o),r=i[0]-e[0],n=i[1]-e[1];return new JXG.Coords(JXG.COORDS_BY_SCREEN,[r,n],b.board)},b.sign=function(t){return t=+t,Math.abs(t)Math.abs(e)?this.setPosition(JXG.COORDS_BY_USER,[this.X(),this.Y()+b.snapSizeY]):e>Math.abs(i)?this.setPosition(JXG.COORDS_BY_USER,[this.X()+b.snapSizeX,this.Y()]):e<-Math.abs(i)?this.setPosition(JXG.COORDS_BY_USER,[this.X()-b.snapSizeX,this.Y()]):this.setPosition(JXG.COORDS_BY_USER,[this.X(),this.Y()-b.snapSizeY])}b.updateObjects(),b.updateText()},b.pairedPointDragRestricted=function(t){var o=b.getMouseCoords(t),e=this.X(),i=this.Y();this.X()==this.paired_point.X()&&(o.usrCoords[1]>this.paired_point.X()?e+=b.snapSizeX:e-=b.snapSizeX),this.Y()==this.paired_point.Y()&&(o.usrCoords[2]>this.paired_point.Y()?i+=b.snapSizeX:i-=b.snapSizeX),this.X()!=this.paired_point.X()&&this.Y()!=this.paired_point.Y()||this.setPosition(JXG.COORDS_BY_USER,[e,i]),b.updateObjects(),b.updateText()},b.createPoint=function(t,o,e,i){var r=b.board.create("point",[t,o],{size:2,snapToGrid:!0,snapSizeX:b.snapSizeX,snapSizeY:b.snapSizeY,withLabel:!1});return r.on("down",function(){b.board.containerObj.style.cursor="none"}),r.on("up",function(){b.board.containerObj.style.cursor="auto"}),void 0!==e&&((r.paired_point=e).paired_point=r,e.on("drag",i?b.pairedPointDragRestricted:b.pairedPointDrag),r.on("drag",i?b.pairedPointDragRestricted:b.pairedPointDrag)),r},b.updateObjects=function(){b.graphedObjs.forEach(function(t){t.update()}),b.staticObjs.forEach(function(t){t.update()})},a.prototype.blur=function(){Object.values(this.definingPts).forEach(function(t){t.setAttribute({visible:!1})}),this.baseObj.setAttribute({strokeColor:b.curveColor,strokeWidth:2})},a.prototype.focus=function(){Object.values(this.definingPts).forEach(function(t){t.setAttribute({visible:!0,strokeColor:b.focusCurveColor,strokeWidth:1,size:3,fillColor:b.pointColor,highlightStrokeColor:b.focusCurveColor,highlightFillColor:b.pointHighlightColor})}),this.baseObj.setAttribute({strokeColor:b.focusCurveColor,strokeWidth:3}),b.drawSolid=0==this.baseObj.getAttribute("dash"),"solidButton"in b&&b.solidButton.prop("disabled",b.drawSolid),"dashedButton"in b&&b.dashedButton.prop("disabled",!b.drawSolid)},a.prototype.update=function(){},a.prototype.fillCmp=function(t){return 1},a.prototype.remove=function(){Object.values(this.definingPts).forEach(function(t){b.board.removeObject(t)}),b.board.removeObject(this.baseObj)},a.prototype.setSolid=function(t){this.baseObj.setAttribute({dash:t?0:2})},a.prototype.stringify=function(){return""},a.prototype.id=function(){return this.baseObj.id},a.prototype.on=function(t,o,e){this.baseObj.on(t,o,e)},a.prototype.off=function(t,o){this.baseObj.off(t,o)},a.prototype.onResize=function(){},a.prototype.updateTextCoords=function(o){return!Object.keys(this.definingPts).every(function(t){return!this[t].hasPoint(o.scrCoords[1],o.scrCoords[2])||(b.setTextCoords(this[t].X(),this[t].Y()),!1)},this.definingPts)},a.restore=function(t){var o=t.match(/^(.*?),(.*)/);if(o.length<3)return!1;var e=!1;return Object.keys(b.graphObjectTypes).every(function(t){return o[1]!=b.graphObjectTypes[t].strId||(e=b.graphObjectTypes[t].restore(o[2]),!1)}),!1!==e&&e.blur(),e},d.prototype=Object.create(a.prototype),Object.defineProperty(d.prototype,"constructor",{value:d,enumerable:!1,writable:!0}),d.prototype.stringify=function(){return[d.strId,0==this.baseObj.getAttribute("dash")?"solid":"dashed","("+b.snapRound(this.definingPts.point1.X(),b.snapSizeX)+","+b.snapRound(this.definingPts.point1.Y(),b.snapSizeY)+")","("+b.snapRound(this.definingPts.point2.X(),b.snapSizeX)+","+b.snapRound(this.definingPts.point2.Y(),b.snapSizeY)+")"].join(",")},d.prototype.fillCmp=function(t){return b.sign(JXG.Math.innerProduct(t,this.baseObj.stdform))},d.strId="line",d.restore=function(t){for(var o,e=[];o=b.pointRegexp.exec(t);)e.push(o.slice(1,3));if(e.length<2)return!1;var i=b.createPoint(parseFloat(e[0][0]),parseFloat(e[0][1])),r=b.createPoint(parseFloat(e[1][0]),parseFloat(e[1][1]),i);return new b.graphObjectTypes.line(i,r,/solid/.test(t),b.curveColor)},l.prototype=Object.create(a.prototype),Object.defineProperty(l.prototype,"constructor",{value:l,enumerable:!1,writable:!0}),l.prototype.stringify=function(){return[l.strId,0==this.baseObj.getAttribute("dash")?"solid":"dashed","("+b.snapRound(this.definingPts.center.X(),b.snapSizeX)+","+b.snapRound(this.definingPts.center.Y(),b.snapSizeY)+")","("+b.snapRound(this.definingPts.point.X(),b.snapSizeX)+","+b.snapRound(this.definingPts.point.Y(),b.snapSizeY)+")"].join(",")},l.prototype.fillCmp=function(t){return b.sign(this.baseObj.stdform[3]*(t[1]*t[1]+t[2]*t[2])+JXG.Math.innerProduct(t,this.baseObj.stdform))},l.strId="circle",l.restore=function(t){for(var o,e=[];o=b.pointRegexp.exec(t);)e.push(o.slice(1,3));if(e.length<2)return!1;var i=b.createPoint(parseFloat(e[0][0]),parseFloat(e[0][1])),r=b.createPoint(parseFloat(e[1][0]),parseFloat(e[1][1]),i);return new b.graphObjectTypes.circle(i,r,/solid/.test(t),b.curveColor)},u.prototype=Object.create(a.prototype),Object.defineProperty(u.prototype,"constructor",{value:u,enumerable:!1,writable:!0}),u.prototype.stringify=function(){return[u.strId,0==this.baseObj.getAttribute("dash")?"solid":"dashed",this.vertical?"vertical":"horizontal","("+b.snapRound(this.definingPts.vertex.X(),b.snapSizeX)+","+b.snapRound(this.definingPts.vertex.Y(),b.snapSizeY)+")","("+b.snapRound(this.definingPts.point.X(),b.snapSizeX)+","+b.snapRound(this.definingPts.point.Y(),b.snapSizeY)+")"].join(",")},u.prototype.fillCmp=function(t){return this.vertical?b.sign(t[2]-this.baseObj.Y(t[1])):b.sign(t[1]-this.baseObj.X(t[2]))},u.strId="parabola",u.restore=function(t){for(var o,e=[];o=b.pointRegexp.exec(t);)e.push(o.slice(1,3));if(e.length<2)return!1;var i=b.createPoint(parseFloat(e[0][0]),parseFloat(e[0][1])),r=b.createPoint(parseFloat(e[1][0]),parseFloat(e[1][1]),i,!0);return new b.graphObjectTypes.parabola(i,r,/vertical/.test(t),/solid/.test(t),b.curveColor)},h.prototype=Object.create(a.prototype),Object.defineProperty(h.prototype,"constructor",{value:h,enumerable:!1,writable:!0}),h.prototype.blur=function(){this.focused=!1,this.definingPts.icon.setAttribute({fixed:!0})},h.prototype.focus=function(){this.focused=!0,this.definingPts.icon.setAttribute({fixed:!1})},h.prototype.on=function(t,o,e){this.definingPts.icon.on(t,o,e)},h.prototype.off=function(t,o){this.definingPts.icon.off(t,o)},h.prototype.remove=function(){"fillObj"in this&&b.board.removeObject(this.fillObj),a.prototype.remove.call(this)},h.prototype.update=function(){if(!this.isStatic){this.updateTimeout&&clearTimeout(this.updateTimeout);var h=this;this.updateTimeout=setTimeout(function(){h.updateTimeout=0,"fillObj"in h&&(b.board.removeObject(h.fillObj),delete h.fillObj);for(var t=h.definingPts.point.coords.usrCoords,r=b.graphedObjs.concat(b.staticObjs),n=Array(r.length),o=0;o
");b.solidButton=$("").on("click",{solid:!0},S),o.append(b.solidButton),b.dashedButton=$("").on("click",{solid:!1},S),o.append(b.dashedButton),t.append(o)}};var n=$("
");if(e.append(n),!b.isStatic){var s=$("
");b.selectTool=new g(s),"customTools"in t&&Object.keys(t.customTools).forEach(function(t){var o,e=this[t],i="parent"in e?e.parent?b.toolTypes[e.parent]:null:f;i?((o=function(t){i.call(this,t,e.iconName,e.tooltip),"initialize"in e&&e.initialize.call(this,b,t)}).prototype=Object.create(i.prototype),Object.defineProperty(o.prototype,"constructor",{value:o,enumerable:!1,writable:!0})):o=function(){e.initialize.call(this,b,container)},"activate"in e&&(o.prototype.activate=function(){i.prototype.activate.call(this),e.activate.call(this,b)}),"deactivate"in e&&(o.prototype.deactivate=function(){e.deactivate.call(this,b),i.prototype.deactivate.call(this)}),"updateHighlights"in e&&(o.prototype.updateHighlights=function(t){return e.updateHighlights.call(this,b,t)}),"removeHighlights"in e&&(o.prototype.removeHighlights=function(){e.removeHighlights.call(this,b),i.prototype.removeHighlights.call(this)}),"helperMethods"in e&&Object.keys(e.helperMethods).forEach(function(t){o[t]=function(){return e.helperMethods[t].apply(this,[b].concat(Array.prototype.slice.call(arguments)))}}),b.toolTypes[t]=o},t.customTools),i.forEach(function(t){t in b.toolTypes?new b.toolTypes[t](s):console.log("Unknown tool: "+t)}),s.append($("").on("click",function(){if(this.blur(),b.selectedObj){var o=$('');o.modal("show"),$(".modal-backdrop").css("opacity","0.2"),o.on("hidden",function(){o.remove()}),$("#gt-confirm-delete").on("click",function(){for(var t=0;tClear").on("click",function(){if(this.blur(),0!=b.graphedObjs.length){var t=$('');t.modal("show"),$(".modal-backdrop").css("opacity","0.2"),t.on("hidden",function(){t.remove()}),$("#gt-confirm-clear").on("click",function(){b.graphedObjs.forEach(function(t){t.remove()}),b.graphedObjs=[],b.selectedObj=null,b.selectTool.activate(),b.html_input.value="",t.modal("hide")})}})),e.append(s),$('.gt-button[data-toggle="tooltip"]').tooltip({trigger:"hover",placement:"bottom",delay:{show:1e3,hide:0}})}!function(){b.board=JXG.JSXGraph.initBoard(o+"_graph",r),b.board.suspendUpdate();var t=b.board.getBoundingBox();b.board.defaultAxes.x.point1.setPosition(JXG.COORDS_BY_USER,[t[0],0]),b.board.defaultAxes.x.point2.setPosition(JXG.COORDS_BY_USER,[t[2],0]),b.board.defaultAxes.y.point1.setPosition(JXG.COORDS_BY_USER,[0,t[3]]),b.board.defaultAxes.y.point2.setPosition(JXG.COORDS_BY_USER,[0,t[1]]),b.board.create("text",[function(){return b.board.getBoundingBox()[2]-3/b.board.unitX},function(){return 1.5/b.board.unitY},function(){return"\\(x\\)"}],{anchorX:"right",anchorY:"bottom",highlight:!1,color:"black",fixed:!0,useMathJax:!0}),b.board.create("text",[function(){return 4.5/b.board.unitX},function(){return b.board.getBoundingBox()[1]+2.5/b.board.unitY},function(){return"\\(y\\)"}],{anchorX:"left",anchorY:"top",highlight:!1,color:"black",fixed:!0,useMathJax:!0}),b.current_pos_text=b.board.create("text",[function(){return b.board.getBoundingBox()[2]-5/b.board.unitX},function(){return b.board.getBoundingBox()[3]+5/b.board.unitY},""],{anchorX:"right",anchorY:"bottom",fixed:!0}),b.board.highlightInfobox=function(t,o,e){return b.board.highlightCustomInfobox("",e)},b.isStatic||(b.board.on("move",function(t){var o=b.getMouseCoords(t);b.activeTool.updateHighlights(o)||b.selectedObj&&b.selectedObj.updateTextCoords(o)||b.setTextCoords(o.usrCoords[1],o.usrCoords[2])}),$(document).on("keydown.ToolDeactivate",function(t){"Escape"===t.key&&b.selectTool.activate()})),$(window).resize(function(t){b.board.canvasWidth==n.width()&&b.board.canvasHeight==n.height()||(b.board.resizeContainer(n.width(),n.height(),!0),b.graphedObjs.forEach(function(t){t.onResize()}),b.staticObjs.forEach(function(t){t.onResize()}))}),b.drawSolid=!0,b.graphedObjs=[],b.staticObjs=[],b.selectedObj=null,b.board.unsuspendUpdate()}(),"html_input"in b&&x(b.html_input.value,!1),"staticObjects"in t&&"string"==typeof t.staticObjects&&t.staticObjects.length&&x(t.staticObjects,!0),b.isStatic||(b.updateText(),b.activeTool=b.selectTool,b.activeTool.activate(!0))}else setTimeout(function(){graphTool(o,t)},100)}function a(t){this.baseObj=t,(this.baseObj.gtGraphObject=this).definingPts={}}function d(t,o,e,i){a.call(this,b.board.create("line",[t,o],{fixed:!0,highlight:!1,strokeColor:i||b.underConstructionColor,dash:e?0:2})),this.definingPts.point1=t,this.definingPts.point2=o}function l(t,o,e,i){a.call(this,b.board.create("circle",[t,o],{fixed:!0,highlight:!1,strokeColor:i||b.underConstructionColor,dash:e?0:2})),this.definingPts.center=t,this.definingPts.point=o}function p(t,o,e){return e?(o.Y()-t.Y())/Math.pow(o.X()-t.X(),2):(o.X()-t.X())/Math.pow(o.Y()-t.Y(),2)}function c(o,e,i,t,r){return i?b.board.create("curve",[function(t){return t},function(t){return p(o,e,i)*Math.pow(t-o.X(),2)+o.Y()},function(){return b.board.getBoundingBox()[0]},function(){return b.board.getBoundingBox()[2]}],{strokeWidth:2,highlight:!1,strokeColor:r||b.underConstructionColor,dash:t?0:2}):b.board.create("curve",[function(t){return p(o,e,i)*Math.pow(t-o.Y(),2)+o.X()},function(t){return t},function(){return b.board.getBoundingBox()[3]},function(){return b.board.getBoundingBox()[1]}],{strokeWidth:2,highlight:!1,strokeColor:r||b.underConstructionColor,dash:t?0:2})}function u(t,o,e,i,r){a.call(this,c(t,o,e,i,r)),this.definingPts.vertex=t,this.definingPts.point=o,this.vertical=e}function h(t){t.setAttribute({visible:!1}),a.call(this,t),this.focused=!0,this.definingPts.point=t,this.updateTimeout=0,this.update();var r=this;this.definingPts.icon=b.board.create("image",[function(){return r.focused?b.fillIconFocused:b.fillIcon},[t.X()-12/b.board.unitX,t.Y()-12/b.board.unitY],[function(){return 24/b.board.unitX},function(){return 24/b.board.unitY}]],{withLabel:!1,highlight:!1,layer:9,name:"FillIcon"}),(this.definingPts.icon.gtGraphObject=this).definingPts.icon.point=t,this.isStatic=b.isStatic,b.isStatic||(this.on("down",function(){b.board.containerObj.style.cursor="none"}),this.on("up",function(){b.board.containerObj.style.cursor="auto"}),this.on("drag",function(t){var o=b.getMouseCoords(t),e=b.snapRound(o.usrCoords[1],b.snapSizeX),i=b.snapRound(o.usrCoords[2],b.snapSizeY);this.setPosition(JXG.COORDS_BY_USER,[e-12/b.board.unitX,i-12/b.board.unitY]),this.point.setPosition(JXG.COORDS_BY_USER,[e,i]),r.update(),b.updateText()}))}function f(t,o,e){this.button=$("");var i=this;this.button.on("click",function(){i.activate()}),t.append(this.button),this.hlObjs={}}function g(t){f.call(this,t,"select","Object Selection Tool")}function y(t,o,e){f.call(this,t,o||"line",e||"Line Tool")}function O(t,o,e){f.call(this,t,o||"circle",e||"Circle Tool")}function v(t,o,e,i){f.call(this,t,e||(o?"vertical-parabola":"horizontal-parabola"),i||(o?"Vertical Parabola Tool":"Horizontal Parabola Tool")),this.vertical=o}function j(t,o,e){v.call(this,t,!0,o,e)}function C(t,o,e){v.call(this,t,!1,o,e)}function m(t,o,e){f.call(this,t,o||"fill",e||"Region Shading Tool")}function S(t){this.blur(),"solidButton"in b&&b.solidButton.prop("disabled",t.data.solid),"dashedButton"in b&&b.dashedButton.prop("disabled",!t.data.solid),b.selectedObj&&(b.selectedObj.setSolid(t.data.solid),b.updateText()),b.drawSolid=t.data.solid,b.activeTool.updateHighlights()}function x(t,o){b.board.suspendUpdate();var e=b.isStatic;b.isStatic=o;for(var i,r=/{(.*?)}/g;i=r.exec(t);){var n=a.restore(i[1]);!1!==n&&(o?b.staticObjs.push(n):b.graphedObjs.push(n))}b.isStatic=e,b.updateObjects(),b.board.unsuspendUpdate()}}Object.values||(Object.values=function(o){return Object.keys(o).map(function(t){return o[t]})}); diff --git a/htdocs/js/apps/GraphTool/images/CircleTool.svg b/htdocs/js/apps/GraphTool/images/CircleTool.svg new file mode 100644 index 0000000000..c8a97a940b --- /dev/null +++ b/htdocs/js/apps/GraphTool/images/CircleTool.svg @@ -0,0 +1,75 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/htdocs/js/apps/GraphTool/images/DashTool.svg b/htdocs/js/apps/GraphTool/images/DashTool.svg new file mode 100644 index 0000000000..aece11cfb5 --- /dev/null +++ b/htdocs/js/apps/GraphTool/images/DashTool.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/htdocs/js/apps/GraphTool/images/FillTool.svg b/htdocs/js/apps/GraphTool/images/FillTool.svg new file mode 100644 index 0000000000..776ae9f83a --- /dev/null +++ b/htdocs/js/apps/GraphTool/images/FillTool.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/htdocs/js/apps/GraphTool/images/HorizontalParabolaTool.svg b/htdocs/js/apps/GraphTool/images/HorizontalParabolaTool.svg new file mode 100644 index 0000000000..918863625d --- /dev/null +++ b/htdocs/js/apps/GraphTool/images/HorizontalParabolaTool.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/htdocs/js/apps/GraphTool/images/LineTool.svg b/htdocs/js/apps/GraphTool/images/LineTool.svg new file mode 100644 index 0000000000..814c8288f7 --- /dev/null +++ b/htdocs/js/apps/GraphTool/images/LineTool.svg @@ -0,0 +1,80 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/htdocs/js/apps/GraphTool/images/SelectTool.svg b/htdocs/js/apps/GraphTool/images/SelectTool.svg new file mode 100644 index 0000000000..dbbd2e5b86 --- /dev/null +++ b/htdocs/js/apps/GraphTool/images/SelectTool.svg @@ -0,0 +1,68 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/htdocs/js/apps/GraphTool/images/SolidTool.svg b/htdocs/js/apps/GraphTool/images/SolidTool.svg new file mode 100644 index 0000000000..33a8a8643b --- /dev/null +++ b/htdocs/js/apps/GraphTool/images/SolidTool.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/htdocs/js/apps/GraphTool/images/VerticalParabolaTool.svg b/htdocs/js/apps/GraphTool/images/VerticalParabolaTool.svg new file mode 100644 index 0000000000..e023d95a1f --- /dev/null +++ b/htdocs/js/apps/GraphTool/images/VerticalParabolaTool.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/htdocs/js/apps/ImageView/imageview.css b/htdocs/js/apps/ImageView/imageview.css new file mode 100644 index 0000000000..2d57d1c0db --- /dev/null +++ b/htdocs/js/apps/ImageView/imageview.css @@ -0,0 +1,61 @@ +.image-view-elt:hover { cursor: pointer; } + +.image-view-dialog.modal { padding: 0 !important; } + +.image-view-dialog .modal-body { + overflow: auto; + padding: 8px; + text-align: center; + box-sizing: content-box !important; + max-height: none; +} + +.image-view-dialog .modal-header { + padding: 0.1rem 0.5rem; +} + +.image-view-dialog .modal-header .drag-handle { + display: inline-block; + cursor: pointer; + width: calc(100% - 72px); + height: 100%; + touch-action: none; +} + +.image-view-dialog .modal-header .btn { + padding: 0 0.2rem; + margin: 0 0.25rem 0 0; + background: transparent; + border: 0; +} + +.image-view-dialog .modal-header .btn svg { + margin-top: 4px; + margin-bottom: -4px; + opacity: 0.5; +} + +.image-view-dialog .modal-header .close { + padding: 0 0.2rem; + margin: 0 -0.2rem 0 auto; + opacity: 0.5; + font-size: 1.5rem; + font-weight: 700; +} + +.image-view-dialog .modal-header .btn svg:hover { + opacity: 1; +} + +.image-view-dialog .modal-header .close:hover { + opacity: 0.75; +} + +.image-view-dialog .modal-body img { + max-width: 100%; + height: 100%; +} + +.image-view-dialog .modal-body svg { + overflow: visible; +} diff --git a/htdocs/js/apps/ImageView/imageview.js b/htdocs/js/apps/ImageView/imageview.js new file mode 100644 index 0000000000..5d110c82db --- /dev/null +++ b/htdocs/js/apps/ImageView/imageview.js @@ -0,0 +1,234 @@ +"use strict"; + +(function() { + function imageViewDialog() { + var elt = $(this); + var img = this.cloneNode(true); + var imgType = img.tagName.toLowerCase(); + img.classList.remove('image-view-elt'); + img.removeAttribute('tabindex'); + img.removeAttribute('role'); + img.removeAttribute('width'); + img.removeAttribute('height'); + img.removeAttribute('style'); + + var imgHtml = img.outerHTML; + if (imgType == 'svg') { + var ids = imgHtml.match(/\bid="[^"]*"/g); + if (ids) { + // Sort the ids from longest to shortest. + ids.sort(function(a, b) { return b.length - a.length; }); + ids.forEach(function(id) { + var idString = id.replace(/id="(.*)"/, "$1"); + imgHtml = imgHtml.replaceAll(idString, "viewDialog" + idString); + }); + } + } + + var modal = $('' + ); + modal.css('margin', '0px'); + + var body = modal.find('.modal-body'); + var header = modal.find('.modal-header'); + var dragHandle = header.find('.drag-handle'); + var zoomIn = header.find('.zoom-in'); + var zoomOut = header.find('.zoom-out'); + + modal.on('shown', function () { + // Find the natural dimensions of the image. + var naturalWidth, naturalHeight; + if (imgType == 'img') { + naturalWidth = elt.prop('naturalWidth'); + naturalHeight = elt.prop('naturalHeight'); + } else if (imgType == 'svg') { + var svg = modal.find('.modal-body svg'); + var viewBoxDims = svg.prop('viewBox').baseVal; + // This assumes the units of the view box dimensions are points. + naturalWidth = viewBoxDims.width * 4 / 3; + naturalHeight = viewBoxDims.height * 4 / 3; + } + + var headerHeight = header.outerHeight(); + + // Initial image maximum width and height + var maxWidth = window.innerWidth - 18; + var maxHeight = window.innerHeight - headerHeight - 18; + + // Dialog maximum width and height + modal.css({ + 'max-width': maxWidth + 16, + 'max-height': maxHeight + headerHeight + 16 + }); + + // Initial image width and height. + var width = naturalWidth; + var height = naturalHeight; + + // Dialog position + var left; + var top; + + function repositionModal(x, y) { + if (x < 0 || width >= maxWidth) left = 0; + else if (x + width > maxWidth) left = maxWidth - width; + else left = x; + if (y < 0 || height >= maxHeight) top = 0; + else if (y + height > maxHeight) top = maxHeight - height; + else top = y; + + modal.css({ 'left': left + 'px', 'top': top + 'px' }); + } + + // Resize the modal. Care is taken to maintain the aspect ratio. + function zoom(factor, initial) { + // Save the current dimensions for repositioning later. + var initialWidth = width; + var initialHeight = height; + + // Determine the width and height after applying the zoom factor. + if (factor * width > maxWidth || factor * height > maxHeight) { + width = maxWidth; + height = width * naturalHeight / naturalWidth; + if (height > maxHeight) { + height = maxHeight; + width = height * naturalWidth / naturalHeight; + } + } else if (factor * width < 100 || factor * height < 100) { + width = 100; + height = width * naturalHeight / naturalWidth; + if (height < 100) { + height = 100; + width = height * naturalWidth / naturalHeight; + } + } else { + width = factor * width; + height = factor * height; + } + + // Resize the modal + body.css({ width: width + "px", height: height + "px" }); + modal.css({ width: (width + 16) + "px", height: (height + headerHeight + 16) + "px" }); + + // Re-position the modal. + if (initial) { + // Center the modal initially + repositionModal((maxWidth - width) / 2, (maxHeight - height) / 2); + } else { + repositionModal(left - (width - initialWidth) / 2, top - (height - initialHeight) / 2); + } + + modal.focus(); + }; + + // Make the dialog draggable + dragHandle.on('pointerdown', function(e) { + e.preventDefault(); + + // Save the position of the pointer event relative to the top left corner of the dialog. + var pointerPosX = e.originalEvent.offsetX + dragHandle[0].offsetLeft; + var pointerPosY = e.originalEvent.offsetY + dragHandle[0].offsetTop; + + dragHandle.on('pointermove.ImageViewDrag', function(e) { + e.preventDefault(); + repositionModal(e.originalEvent.clientX - pointerPosX, e.originalEvent.clientY - pointerPosY); + }); + dragHandle[0].setPointerCapture(e.originalEvent.pointerId); + }); + + dragHandle.on('lostpointercapture', function(e) { + e.preventDefault(); + dragHandle.off('pointermove.ImageViewDrag'); + }); + + // Set up the zoom in and zoom out click handlers. + zoomIn.click(function(e) { this.blur(); zoom(1.25); }); + zoomOut.click(function(e) { this.blur(); zoom(0.8); }); + + $(window).on('resize.ImageView', function(e) { + maxWidth = window.innerWidth - 18; + maxHeight = window.innerHeight - headerHeight - 18; + modal.css({ 'max-width': maxWidth + 16, 'max-height': maxHeight + headerHeight + 16 }); + zoom(1); + }); + + // The + or = key zooms in and the - key zooms out. + modal.on('keydown', function(e) { + if (e.key === '=' || e.key === '+') zoom(1.25); + if (e.key === '-') zoom(0.8); + + // ctrl+0 resets to the natural width and height + if (e.ctrlKey && e.key === '0') { + width = naturalWidth; + height = naturalHeight; + zoom(1); + } + }); + + // The mouse wheel zooms in and out also. + modal.on('wheel', function(e) { + e.preventDefault(); + if (e.originalEvent.deltaY < 0) zoom(1.25); + if (e.originalEvent.deltaY > 0) zoom(0.8); + }); + + // Perform the initial zoom + zoom(1, true); + + // Make the backdrop a little lighter + $('.modal-backdrop').css('opacity', '0.2'); + }); + modal.on('hidden', function() { + modal.remove(); + $(window).off("resize.ImageView"); + elt.focus(); + }) + modal.modal('show'); + } + + function keyHandler(e) { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + imageViewDialog.call(this); + } + } + + $(function() { + // Set up images that are already in the page. + $('.image-view-elt').on('click.ImageView', imageViewDialog).on('keydown.ImageView', keyHandler); + + // Deal with images that are added to the page later. + var observer = new MutationObserver(function(mutationsList, observer) { + mutationsList.forEach(function(mutation) { + $(mutation.addedNodes).each(function() { + if (this.classList && this.classList.contains('image-view-elt')) { + $(this).off('click.ImageView').on('click.ImageView', imageViewDialog) + .off('keydown.ImageView').on('keydown.ImageView', keyHandler); + } else { + $(this).find('.image-view-elt') + .off('click.ImageView').on('click.ImageView', imageViewDialog) + .off('keydown.ImageView').on('keydown.ImageView', keyHandler); + } + }); + }); + }); + observer.observe($('body')[0], { childList: true, subtree: true }); + + // Stop the mutation observer when the window is closed. + window.addEventListener('unload', function() { observer.disconnect(); }); + }); +})(); diff --git a/htdocs/js/apps/InputColor/color.js b/htdocs/js/apps/InputColor/color.js new file mode 100644 index 0000000000..42bf703518 --- /dev/null +++ b/htdocs/js/apps/InputColor/color.js @@ -0,0 +1,36 @@ +/* + * color.js + * + * for coloring the input elements with the proper color based on whether they are correct or incorrect + * + * Originally by ghe3 + * Edited by dpvc 2014-08 + */ + +function color_inputs(correct,incorrect) { + var className = {}; + var i, m, inputs, input, name; + var addClass = function (input,name) { + if (input) { + if (input.className == "") {input.className = name} else {input.className += " "+name} + } + }; + + for (i = 0, m = correct.length; i < m; i++) { + addClass(document.getElementById(correct[i]),"correct"); + className[correct[i]] = "correct"; + } + for (i = 0, m = incorrect.length; i < m; i++) { + addClass(document.getElementById(incorrect[i]),"incorrect"); + className[incorrect[i]] = "incorrect"; + } + + inputs = document.getElementsByTagName("input"); + for (i = 0, m = inputs.length; i < m; i++) { + input = inputs[i]; + if (!input.hidden && input.name === input.id) { + name = input.name.replace(/^(MaTrIx_MuLtIaNsWeR|MaTrIx|MuLtIaNsWeR)_/,"").replace(/(_\d+)+$/,""); + if (name !== input.name && className[name]) addClass(input,className[name]); + } + } +} diff --git a/htdocs/js/apps/Knowl/knowl.js b/htdocs/js/apps/Knowl/knowl.js new file mode 100644 index 0000000000..9c87909a83 --- /dev/null +++ b/htdocs/js/apps/Knowl/knowl.js @@ -0,0 +1,149 @@ +/* + * Knowl - Feature Demo for Knowls + * Copyright (C) 2011 Harald Schilly + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * 4/11/2012 Modified by David Guichard to allow inline knowl code. + * Sample use: + * This is an inline knowl. + */ + +/* javascript code for the knowl features + * global counter, used to uniquely identify each knowl-output element + * that's necessary because the same knowl could be referenced several times + * on the same page */ +var knowl_id_counter = 0; + +function knowl_click_handler($el) { + // the knowl attribute holds the id of the knowl + var knowl_id = $el.attr("knowl"); + // the uid is necessary if we want to reference the same content several times + var uid = $el.attr("knowl-uid"); + var output_id = '#knowl-output-' + uid; + var $output_id = $(output_id); + // create the element for the content, insert it after the one where the + // knowl element is included (e.g. inside a

tag) (sibling in DOM) + var idtag = "id='"+output_id.substring(1) + "'"; + var kid = "id='kuid-"+ uid + "'"; + // if we already have the content, toggle visibility + if ($output_id.length > 0) { + $("#kuid-"+uid).slideToggle("fast"); + $el.toggleClass("active"); + + // otherwise download it or get it from the cache + } else { + var knowl = "
loading '"+knowl_id+"'
"; + + // check, if the knowl is inside a td or th in a table. otherwise assume its + // properly sitting inside a
or

+ if($el.parent().is("td") || $el.parent().is("th") ) { + // assume we are in a td or th tag, go 2 levels up + var cols = $el.parent().parent().children().length; + $el.parents().eq(1).after( + // .parents().eq(1) was formerly written as .parent().parent() + ""+knowl+""); + } else if ($el.parent().is("li")) { + $el.parent().after(knowl); + // the following is implemented stupidly, but I had to do it quickly. + // someone please replace it with an appropriate loop -- DF + //also, after you close the knowl, it still has a shaded background + } else if ($el.parent().css('display') == "block") { + $el.parent().after(knowl); + } else if ($el.parent().parent().css('display') == "block") { + $el.parent().parent().after(knowl); + } else { + $el.parent().parent().parent().after(knowl); + } + +//else { +// // $el.parent().after(knowl); +// var theparents=$el.parents(); +// var ct=0; +// while (theparents[ct] != "block" && ct<2) +// ct++; +// ct=0; +// //$el.parents().eq(ct).after(knowl); +// $el.parents().eq(ct).after(theparents[1]); +// } + + // "select" where the output is and get a hold of it + var $output = $(output_id); + var $knowl = $("#kuid-"+uid); + $output.addClass("loading"); + $knowl.hide(); + // DRG: inline code + if ($el.attr("class") == 'internal') { + if ($el.attr("base64") == 1 ){ + $output.html(Base64.decode( $el.attr("value") )); + } else { + $output.html( $el.attr("value") ); + } + //console.log("here" +Base64.decode( $el.attr("value") )); + $knowl.hide(); + $el.addClass("active"); + if(window.MathJax == undefined) { + $knowl.slideDown("slow"); + } else { + MathJax.startup.promise = MathJax.startup.promise.then(function() { return MathJax.typesetPromise([$output[0]]); }); + MathJax.startup.promise.then(function () {$knowl.slideDown("slow")}); + } + } else { + // Get code from server. + $output.load(knowl_id, + function(response, status, xhr) { + $knowl.removeClass("loading"); + if (status == "error") { + $el.removeClass("active"); + $output.html("

ERROR: " + xhr.status + " " + xhr.statusText + '
'); + $output.show(); + } else if (status == "timeout") { + $el.removeClass("active"); + $output.html("
ERROR: timeout. " + xhr.status + " " + xhr.statusText + '
'); + $output.show(); + } else { + $knowl.hide(); + $el.addClass("active"); + } + // if we are using MathJax, then we reveal the knowl after it has finished rendering the contents + if(window.MathJax == undefined) { + $knowl.slideDown("slow"); + } else { + MathJax.startup.promise = MathJax.startup.promise.then(function() { return MathJax.typesetPromise([$output[0]]); }); + MathJax.startup.promise.then(function () {$knowl.slideDown("slow")}); + } + }); + } + } +} //~~ end click handler for *[knowl] elements + +/** register a click handler for each element with the knowl attribute + * @see jquery's doc about 'live'! the handler function does the + * download/show/hide magic. also add a unique ID, + * necessary when the same reference is used several times. */ +$(function() { + // $("*[knowl]").live({ + $("body").on("click", "*[knowl]", function(evt) { +// click: function(evt) { + evt.preventDefault(); + var $knowl = $(this); + if(!$knowl.attr("knowl-uid")) { + $knowl.attr("knowl-uid", knowl_id_counter); + knowl_id_counter++; + } + knowl_click_handler($knowl, evt); +// } + }); +}); diff --git a/htdocs/js/apps/Knowl/knowlstyle.css b/htdocs/js/apps/Knowl/knowlstyle.css new file mode 100644 index 0000000000..1b59cfd85e --- /dev/null +++ b/htdocs/js/apps/Knowl/knowlstyle.css @@ -0,0 +1,108 @@ +/** page wide defs for knowls, the *[knowl] is a selector for + * all elements, that have a knowl attribute set to any value */ +.knowl-content { + padding: 10px 10px 0 10px; + border-bottom-left-radius: 10px; + -moz-border-radius-bottomleft: 10px; +} +.knowl-content h1 { + margin: 0px 0px 10px 0px; +} +.knowl-content h2 { + margin: 0px 0px 5px 0px; +} + +/* next defninition is needed to over-ride the default, which is the + default font size, which is 16pt */ +.knowl p { + margin-bottom: 0; + margin-top: 10px; +} + + +*[knowl] { + display: inline; + border-bottom: 2px dotted #00a; + color: #00a; + cursor: pointer; + border-radius: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + margin: 3px 0 0 0; +} +*[knowl]:hover, +*[knowl].active { + border-bottom: 2px solid #aaf; + background: #ddf; + color: #006; + padding: 0; + margin: 0 0 0 0; + border-top-left-radius: 3px; + -moz-border-radius-topleft: 3px; + border-top-right-radius: 3px; + -moz-border-radius-topright: 3px; +} + + +div > *[knowl], p > *[knowl] { + position: relative; +} + +.knowl-error { + color: red; + border-bottom: 0; +} +.knowl-output { + background: #eef; + border-left: 10px solid #ddf; + border-right: 10px solid #ddf; + border-bottom: 10px solid #ddf; + border-bottom-left-radius: 10px; + -moz-border-radius-bottomleft: 10px; + border-bottom-right-radius: 10px; + -moz-border-radius-bottomright: 10px; + display: none; + padding: 0px; + margin-top: 10px; + margin-bottom: 0px; + margin-right: 0px; +} +.knowl-output .knowl-output, +.knowl-output .knowl-output.loading { + margin-left: 0; + margin-right: 0; +} +.knowl-output.loading { + color: grey; + font-style: italic; + font-size: small; +} +.knowl-output h1, .knowl-output h2 { + margin: 5px 0; +} +.knowl-output h1 { + color: #006; +} +.knowl-output h2 { + color: #006; +} +.knowl-output a { display: inline; } + +.knowl-footer { + position: relative; + bottom: -10px; + font-size: x-small; + background: #ddf; + color: grey; + padding: 1px 0 1px 10px; + margin: 0 0 0 0; +} +.knowl-footer a { + color: #006; +} +.knowl-footer a:hover { + background: none; + color: #88f; +} + +/* end knowl */ diff --git a/htdocs/js/apps/MathQuill/fonts/Symbola-basic.eot b/htdocs/js/apps/MathQuill/fonts/Symbola-basic.eot new file mode 100644 index 0000000000000000000000000000000000000000..2e39ec7bbf5d72daac8ea2fe02325ec92ae0b19c GIT binary patch literal 6828 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 HzmES0(+{lB@~cQ5u`+EX%KE&Is~L{ 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 Ri65ox9bV5n79v~;{0DWHmfQdU literal 0 HcmV?d00001 diff --git a/htdocs/js/apps/MathQuill/fonts/Symbola-basic.woff2 b/htdocs/js/apps/MathQuill/fonts/Symbola-basic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..7c0d6c8cf6a0e08c4d496b1abbd5467c9ab62932 GIT binary patch literal 3612 zcmV+%4&(86Pew8T0RR9101g}g4FCWD02%B601d_f0RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/htdocs/js/apps/MathQuill/fonts/Symbola.eot b/htdocs/js/apps/MathQuill/fonts/Symbola.eot new file mode 100644 index 0000000000000000000000000000000000000000..a78eb196e8ce03db7fd676cc7ce3a6435f4a2743 GIT binary patch literal 403238 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/htdocs/js/apps/MathQuill/fonts/Symbola.ttf b/htdocs/js/apps/MathQuill/fonts/Symbola.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ea6a8bba9b833912cbee65e2165241609ef7c548 GIT binary patch literal 403144 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%0Av(t<`1#Qg0Q84CM#}KZA|h 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