diff --git a/lib/RenderApp.pm b/lib/RenderApp.pm
index a8f64d73d..5deedf83b 100644
--- a/lib/RenderApp.pm
+++ b/lib/RenderApp.pm
@@ -1,3 +1,8 @@
+use strict;
+use warnings;
+# use feature 'signatures';
+# no warnings qw(experimental::signatures);
+
package RenderApp;
use Mojo::Base 'Mojolicious';
@@ -15,7 +20,7 @@ BEGIN {
$ENV{PG_ROOT} = $main::dirname . '/PG';
# Used for reconstructing library paths from sym-links.
- $ENV{OPL_DIRECTORY} = "webwork-open-problem-library";
+ $ENV{OPL_DIRECTORY} = "$ENV{RENDER_ROOT}/webwork-open-problem-library";
$ENV{MOJO_CONFIG} = (-r "$ENV{RENDER_ROOT}/render_app.conf") ? "$ENV{RENDER_ROOT}/render_app.conf" : "$ENV{RENDER_ROOT}/render_app.conf.dist";
# $ENV{MOJO_MODE} = 'production';
@@ -26,8 +31,9 @@ use lib "$main::dirname";
print "home directory " . $main::dirname . "\n";
use RenderApp::Model::Problem;
-use RenderApp::Controller::RenderProblem;
use RenderApp::Controller::IO;
+use WeBWorK::RenderProblem;
+use WeBWorK::FormatRenderedProblem;
sub startup {
my $self = shift;
@@ -66,6 +72,7 @@ sub startup {
$self->helper(newProblem => sub { shift; RenderApp::Model::Problem->new(@_) });
# Helpers
+ $self->helper(format => sub { WeBWorK::FormatRenderedProblem::formatRenderedProblem(@_) });
$self->helper(validateRequest => sub { RenderApp::Controller::IO::validate(@_) });
$self->helper(parseRequest => sub { RenderApp::Controller::Render::parseRequest(@_) });
$self->helper(croak => sub { RenderApp::Controller::Render::croak(@_) });
@@ -107,20 +114,7 @@ sub startup {
$r->any('/pg_files/CAPA_Graphics/*static')->to('StaticFiles#CAPA_graphics_file');
$r->any('/pg_files/tmp/*static')->to('StaticFiles#temp_file');
$r->any('/pg_files/*static')->to('StaticFiles#pg_file');
- $r->any('/*fail')->to('StaticFiles#public_file');
- # # any other requests fall through
- # $r->any('/*fail' => sub {
- # my $c = shift;
- # my $report = $c->stash('fail')."\nCOOKIE:";
- # for my $cookie (@{$c->req->cookies}) {
- # $report .= "\n".$cookie->to_string;
- # }
- # $report .= "\nFORM DATA:";
- # foreach my $k (@{$c->req->params->names}) {
- # $report .= "\n$k = ".join ', ', @{$c->req->params->every_param($k)};
- # }
- # $c->log->fatal($report);
- # $c->rendered(404)});
+ $r->any('/*static')->to('StaticFiles#public_file');
}
1;
diff --git a/lib/RenderApp/Controller/FormatRenderedProblem.pm b/lib/RenderApp/Controller/FormatRenderedProblem.pm
deleted file mode 100755
index 8ca271591..000000000
--- a/lib/RenderApp/Controller/FormatRenderedProblem.pm
+++ /dev/null
@@ -1,357 +0,0 @@
-#!/usr/bin/perl -w
-
-################################################################################
-# WeBWorK Online Homework Delivery System
-# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: webwork2/lib/WebworkClient.pm,v 1.1 2010/06/08 11:46:38 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.
-################################################################################
-
-=head1 NAME
-
-FormatRenderedProblem.pm
-
-=cut
-
-package RenderApp::Controller::FormatRenderedProblem;
-
-use warnings;
-use strict;
-
-use lib "$ENV{PG_ROOT}/lib";
-
-use MIME::Base64 qw( encode_base64 decode_base64);
-use WeBWorK::Utils::AttemptsTable; #import from ww2
-use WeBWorK::Utils::LanguageAndDirection;
-use WeBWorK::Utils qw(wwRound getAssetURL); # required for score summary
-use WeBWorK::Localize ; # for maketext
-our $UNIT_TESTS_ON = 0;
-
-#####################
-# error formatting
-
-sub format_hash_ref {
- my $hash = shift;
- warn "Use a hash reference" unless ref($hash) =~/HASH/;
- return join(" ", map {$_="--" unless defined($_);$_ } %$hash),"\n";
-}
-
-sub new {
- my $invocant = shift;
- my $class = ref $invocant || $invocant;
- my $self = { # Is this function redundant given the declarations within sub formatRenderedProblem?
- return_object => {},
- encoded_source => {},
- sourceFilePath => '',
- baseURL => $ENV{baseURL},
- form_action_url =>$ENV{formURL},
- maketext => sub {return @_},
- courseID => 'foo', # optional?
- userID => 'bar', # optional?
- course_password => 'baz',
- inputs_ref => {},
- problem_seed => 6666,
- @_,
- };
- bless $self, $class;
-}
-
-sub return_object { # out
- my $self = shift;
- my $object = shift;
- $self->{return_object} = $object if defined $object and ref($object); # source is non-empty
- $self->{return_object};
-}
-
-sub encoded_source {
- my $self = shift;
- my $source = shift;
- $self->{encoded_source} =$source if defined $source and $source =~/\S/; # source is non-empty
- $self->{encoded_source};
-}
-
-sub url {
- my $self = shift;
- my $new_url = shift;
- $self->{url} = $new_url if defined($new_url) and $new_url =~ /\S/;
- $self->{url};
-}
-
-sub formatRenderedProblem {
- my $self = shift;
- my $problemText ='';
- my $rh_result = $self->return_object() || {}; # wrap problem in formats
- $problemText = "No output from rendered Problem" unless $rh_result;
- print "\nformatRenderedProblem return_object $rh_result = ",join(" ", sort keys %$rh_result),"\n" if $UNIT_TESTS_ON;
- if (ref($rh_result) and $rh_result->{text} ) { ##text vs body_text
- $problemText = $rh_result->{text};
- $problemText .= $rh_result->{flags}{comment} if ( $rh_result->{flags}{comment} && $self->{inputs_ref}{showComments} );
- } else {
- $problemText .= "Unable to decode problem text \n".
- $self->{error_string}."\n".format_hash_ref($rh_result);
- }
- my $problemHeadText = $rh_result->{header_text}//''; ##head_text vs header_text
- my $problemPostHeaderText = $rh_result->{post_header_text}//'';
- my $rh_answers = $rh_result->{answers}//{};
- my $answerOrder = $rh_result->{flags}->{ANSWER_ENTRY_ORDER}//[]; #[sort keys %{ $rh_result->{answers} }];
- my $encoded_source = $self->encoded_source//'';
- my $sourceFilePath = $self->{sourceFilePath}//'';
- my $problemSourceURL = $self->{inputs_ref}->{problemSourceURL};
- my $warnings = '';
- print "\n return_object answers ",
- join( " ", %{ $rh_result->{PG_ANSWERS_HASH} } )
- if $UNIT_TESTS_ON;
-
-
- #################################################
- # regular Perl warning messages generated with warn
- #################################################
-
- if ( defined ($rh_result->{WARNINGS}) and $rh_result->{WARNINGS} ){
- $warnings = "
-
WARNINGS
"
- . $rh_result->{WARNINGS}
- . "
";
- }
- #warn "keys: ", join(" | ", sort keys %{$rh_result });
-
- #################################################
- # PG debug messages generated with DEBUG_message();
- #################################################
-
- my $debug_messages = $rh_result->{debug_messages} || [];
- $debug_messages = join(" \n", @{ $debug_messages });
-
- #################################################
- # PG warning messages generated with WARN_message();
- #################################################
-
- my $PG_warning_messages = $rh_result->{warning_messages} || [];
- $PG_warning_messages = join(" \n", @{ $PG_warning_messages } );
-
- #################################################
- # internal debug messages generated within PG_core
- # these are sometimes needed if the PG_core warning message system
- # isn't properly set up before the bug occurs.
- # In general don't use these unless necessary.
- #################################################
-
- my $internal_debug_messages = $rh_result->{internal_debug_messages} || [];
- $internal_debug_messages = join(" \n", @{ $internal_debug_messages } );
-
- my $fileName = $self->{input}->{envir}->{fileName} || "";
-
- #################################################
-
- my $XML_URL = $self->url // '';
- my $FORM_ACTION_URL = $self->{form_action_url} // '';
- my $SITE_URL = $self->{baseURL} // '';
- my $SITE_HOST = $ENV{SITE_HOST} // '';
- my $courseID = $self->{courseID} // '';
- my $userID = $self->{userID} // '';
- my $course_password = $self->{course_password} // '';
- my $problemSeed = $self->{problem_seed};
- my $psvn = $self->{inputs_ref}{psvn} // 54321;
- my $displayMode = $self->{inputs_ref}{displayMode} // 'MathJax';
- my $problemJWT = $self->{inputs_ref}{problemJWT} // '';
- my $sessionJWT = $self->{return_object}{sessionJWT} // '';
-
- my $previewMode = defined( $self->{inputs_ref}{previewAnswers} ) || 0;
- # showCorrectMode needs more security -- ww2 uses want/can/will
- my $showCorrectMode = defined( $self->{inputs_ref}{showCorrectAnswers} ) || 0;
- my $submitMode = defined($self->{inputs_ref}{submitAnswers}) || $self->{inputs_ref}{answersSubmitted} || 0;
-
- # problemUUID can be added to the request as a parameter. It adds a prefix
- # to the identifier used by the format so that several different problems
- # can appear on the same page.
- my $problemUUID = $self->{inputs_ref}{problemUUID} // 1;
- my $problemResult = $rh_result->{problem_result} // '';
- my $problemState = $rh_result->{problem_state} // '';
- my $showPartialCorrectAnswers = $self->{inputs_ref}{showPartialCorrectAnswers}
- // $rh_result->{flags}{showPartialCorrectAnswers};
- my $showSummary = $self->{inputs_ref}{showSummary} // 1; #default to show summary for the moment
- my $formLanguage = $self->{inputs_ref}{language} // 'en';
- my $showTable = $self->{inputs_ref}{hideAttemptsTable} ? 0 : 1;
- my $showMessages = $self->{inputs_ref}{hideMessages} ? 0 : 1;
- my $scoreSummary = '';
-
- my $COURSE_LANG_AND_DIR = get_lang_and_dir($formLanguage);
- # Set up the problem language and direction
- # PG files can request their language and text direction be set. If we do
- # not have access to a default course language, fall back to the
- # $formLanguage instead.
- my %PROBLEM_LANG_AND_DIR = get_problem_lang_and_dir($rh_result->{flags}, "auto:en:ltr", $formLanguage);
- my $PROBLEM_LANG_AND_DIR = join(" ", map { qq{$_="$PROBLEM_LANG_AND_DIR{$_}"} } keys %PROBLEM_LANG_AND_DIR);
- my $mt = WeBWorK::Localize::getLangHandle($self->{inputs_ref}{language} // 'en');
-
- my $answerTemplate = '';
- if ($submitMode && $showTable) {
- my $tbl = WeBWorK::Utils::AttemptsTable->new(
- $rh_answers,
- answersSubmitted => 1,
- answerOrder => $answerOrder,
- displayMode => $displayMode,
- showAnswerNumbers => 0,
- showAttemptAnswers => 0,
- showAttemptPreviews => 1,
- showAttemptResults => $showPartialCorrectAnswers,
- showCorrectAnswers => $showCorrectMode,
- showMessages => $showMessages,
- showSummary => $showSummary,
- maketext => WeBWorK::Localize::getLoc($formLanguage),
- summary => $problemResult->{summary} // '', # can be set by problem grader???
- );
-
- $answerTemplate = $tbl->answerTemplate;
- $tbl->imgGen->render(body_text => \$answerTemplate) if $tbl->displayMode eq 'images';
- }
-
- # warn "imgGen is ", $tbl->imgGen;
- #warn "answerOrder ", $tbl->answerOrder;
- #warn "answersSubmitted ", $tbl->answersSubmitted;
- # render equation images
-
- if ($submitMode && $problemResult && $showSummary) {
- $scoreSummary = CGI::p($mt->maketext('Your score on this attempt is [_1]', wwRound(0, $problemResult->{score} * 100).'%'));
-
- #$scoreSummary .= CGI::p($mt->maketext("Your score was not recorded."));
-
- #scoreSummary .= CGI::p('Your score on this problem has not been recorded.');
- #$scoreSummary .= CGI::hidden({id=>'problem-result-score', name=>'problem-result-score',value=>$problemResult->{score}});
- }
-
- # this should never? be blocked -- contains relevant info for
- if ($problemResult->{msg}) {
- $scoreSummary .= CGI::p($problemResult->{msg});
- }
-
- # This stuff is put here because eventually we will add locale support so the
- # text will have to be done server side.
- my $localStorageMessages = CGI::start_div({id=>'local-storage-messages'});
- $localStorageMessages.= CGI::p('Your overall score for this problem is'.' '.CGI::span({id=>'problem-overall-score'},''));
- $localStorageMessages .= CGI::end_div();
-
- # Add JS files requested by problems via ADD_JS_FILE() in the PG file.
- my $extra_js_files = '';
- if (ref($rh_result->{flags}{extra_js_files}) eq 'ARRAY') {
- $rh_result->{js} = [];
- my %jsFiles;
- for (@{ $rh_result->{flags}{extra_js_files} }) {
- next if $jsFiles{ $_->{file} };
- $jsFiles{ $_->{file} } = 1;
- my %attributes = ref($_->{attributes}) eq 'HASH' ? %{ $_->{attributes} } : ();
- if ($_->{external}) {
- push @{ $rh_result->{js} }, $_->{file};
- $extra_js_files .= CGI::script({ src => $_->{file}, %attributes }, '');
- } else {
- my $url = getAssetURL($self->{inputs_ref}{language} // 'en', $_->{file});
- push @{ $rh_result->{js} }, $SITE_URL.$url;
- $extra_js_files .= CGI::script({ src => $SITE_URL.$url, %attributes }, '');
- }
- }
- }
-
- # Add CSS files requested by problems via ADD_CSS_FILE() in the PG file
- # (the value should be an anonomous array).
- my $extra_css_files = '';
- my @cssFiles;
- if (ref($rh_result->{flags}{extra_css_files}) eq 'ARRAY') {
- push @cssFiles, @{ $rh_result->{flags}{extra_css_files} };
- }
- my %cssFilesAdded; # Used to avoid duplicates
- $rh_result->{css} = [];
- for (@cssFiles) {
- next if $cssFilesAdded{ $_->{file} };
- $cssFilesAdded{ $_->{file} } = 1;
- if ($_->{external}) {
- push @{ $rh_result->{css} }, $_->{file};
- $extra_css_files .= CGI::Link({ rel => 'stylesheet', href => $_->{file} });
- } else {
- my $url = getAssetURL($self->{inputs_ref}{language} // 'en', $_->{file});
- push @{ $rh_result->{css} }, $SITE_URL.$url;
- $extra_css_files .= CGI::Link({ href => $SITE_URL.$url, rel => 'stylesheet' });
- }
- }
-
- my $STRING_Preview = $mt->maketext("Preview My Answers");
- my $STRING_ShowCorrect = $mt->maketext("Show Correct Answers");
- my $STRING_Submit = $mt->maketext("Submit Answers");
-
- #my $pretty_print_self = pretty_print($self);
-
- ######################################################
- # Return interpolated problem template
- ######################################################
- my $format_name = $self->{inputs_ref}->{outputFormat};
-
- if ($format_name eq "ww3") {
- my $json_output = do("WebworkClient/ww3_format.pl");
- for my $key (keys %$json_output) {
- # Interpolate values
- $json_output->{$key} =~ s/(\$\w+)/"defined $1 ? $1 : ''"/gee;
- }
- $json_output->{submitButtons} = [];
- push(@{$json_output->{submitButtons}}, { name => 'previewAnswers', value => $STRING_Preview })
- if $self->{inputs_ref}{showPreviewButton};
- push(@{$json_output->{submitButtons}}, { name => 'submitAnswers', value => $STRING_Submit })
- if $self->{inputs_ref}{showCheckAnswersButton};
- push(@{$json_output->{submitButtons}}, { name => 'showCorrectAnswers', value => $STRING_ShowCorrect })
- if $self->{inputs_ref}{showCorrectAnswersButton};
- return $json_output;
- }
-
- $format_name //= 'formatRenderedProblemFailure';
- # find the appropriate template in WebworkClient folder
- my $template = do("WebworkClient/${format_name}_format.pl")//'';
- die "Unknown format name $format_name" unless $template;
- # interpolate values into template
- $template =~ s/(\$\w+)/"defined $1 ? $1 : ''"/gee;
- return $template;
-}
-
-sub pretty_print { # provides html output -- NOT a method
- my $r_input = shift;
- my $level = shift;
- $level = 4 unless defined($level);
- $level--;
- return '' unless $level > 0; # only print three levels of hashes (safety feature)
- my $out = '';
- if ( not ref($r_input) ) {
- $out = $r_input if defined $r_input; # not a reference
- $out =~ s/</g ; # protect for HTML output
- } elsif ("$r_input" =~/hash/i) { # this will pick up objects whose '$self' is hash and so works better than ref($r_iput).
- local($^W) = 0;
-
- $out .= "$r_input " ."";
-
- foreach my $key ( sort ( keys %$r_input )) {
- $out .= " $key => ".pretty_print($r_input->{$key}) . " ";
- }
- $out .="
";
- } elsif (ref($r_input) eq 'ARRAY' ) {
- my @array = @$r_input;
- $out .= "( " ;
- while (@array) {
- $out .= pretty_print(shift @array, $level) . " , ";
- }
- $out .= " )";
- } elsif (ref($r_input) eq 'CODE') {
- $out = "$r_input";
- } else {
- $out = $r_input;
- $out =~ s/</g; # protect for HTML output
- }
-
- return $out." ";
-}
-
-1;
diff --git a/lib/RenderApp/Controller/Render.pm b/lib/RenderApp/Controller/Render.pm
index 57be59d93..563d43f2d 100644
--- a/lib/RenderApp/Controller/Render.pm
+++ b/lib/RenderApp/Controller/Render.pm
@@ -56,7 +56,6 @@ sub parseRequest {
return undef;
};
$claims = $claims->{webwork} if defined $claims->{webwork};
- # $claims->{problemJWT} = $problemJWT; # because we're merging claims, this is unnecessary?
# override key-values in params with those provided in the JWT
@params{ keys %$claims } = values %$claims;
} else {
@@ -107,6 +106,7 @@ async sub problem {
my $c = shift;
my $inputs_ref = $c->parseRequest;
return unless $inputs_ref;
+
$inputs_ref->{problemSource} = fetchRemoteSource_p($c, $inputs_ref->{problemSourceURL}) if $inputs_ref->{problemSourceURL};
my $file_path = $inputs_ref->{sourceFilePath};
@@ -130,71 +130,67 @@ async sub problem {
return $c->exception($problem->{_message}, $problem->{status})
unless $problem->success();
- $inputs_ref->{sourceFilePath} = $problem->{read_path}; # in case the path was updated...
-
- my $input_errs = checkInputs($inputs_ref);
-
$c->render_later; # tell Mojo that this might take a while
my $ww_return_json = await $problem->render($inputs_ref);
return $c->exception( $problem->{_message}, $problem->{status} )
unless $problem->success();
- my $ww_return_hash = decode_json($ww_return_json);
- my $output_errs = checkOutputs($ww_return_hash);
-
- $ww_return_hash->{debug}->{render_warn} = [$input_errs, $output_errs];
-
- # if answers are submitted and there is a provided answerURL...
- if ($inputs_ref->{JWTanswerURL} && $ww_return_hash->{JWT}{answer} && $inputs_ref->{submitAnswers}) {
- my $answerJWTresponse = {
- iss => $ENV{SITE_HOST},
- subject => 'webwork.result',
- status => 502,
- message => 'initial message'
- };
- my $reqBody = {
- Origin => $ENV{SITE_HOST},
- 'Content-Type' => 'text/plain',
- };
-
- $c->log->info("sending answerJWT to $inputs_ref->{JWTanswerURL}");
- await $c->ua->max_redirects(5)->request_timeout(7)->post_p($inputs_ref->{JWTanswerURL}, $reqBody, $ww_return_hash->{JWT}{answer})->
- then(sub {
- my $response = shift->result;
-
- $answerJWTresponse->{status} = int($response->code);
- # answerURL responses are expected to be JSON
- if ($response->json) {
- # munge data with default response object
- $answerJWTresponse = { %$answerJWTresponse, %{$response->json} };
- } else {
- # otherwise throw the whole body as the message
- $answerJWTresponse->{message} = $response->body;
- }
- })->
- catch(sub {
- my $err = shift;
- $c->log->error($err);
+ my $return_object = decode_json($ww_return_json);
- $answerJWTresponse->{status} = 500;
- $answerJWTresponse->{message} = '[' . $c->logID . '] ' . $err;
- });
+ # if answerURL provided and this is a submit, then send the answerJWT
+ if ($inputs_ref->{JWTanswerURL} && $inputs_ref->{submitAnswers} && !$inputs_ref->{showCorrectAnswers}) {
+ $return_object->{JWTanswerURLstatus} = await sendAnswerJWT($c, $inputs_ref->{JWTanswerURL}, $return_object->{answerJWT});
+ }
- $answerJWTresponse = encode_json($answerJWTresponse);
- # this will become a string literal, so single-quote characters must be escaped
- $answerJWTresponse =~ s/'/\\'/g;
- $c->log->info("answerJWT response ".$answerJWTresponse);
+ # format the response
+ $c->format($return_object);
+}
- $ww_return_hash->{renderedHTML} =~ s/JWTanswerURLstatus/$answerJWTresponse/g;
- } else {
- $ww_return_hash->{renderedHTML} =~ s/JWTanswerURLstatus//;
- }
+async sub sendAnswerJWT {
+ my $c = shift;
+ my $JWTanswerURL = shift;
+ my $answerJWT = shift;
+
+ my $answerJWTresponse = {
+ iss => $ENV{SITE_HOST},
+ subject => 'webwork.result',
+ status => 502,
+ message => 'initial message'
+ };
+ my $reqBody = {
+ Origin => $ENV{SITE_HOST},
+ 'Content-Type' => 'text/plain',
+ };
- $c->respond_to(
- html => { text => $ww_return_hash->{renderedHTML} },
- json => { json => $ww_return_hash }
- );
+ $c->log->info("sending answerJWT to $JWTanswerURL");
+ await $c->ua->max_redirects(5)->request_timeout(7)->post_p($JWTanswerURL, $reqBody, $answerJWT)->
+ then(sub {
+ my $response = shift->result;
+
+ $answerJWTresponse->{status} = int($response->code);
+ # answerURL responses are expected to be JSON
+ if ($response->json) {
+ # munge data with default response object
+ $answerJWTresponse = { %$answerJWTresponse, %{$response->json} };
+ } else {
+ # otherwise throw the whole body as the message
+ $answerJWTresponse->{message} = $response->body;
+ }
+ })->
+ catch(sub {
+ my $err = shift;
+ $c->log->error($err);
+
+ $answerJWTresponse->{status} = 500;
+ $answerJWTresponse->{message} = '[' . $c->logID . '] ' . $err;
+ });
+
+ $answerJWTresponse = encode_json($answerJWTresponse);
+ # this will become a string literal, so single-quote characters must be escaped
+ $answerJWTresponse =~ s/'/\\'/g;
+ $c->log->info("answerJWT response ".$answerJWTresponse);
+ return $answerJWTresponse;
}
sub checkInputs {
@@ -215,11 +211,12 @@ sub checkInputs {
push @errs, $err;
}
}
- return "Form data submitted for "
+ return @errs ? "Form data submitted for "
. $inputs_ref->{sourceFilePath}
. " contained errors: {"
. join "}, {", @errs
- . "}";
+ . "}"
+ : undef;
}
sub checkOutputs {
@@ -247,11 +244,12 @@ sub checkOutputs {
}
}
}
- return
+ return @errs ?
"Output from rendering "
- . ($outputs_ref->{sourceFilePath} // '')
- . " contained errors: {"
- . join "}, {", @errs . "}";
+ . ($outputs_ref->{sourceFilePath} // '')
+ . " contained errors: {"
+ . join "}, {", @errs . "}"
+ : undef;
}
sub exception {
diff --git a/lib/RenderApp/Controller/StaticFiles.pm b/lib/RenderApp/Controller/StaticFiles.pm
index 6695d53f5..9b0dc9ad7 100644
--- a/lib/RenderApp/Controller/StaticFiles.pm
+++ b/lib/RenderApp/Controller/StaticFiles.pm
@@ -29,7 +29,7 @@ sub pg_file ($c) {
}
sub public_file($c) {
- $c->reply_with_file_if_readable($c->app->home->child('public', $c->stash('fail')));
+ $c->reply_with_file_if_readable($c->app->home->child('public', $c->stash('static')));
}
1;
diff --git a/lib/RenderApp/Model/Problem.pm b/lib/RenderApp/Model/Problem.pm
index 8747231ec..653c5d12e 100644
--- a/lib/RenderApp/Model/Problem.pm
+++ b/lib/RenderApp/Model/Problem.pm
@@ -9,7 +9,7 @@ use Mojo::JSON qw( encode_json );
use Mojo::Base -async_await;
use Time::HiRes qw( time );
use MIME::Base64 qw( decode_base64 );
-use RenderApp::Controller::RenderProblem;
+use WeBWorK::RenderProblem;
##### Problem params: #####
# = random_seed (set randomization for rendering)
@@ -68,7 +68,7 @@ sub _init {
# sourcecode takes precedence over reading from file path
if ( $problem_contents =~ /\S/ ) {
$self->source($problem_contents);
- $self->{code_origin} = 'pg source (' . $self->path( $read_path, 'force' ) .')';
+ $self->{code_origin} = 'pg source (' . ($self->path( $read_path, 'force' ) || 'no path provided') .')';
# set read_path without failing for !-e
# this supports images in problems via editor
} else {
@@ -222,7 +222,7 @@ sub render {
my $inputs_ref = shift;
$self->{action} = 'render';
my $renderPromise = Mojo::IOLoop->subprocess->run_p( sub {
- return RenderApp::Controller::RenderProblem::process_pg_file( $self, $inputs_ref );
+ return WeBWorK::RenderProblem::process_pg_file( $self, $inputs_ref );
})->catch(sub {
$self->{exception} = Mojo::Exception->new(shift)->trace;
$self->{_error} = "500 Render failed: " . $self->{exception}->message;
diff --git a/lib/WeBWorK/AttemptsTable.pm b/lib/WeBWorK/AttemptsTable.pm
new file mode 100644
index 000000000..56717cf54
--- /dev/null
+++ b/lib/WeBWorK/AttemptsTable.pm
@@ -0,0 +1,467 @@
+################################################################################
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of either: (a) the GNU General Public License as published by the
+# Free Software Foundation; either version 2, or (at your option) any later
+# version, or (b) the "Artistic License" which comes with this package.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
+# Artistic License for more details.
+################################################################################
+
+=head1 NAME
+
+ AttemptsTable
+
+=head1 SYNPOSIS
+
+ my $tbl = WeBWorK::HTML::AttemptsTable->new(
+ $answers,
+ answersSubmitted => 1,
+ answerOrder => $pg->{flags}{ANSWER_ENTRY_ORDER},
+ displayMode => 'MathJax',
+ showAnswerNumbers => 0,
+ showAttemptAnswers => $showAttemptAnswers && $showEvaluatedAnswers,
+ showAttemptPreviews => $showAttemptPreview,
+ showAttemptResults => $showAttemptResults,
+ showCorrectAnswers => $showCorrectAnswers,
+ showMessages => $showAttemptAnswers, # internally checks for messages
+ showSummary => $showSummary,
+ imgGen => $imgGen, # not needed if ce is present ,
+ ce => '', # not needed if $imgGen is present
+ maketext => WeBWorK::Localize::getLoc("en"),
+ );
+ $tbl->{imgGen}->render(refresh => 1) if $tbl->displayMode eq 'images';
+ my $answerTemplate = $tbl->answerTemplate;
+
+
+=head1 DESCRIPTION
+
+This module handles the formatting of the table which presents the results of analyzing a student's
+answer to a WeBWorK problem. It is used in Problem.pm, OpaqueServer.pm, standAlonePGproblemRender
+
+=head2 new
+
+ my $tbl = WeBWorK::HTML::AttemptsTable->new(
+ $answers,
+ answersSubmitted => 1,
+ answerOrder => $pg->{flags}{ANSWER_ENTRY_ORDER},
+ displayMode => 'MathJax',
+ showHeadline => 1,
+ showAnswerNumbers => 0,
+ showAttemptAnswers => $showAttemptAnswers && $showEvaluatedAnswers,
+ showAttemptPreviews => $showAttemptPreview,
+ showAttemptResults => $showAttemptResults,
+ showCorrectAnswers => $showCorrectAnswers,
+ showMessages => $showAttemptAnswers, # internally checks for messages
+ showSummary => $showSummary,
+ imgGen => $imgGen, # not needed if ce is present ,
+ ce => '', # not needed if $imgGen is present
+ maketext => WeBWorK::Localize::getLoc("en"),
+ summary =>'',
+ );
+
+ $answers -- a hash of student answers e.g. $pg->{answers}
+ answersSubmitted if 0 then then the attemptsTable is not displayed (???)
+ answerOrder -- an array indicating the order the answers appear on the page.
+ displayMode 'MathJax' and 'images' are the most common
+
+ showHeadline Show the header line 'Results for this submission'
+
+ showAnswerNumbers, showAttemptAnswers, showAttemptPreviews,showAttemptResults,
+ showCorrectAnswers and showMessages control the display of each column in the table.
+
+ attemptAnswers the student's typed in answer (possibly simplified numerically)
+ attemptPreview the student's answer after typesetting
+ attemptResults "correct", "_% correct", "incorrect" or "ungraded"- links to the answer blank
+ correctAnswers typeset version (untypeset versions are available via popups)
+ messages warns of formatting typos in the answer, or
+ more detailed messages about a wrong answer
+ summary is obtained from $pg->{result}{summary}.
+ If this is empty then a (localized)
+ version of "all answers are correct"
+ or "at least one answer is not coorrect"
+ imgGen points to a prebuilt image generator objectfor "images" mode
+ ce points to the CourseEnvironment -- it is needed if AttemptsTable
+ is required to build its own imgGen object
+ maketext points to a localization subroutine
+
+=head2 Methods
+
+=over 4
+
+=item answerTemplate
+
+Returns HTML which formats the analysis of the student's answers to the problem.
+
+=back
+
+=head2 Read/Write Properties
+
+=over 4
+
+=item showMessages,
+
+This can be switched on or off before exporting the answerTemplate, perhaps
+under instructions from the PG problem.
+
+=item summary
+
+The contents of the summary can be defined when the attemptsTable object is created.
+
+The summary can be defined by the PG problem grader usually returned as
+$pg->{result}{summary}.
+
+If the summary is not explicitly defined then (localized) versions
+of the default summaries are created:
+
+ "The answer above is correct.",
+ "Some answers will be graded later.",
+ "All of the [gradeable] answers above are correct.",
+ "[N] of the questions remain unanswered.",
+ "At least one of the answers above is NOT [fully] correct.',
+
+Note that if this is set after initialization, you must ensure that it is a
+Mojo::ByteStream object if it contains html or characters that need escaping.
+
+=back
+
+=cut
+
+package WeBWorK::AttemptsTable;
+use Mojo::Base 'Class::Accessor', -signatures;
+
+use Scalar::Util 'blessed';
+use WeBWorK::Utils 'wwRound';
+
+# %options may contain: displayMode, submitted, imgGen, ce
+# At least one of imgGen or ce must be provided if displayMode is 'images'.
+sub new ($class, $rh_answers, $c, %options) {
+ $class = ref $class || $class;
+ ref($rh_answers) =~ /HASH/ or die 'The first entry to AttemptsTable must be a hash of answers';
+ $c->isa('Mojolicious::Controller') or die 'The second entry to AttemptsTable must be a WeBWorK::Controller';
+ my $self = bless {
+ answers => $rh_answers,
+ c => $c,
+ answerOrder => $options{answerOrder} // [],
+ answersSubmitted => $options{answersSubmitted} // 0,
+ summary => undef, # summary provided by problem grader (set in _init)
+ displayMode => $options{displayMode} || 'MathJax',
+ showHeadline => $options{showHeadline} // 1,
+ showAnswerNumbers => $options{showAnswerNumbers} // 1,
+ showAttemptAnswers => $options{showAttemptAnswers} // 1, # show student answer as entered and parsed
+ showAttemptPreviews => $options{showAttemptPreviews} // 1, # show preview of student answer
+ showAttemptResults => $options{showAttemptResults} // 1, # show results of grading student answer
+ showMessages => $options{showMessages} // 1, # show messages generated by evaluation
+ showCorrectAnswers => $options{showCorrectAnswers} // 0, # show the correct answers
+ showSummary => $options{showSummary} // 1, # show result summary
+ imgGen => undef, # set or created in _init method
+ mtRef => $options{mtRef} // sub { return $_[0] },
+ }, $class;
+
+ # Create accessors/mutators
+ $self->mk_ro_accessors(qw(answers c answerOrder answersSubmitted displayMode imgGen showAnswerNumbers
+ showAttemptAnswers showHeadline showAttemptPreviews showAttemptResults showCorrectAnswers showSummary));
+ $self->mk_accessors(qw(showMessages summary));
+
+ # Sanity check and initialize imgGenerator.
+ $self->_init(%options);
+
+ return $self;
+}
+
+# Verify the display mode, and build imgGen if it is not supplied.
+sub _init ($self, %options) {
+ $self->{submitted} = $options{submitted} // 0;
+ $self->{displayMode} = $options{displayMode} || 'MathJax';
+
+ # Only show message column if there is at least one message.
+ my @reallyShowMessages = grep { $self->answers->{$_}{ans_message} } @{ $self->answerOrder };
+ $self->showMessages($self->showMessages && !!@reallyShowMessages);
+
+ # Only used internally. Accessors are not needed.
+ $self->{numCorrect} = 0;
+ $self->{numBlanks} = 0;
+ $self->{numEssay} = 0;
+
+ if ($self->displayMode eq 'images') {
+ if (blessed($options{imgGen}) && $options{imgGen}->isa('WeBWorK::PG::ImageGenerator')) {
+ $self->{imgGen} = $options{imgGen};
+ } elsif (blessed($options{ce}) && $options{ce}->isa('WeBWorK::CourseEnvironment')) {
+ my $ce = $options{ce};
+
+ $self->{imgGen} = WeBWorK::PG::ImageGenerator->new(
+ tempDir => $ce->{webworkDirs}{tmp},
+ latex => $ce->{externalPrograms}{latex},
+ dvipng => $ce->{externalPrograms}{dvipng},
+ useCache => 1,
+ cacheDir => $ce->{webworkDirs}{equationCache},
+ cacheURL => $ce->{server_root_url} . $ce->{webworkURLs}{equationCache},
+ cacheDB => $ce->{webworkFiles}{equationCacheDB},
+ dvipng_align => $ce->{pg}{displayModeOptions}{images}{dvipng_align},
+ dvipng_depth_db => $ce->{pg}{displayModeOptions}{images}{dvipng_depth_db},
+ );
+ } else {
+ warn 'Must provide image Generator (imgGen) or a course environment (ce) to build attempts table.';
+ }
+ }
+
+ # Make sure that the provided summary is a Mojo::ByteStream object.
+ $self->summary(blessed($options{summary})
+ && $options{summary}->isa('Mojo::ByteStream') ? $options{summary} : $self->c->b($options{summary} // ''));
+
+ return;
+}
+
+sub formatAnswerRow ($self, $rh_answer, $ans_id, $answerNumber) {
+ my $c = $self->c;
+
+ my $answerString = $rh_answer->{student_ans} // '';
+ my $answerPreview = $self->previewAnswer($rh_answer) // ' ';
+ my $correctAnswer = $rh_answer->{correct_ans} // '';
+ my $correctAnswerPreview = $self->previewCorrectAnswer($rh_answer) // ' ';
+
+ my $answerMessage = $rh_answer->{ans_message} // '';
+ $answerMessage =~ s/\n/ /g;
+ my $answerScore = $rh_answer->{score} // 0;
+ $self->{numCorrect} += $answerScore >= 1;
+ $self->{numEssay} += ($rh_answer->{type} // '') eq 'essay';
+ $self->{numBlanks}++ unless $answerString =~ /\S/ || $answerScore >= 1;
+
+ my $feedbackMessageClass = ($answerMessage eq '') ? '' : $self->maketext('FeedbackMessage');
+
+ my $resultString;
+ my $resultStringClass;
+ if ($answerScore >= 1) {
+ $resultString = $self->maketext('correct');
+ $resultStringClass = 'ResultsWithoutError';
+ } elsif (($rh_answer->{type} // '') eq 'essay') {
+ $resultString = $self->maketext('Ungraded');
+ $self->{essayFlag} = 1;
+ } elsif ($answerScore == 0) {
+ $resultStringClass = 'ResultsWithError';
+ $resultString = $self->maketext('incorrect');
+ } else {
+ $resultString = $self->maketext('[_1]% correct', wwRound(0, $answerScore * 100));
+ }
+ my $attemptResults = $c->tag(
+ 'td',
+ class => $resultStringClass,
+ $c->tag('a', href => '#', data => { answer_id => $ans_id }, $self->nbsp($resultString))
+ );
+
+ return $c->c(
+ $self->showAnswerNumbers ? $c->tag('td', $answerNumber) : '',
+ $self->showAttemptAnswers ? $c->tag('td', dir => 'auto', $self->nbsp($answerString)) : '',
+ $self->showAttemptPreviews ? $self->formatToolTip($answerString, $answerPreview) : '',
+ $self->showAttemptResults ? $attemptResults : '',
+ $self->showCorrectAnswers ? $self->formatToolTip($correctAnswer, $correctAnswerPreview) : '',
+ $self->showMessages ? $c->tag('td', class => $feedbackMessageClass, $self->nbsp($answerMessage)) : ''
+ )->join('');
+}
+
+# Determine whether any answers were submitted and create answer template if they have been.
+sub answerTemplate ($self) {
+ my $c = $self->c;
+
+ return '' unless $self->answersSubmitted; # Only print if there is at least one non-blank answer
+
+ my $tableRows = $c->c;
+
+ push(
+ @$tableRows,
+ $c->tag(
+ 'tr',
+ $c->c(
+ $self->showAnswerNumbers ? $c->tag('th', '#') : '',
+ $self->showAttemptAnswers ? $c->tag('th', $self->maketext('Entered')) : '',
+ $self->showAttemptPreviews ? $c->tag('th', $self->maketext('Answer Preview')) : '',
+ $self->showAttemptResults ? $c->tag('th', $self->maketext('Result')) : '',
+ $self->showCorrectAnswers ? $c->tag('th', $self->maketext('Correct Answer')) : '',
+ $self->showMessages ? $c->tag('th', $self->maketext('Message')) : ''
+ )->join('')
+ )
+ );
+
+ my $answerNumber = 0;
+ for (@{ $self->answerOrder() }) {
+ push @$tableRows, $c->tag('tr', $self->formatAnswerRow($self->{answers}{$_}, $_, ++$answerNumber));
+ }
+
+ return $c->c(
+ $self->showHeadline
+ ? $c->tag('h2', class => 'attemptResultsHeader', $self->maketext('Results for this submission'))
+ : '',
+ $c->tag(
+ 'div',
+ class => 'table-responsive',
+ $c->tag('table', class => 'attemptResults table table-sm table-bordered', $tableRows->join(''))
+ ),
+ $self->showSummary ? $self->createSummary : ''
+ )->join('');
+}
+
+sub previewAnswer ($self, $answerResult) {
+ my $displayMode = $self->displayMode;
+ my $imgGen = $self->imgGen;
+
+ my $tex = $answerResult->{preview_latex_string};
+
+ return '' unless defined $tex and $tex ne '';
+
+ return $tex if $answerResult->{non_tex_preview};
+
+ if ($displayMode eq 'plainText') {
+ return $tex;
+ } elsif (($answerResult->{type} // '') eq 'essay') {
+ return $tex;
+ } elsif ($displayMode eq 'images') {
+ return $imgGen->add($tex);
+ } elsif ($displayMode eq 'MathJax') {
+ return $self->c->tag('script', type => 'math/tex; mode=display', $self->c->b($tex));
+ }
+}
+
+sub previewCorrectAnswer ($self, $answerResult) {
+ my $displayMode = $self->displayMode;
+ my $imgGen = $self->imgGen;
+
+ my $tex = $answerResult->{correct_ans_latex_string};
+
+ # Some answers don't have latex strings defined return the raw correct answer
+ # unless defined $tex and $tex contains non whitespace characters;
+ return $answerResult->{correct_ans}
+ unless defined $tex and $tex =~ /\S/;
+
+ return $tex if $answerResult->{non_tex_preview};
+
+ if ($displayMode eq 'plainText') {
+ return $tex;
+ } elsif ($displayMode eq 'images') {
+ return $imgGen->add($tex);
+ } elsif ($displayMode eq 'MathJax') {
+ return $self->c->tag('script', type => 'math/tex; mode=display', $self->c->b($tex));
+ }
+}
+
+# Create summary
+sub createSummary ($self) {
+ my $c = $self->c;
+
+ my $numCorrect = $self->{numCorrect};
+ my $numBlanks = $self->{numBlanks};
+ my $numEssay = $self->{numEssay};
+
+ my $summary;
+
+ unless (defined($self->summary) and $self->summary =~ /\S/) {
+ # Default messages
+ $summary = $c->c;
+ my @answerNames = @{ $self->answerOrder() };
+ if (scalar @answerNames == 1) {
+ if ($numCorrect == scalar @answerNames) {
+ push(
+ @$summary,
+ $c->tag(
+ 'div',
+ class => 'ResultsWithoutError mb-2',
+ $self->maketext('The answer above is correct.')
+ )
+ );
+ } elsif ($self->{essayFlag}) {
+ push(@$summary, $c->tag('div', $self->maketext('Some answers will be graded later.')));
+ } else {
+ push(
+ @$summary,
+ $c->tag(
+ 'div',
+ class => 'ResultsWithError mb-2',
+ $self->maketext('The answer above is NOT correct.')
+ )
+ );
+ }
+ } else {
+ if ($numCorrect + $numEssay == scalar @answerNames) {
+ if ($numEssay) {
+ push(
+ @$summary,
+ $c->tag(
+ 'div',
+ class => 'ResultsWithoutError mb-2',
+ $self->maketext('All of the gradeable answers above are correct.')
+ )
+ );
+ } else {
+ push(
+ @$summary,
+ $c->tag(
+ 'div',
+ class => 'ResultsWithoutError mb-2',
+ $self->maketext('All of the answers above are correct.')
+ )
+ );
+ }
+ } elsif ($numBlanks + $numEssay != scalar(@answerNames)) {
+ push(
+ @$summary,
+ $c->tag(
+ 'div',
+ class => 'ResultsWithError mb-2',
+ $self->maketext('At least one of the answers above is NOT correct.')
+ )
+ );
+ }
+ if ($numBlanks > $numEssay) {
+ my $s = ($numBlanks > 1) ? '' : 's';
+ push(
+ @$summary,
+ $c->tag(
+ 'div',
+ class => 'ResultsAlert mb-2',
+ $self->maketext(
+ '[quant,_1,of the questions remains,of the questions remain] unanswered.', $numBlanks
+ )
+ )
+ );
+ }
+ }
+ $summary = $summary->join('');
+ } else {
+ $summary = $self->summary; # Summary defined by grader
+ }
+ $summary = $c->tag('div', role => 'alert', class => 'attemptResultsSummary', $summary);
+ $self->summary($summary);
+ return $summary;
+}
+
+# Utility subroutine that prevents unwanted line breaks, and ensures that the return value is a Mojo::ByteStream object.
+sub nbsp ($self, $str) {
+ return $self->c->b(defined $str && $str =~ /\S/ ? $str : ' ');
+}
+
+# Note that formatToolTip output includes the wrapper.
+sub formatToolTip ($self, $answer, $formattedAnswer) {
+ return $self->c->tag(
+ 'td',
+ $self->c->tag(
+ 'div',
+ class => 'answer-preview',
+ data => {
+ bs_toggle => 'popover',
+ bs_content => $answer,
+ bs_placement => 'bottom',
+ },
+ $self->nbsp($formattedAnswer)
+ )
+ );
+}
+
+sub maketext ($self, @args) {
+ return $self->{mtRef}->(@args);
+}
+
+1;
diff --git a/lib/WeBWorK/FormatRenderedProblem.pm b/lib/WeBWorK/FormatRenderedProblem.pm
new file mode 100644
index 000000000..6d513f738
--- /dev/null
+++ b/lib/WeBWorK/FormatRenderedProblem.pm
@@ -0,0 +1,314 @@
+################################################################################
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
+# This program is free software; you can redistribute it and/or modify it under
+# the terms of either: (a) the GNU General Public License as published by the
+# Free Software Foundation; either version 2, or (at your option) any later
+# version, or (b) the "Artistic License" which comes with this package.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
+# Artistic License for more details.
+################################################################################
+
+=head1 NAME
+
+FormatRenderedProblem.pm
+
+=cut
+
+package WeBWorK::FormatRenderedProblem;
+
+use strict;
+use warnings;
+
+use JSON;
+use Digest::SHA qw(sha1_base64);
+use Mojo::Util qw(xml_escape);
+use Mojo::DOM;
+
+use WeBWorK::Localize;
+use WeBWorK::AttemptsTable;
+use WeBWorK::Utils qw(getAssetURL);
+use WeBWorK::Utils::LanguageAndDirection;
+
+sub formatRenderedProblem {
+ my $c = shift;
+ my $rh_result = shift;
+ my $inputs_ref = $rh_result->{inputs_ref};
+
+ my $renderErrorOccurred = 0;
+
+ my $problemText = $rh_result->{text} // '';
+ $problemText .= $rh_result->{flags}{comment} if ( $rh_result->{flags}{comment} && $inputs_ref->{showComments} );
+
+ if ($rh_result->{flags}{error_flag}) {
+ $rh_result->{problem_result}{score} = 0; # force score to 0 for such errors.
+ $renderErrorOccurred = 1;
+ }
+
+ my $SITE_URL = $ENV{baseURL};
+ my $FORM_ACTION_URL = $ENV{formURL};
+
+ my $displayMode = $inputs_ref->{displayMode} // 'MathJax';
+
+ # HTML document language setting
+ my $formLanguage = $inputs_ref->{language} // 'en';
+
+ # Third party CSS
+ # The second element of each array in the following is whether or not the file is a theme file.
+ # customize source for bootstrap.css
+ my @third_party_css = map { getAssetURL($formLanguage, $_->[0]) } (
+ [ 'css/bootstrap.css', ],
+ [ 'node_modules/jquery-ui-dist/jquery-ui.min.css', ],
+ [ 'node_modules/@fortawesome/fontawesome-free/css/all.min.css' ],
+ );
+
+ # Add CSS files requested by problems via ADD_CSS_FILE() in the PG file
+ # or via a setting of $ce->{pg}{specialPGEnvironmentVars}{extra_css_files}
+ # which can be set in course.conf (the value should be an anonomous array).
+ my @cssFiles;
+ # if (ref($ce->{pg}{specialPGEnvironmentVars}{extra_css_files}) eq 'ARRAY') {
+ # push(@cssFiles, { file => $_, external => 0 }) for @{ $ce->{pg}{specialPGEnvironmentVars}{extra_css_files} };
+ # }
+ if (ref($rh_result->{flags}{extra_css_files}) eq 'ARRAY') {
+ push @cssFiles, @{ $rh_result->{flags}{extra_css_files} };
+ }
+ my %cssFilesAdded; # Used to avoid duplicates
+ my @extra_css_files;
+ for (@cssFiles) {
+ next if $cssFilesAdded{ $_->{file} };
+ $cssFilesAdded{ $_->{file} } = 1;
+ if ($_->{external}) {
+ push(@extra_css_files, $_);
+ } else {
+ push(@extra_css_files, { file => getAssetURL($formLanguage, $_->{file}), external => 0 });
+ }
+ }
+
+ # Third party JavaScript
+ # The second element of each array in the following is whether or not the file is a theme file.
+ # The third element is a hash containing the necessary attributes for the script tag.
+ my @third_party_js = map { [ getAssetURL($formLanguage, $_->[0]), $_->[1] ] } (
+ [ 'node_modules/jquery/dist/jquery.min.js', {} ],
+ [ 'node_modules/jquery-ui-dist/jquery-ui.min.js', {} ],
+ [ 'node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js', {} ],
+ [ "js/apps/MathJaxConfig/mathjax-config.js", { defer => undef } ],
+ [ 'node_modules/mathjax/es5/tex-svg.js', { defer => undef, id => 'MathJax-script' } ],
+ [ 'node_modules/bootstrap/dist/js/bootstrap.bundle.min.js', { defer => undef } ],
+ [ "js/apps/Problem/problem.js", { defer => undef } ],
+ [ "js/apps/Problem/submithelper.js", { defer => undef } ],
+ [ "js/apps/CSSMessage/css-message.js", { defer => undef } ],
+ );
+
+ # Get the requested format. (outputFormat or outputformat)
+ # override to static mode if showCorrectAnswers has been set
+ my $formatName = $inputs_ref->{showCorrectAnswers}
+ ? 'static'
+ : $inputs_ref->{outputFormat} // $inputs_ref->{outputformat} // 'simple';
+
+ # Add JS files requested by problems via ADD_JS_FILE() in the PG file.
+ my @extra_js_files;
+ if (ref($rh_result->{flags}{extra_js_files}) eq 'ARRAY') {
+ my %jsFiles;
+ for (@{ $rh_result->{flags}{extra_js_files} }) {
+ next if $jsFiles{ $_->{file} };
+ $jsFiles{ $_->{file} } = 1;
+ my %attributes = ref($_->{attributes}) eq 'HASH' ? %{ $_->{attributes} } : ();
+ if ($_->{external}) {
+ push(@extra_js_files, $_);
+ } else {
+ push(@extra_js_files,
+ { file => getAssetURL($formLanguage, $_->{file}), external => 0, attributes => $_->{attributes} });
+ }
+ }
+ }
+
+ # Set up the problem language and direction
+ # PG files can request their language and text direction be set. If we do not have access to a default course
+ # language, fall back to the $formLanguage instead.
+ # TODO: support for right-to-left languages
+ my %PROBLEM_LANG_AND_DIR =
+ get_problem_lang_and_dir($rh_result->{flags}, 'auto:en:ltr', $formLanguage);
+ my $PROBLEM_LANG_AND_DIR = join(' ', map {qq{$_="$PROBLEM_LANG_AND_DIR{$_}"}} keys %PROBLEM_LANG_AND_DIR);
+
+ # is there a reason this doesn't use the same button IDs?
+ my $previewMode = defined($inputs_ref->{previewAnswers}) || 0;
+ my $submitMode = defined($inputs_ref->{submitAnswers}) || $inputs_ref->{answersSubmitted} || 0;
+ my $showCorrectMode = defined($inputs_ref->{showCorrectAnswers}) || 0;
+ # A problemUUID should be added to the request as a parameter. It is used by PG to create a proper UUID for use in
+ # aliases for resources. It should be unique for a course, user, set, problem, and version.
+ my $problemUUID = $inputs_ref->{problemUUID} // '';
+ my $problemResult = $rh_result->{problem_result} // {};
+ my $showSummary = $inputs_ref->{showSummary} // 1;
+ my $showAnswerNumbers = $inputs_ref->{showAnswerNumbers} // 0; # default no
+ # allow the request to hide the results table or messages
+ my $showTable = $inputs_ref->{hideAttemptsTable} ? 0 : 1;
+ my $showMessages = $inputs_ref->{hideMessages} ? 0 : 1;
+ # allow the request to override the display of partial correct answers
+ my $showPartialCorrectAnswers = $inputs_ref->{showPartialCorrectAnswers}
+ // $rh_result->{flags}{showPartialCorrectAnswers};
+
+ # Attempts table
+ my $answerTemplate = '';
+
+ # Do not produce an AttemptsTable when we had a rendering error.
+ if (!$renderErrorOccurred && $submitMode && $showTable) {
+ my $tbl = WeBWorK::AttemptsTable->new(
+ $rh_result->{answers} // {}, $c,
+ answersSubmitted => 1,
+ answerOrder => $rh_result->{flags}{ANSWER_ENTRY_ORDER} // [],
+ displayMode => $displayMode,
+ showAnswerNumbers => $showAnswerNumbers,
+ showAttemptAnswers => 0,
+ showAttemptPreviews => 1,
+ showAttemptResults => $showPartialCorrectAnswers,
+ showCorrectAnswers => $showCorrectMode,
+ showMessages => $showMessages,
+ showSummary => $showSummary,
+ mtRef => WeBWorK::Localize::getLoc($formLanguage),
+ summary => $problemResult->{summary} // '', # can be set by problem grader
+ );
+ $answerTemplate = $tbl->answerTemplate;
+ # $tbl->imgGen->render(refresh => 1) if $tbl->displayMode eq 'images';
+ }
+
+ # Answer hash in XML format used by the PTX format.
+ my $answerhashXML = '';
+ if ($formatName eq 'ptx') {
+ my $dom = Mojo::DOM->new->xml(1);
+ for my $answer (sort keys %{ $rh_result->{answers} }) {
+ $dom->append_content($dom->new_tag(
+ $answer,
+ map { $_ => ($rh_result->{answers}{$answer}{$_} // '') } keys %{ $rh_result->{answers}{$answer} }
+ ));
+ }
+ $dom->wrap_content(' ');
+ $answerhashXML = $dom->to_string;
+ }
+
+ # Make sure this is defined and is an array reference as saveGradeToLTI might add to it.
+ $rh_result->{debug_messages} = [] unless defined $rh_result && ref $rh_result->{debug_messages} eq 'ARRAY';
+
+ # Execute and return the interpolated problem template
+
+ # Raw format
+ # This format returns javascript object notation corresponding to the perl hash
+ # with everything that a client-side application could use to work with the problem.
+ # There is no wrapping HTML "_format" template.
+ if ($formatName eq 'raw') {
+ my $output = {};
+
+ # Everything that ships out with other formats can be constructed from these
+ $output->{rh_result} = $rh_result;
+ $output->{inputs_ref} = $inputs_ref;
+ # $output->{input} = $ws->{input};
+
+ # The following could be constructed from the above, but this is a convenience
+ $output->{answerTemplate} = $answerTemplate if ($answerTemplate);
+ $output->{lang} = $PROBLEM_LANG_AND_DIR{lang};
+ $output->{dir} = $PROBLEM_LANG_AND_DIR{dir};
+ $output->{extra_css_files} = \@extra_css_files;
+ $output->{extra_js_files} = \@extra_js_files;
+
+ # Include third party css and javascript files. Only jquery, jquery-ui, mathjax, and bootstrap are needed for
+ # PG. See the comments before the subroutine definitions for load_css and load_js in pg/macros/PG.pl.
+ # The other files included are only needed to make themes work in the webwork2 formats.
+ $output->{third_party_css} = \@third_party_css;
+ $output->{third_party_js} = \@third_party_js;
+
+ # Say what version of WeBWorK this is
+ # $output->{ww_version} = $ce->{WW_VERSION};
+ # $output->{pg_version} = $ce->{PG_VERSION};
+
+ # Convert to JSON and render.
+ return $c->render(data => JSON->new->utf8(1)->encode($output));
+ }
+
+ # Setup and render the appropriate template in the templates/RPCRenderFormats folder depending on the outputformat.
+ # "ptx" has a special template. "json" uses the default json template. All others use the default html template.
+ my %template_params = (
+ template => $formatName eq 'ptx' ? 'RPCRenderFormats/ptx' : 'RPCRenderFormats/default',
+ $formatName eq 'json' ? (format => 'json') : (),
+ formatName => $formatName,
+ lh => WeBWorK::Localize::getLangHandle($inputs_ref->{language} // 'en'),
+ rh_result => $rh_result,
+ SITE_URL => $SITE_URL,
+ FORM_ACTION_URL => $FORM_ACTION_URL,
+ COURSE_LANG_AND_DIR => get_lang_and_dir($formLanguage),
+ PROBLEM_LANG_AND_DIR => $PROBLEM_LANG_AND_DIR,
+ third_party_css => \@third_party_css,
+ extra_css_files => \@extra_css_files,
+ third_party_js => \@third_party_js,
+ extra_js_files => \@extra_js_files,
+ problemText => $problemText,
+ extra_header_text => $inputs_ref->{extra_header_text} // '',
+ answerTemplate => $answerTemplate,
+ showScoreSummary => $submitMode && !$renderErrorOccurred && $problemResult,
+ answerhashXML => $answerhashXML,
+ showPreviewButton => $inputs_ref->{showPreviewButton} // '',
+ showCheckAnswersButton => $inputs_ref->{showCheckAnswersButton} // '',
+ showCorrectAnswersButton => $inputs_ref->{showCorrectAnswersButton} // '0',
+ showFooter => $inputs_ref->{showFooter} // '',
+ pretty_print => \&pretty_print,
+ );
+
+ return $c->render(%template_params) if $formatName eq 'json' && !$inputs_ref->{send_pg_flags};
+ $rh_result->{renderedHTML} = $c->render_to_string(%template_params)->to_string;
+ return $c->respond_to(
+ html => { text => $rh_result->{renderedHTML} },
+ json => { json => $rh_result });
+}
+
+# Nice output for debugging
+sub pretty_print {
+ my ($r_input, $level) = @_;
+ $level //= 4;
+ $level--;
+ return '' unless $level > 0; # Only print three levels of hashes (safety feature)
+ my $out = '';
+ if (!ref $r_input) {
+ $out = $r_input if defined $r_input;
+ $out =~ s/</g; # protect for HTML output
+ } elsif (eval { %$r_input && 1 }) {
+ # eval { %$r_input && 1 } will pick up all objectes that can be accessed like a hash and so works better than
+ # "ref $r_input". Do not use "$r_input" =~ /hash/i" because that will pick up strings containing the word hash,
+ # and that will cause an error below.
+ local $^W = 0;
+ $out .= qq{$r_input };
+
+ for my $key (sort keys %$r_input) {
+ # Safety feature - we do not want to display the contents of %seed_ce which
+ # contains the database password and lots of other things, and explicitly hide
+ # certain internals of the CourseEnvironment in case one slips in.
+ next
+ if (($key =~ /database/)
+ || ($key =~ /dbLayout/)
+ || ($key eq "ConfigValues")
+ || ($key eq "ENV")
+ || ($key eq "externalPrograms")
+ || ($key eq "permissionLevels")
+ || ($key eq "seed_ce"));
+ $out .= "$key => " . pretty_print($r_input->{$key}, $level) . " ";
+ }
+ $out .= '
';
+ } elsif (ref $r_input eq 'ARRAY') {
+ my @array = @$r_input;
+ $out .= '( ';
+ while (@array) {
+ $out .= pretty_print(shift @array, $level) . ' , ';
+ }
+ $out .= ' )';
+ } elsif (ref $r_input eq 'CODE') {
+ $out = "$r_input";
+ } else {
+ $out = $r_input;
+ $out =~ s/</g; # Protect for HTML output
+ }
+
+ return $out . ' ';
+}
+
+1;
diff --git a/lib/RenderApp/Controller/RenderProblem.pm b/lib/WeBWorK/RenderProblem.pm
similarity index 84%
rename from lib/RenderApp/Controller/RenderProblem.pm
rename to lib/WeBWorK/RenderProblem.pm
index 61fcaba72..bbb4cf186 100644
--- a/lib/RenderApp/Controller/RenderProblem.pm
+++ b/lib/WeBWorK/RenderProblem.pm
@@ -1,4 +1,4 @@
-package RenderApp::Controller::RenderProblem;
+package WeBWorK::RenderProblem;
use strict;
use warnings;
@@ -90,7 +90,7 @@ sub process_pg_file {
my $pg_start = time;
my $memory_use_start = get_current_process_memory();
- my ( $error_flag, $formatter, $error_string ) =
+ my ( $return_object, $error_flag, $error_string ) =
process_problem( $problem, $inputs_ref );
my $pg_stop = time;
@@ -105,47 +105,48 @@ sub process_pg_file {
);
# format result
- my $html = $formatter->formatRenderedProblem;
- my $pg_obj = $formatter->{return_object};
- my $json_rh = {
- renderedHTML => $html,
- answers => $pg_obj->{answers},
- debug => {
- perl_warn => $pg_obj->{WARNINGS},
- pg_warn => $pg_obj->{warning_messages},
- debug => $pg_obj->{debug_messages},
- internal => $pg_obj->{internal_debug_messages}
- },
- problem_result => $pg_obj->{problem_result},
- problem_state => $pg_obj->{problem_state},
- flags => $pg_obj->{flags},
- resources => {
- regex => $pg_obj->{pgResources},
- alias => $pg_obj->{resources},
- js => $pg_obj->{js},
- css => $pg_obj->{css},
- },
- form_data => $inputs_ref,
- raw_metadata_text => $pg_obj->{raw_metadata_text},
- JWT => {
- problem => $inputs_ref->{problemJWT},
- session => $pg_obj->{sessionJWT},
- answer => $pg_obj->{answerJWT}
- },
- };
+ # my $html = $formatter->formatRenderedProblem;
+ # my $pg_obj = $formatter->{return_object};
+ # my $json_rh = {
+ # renderedHTML => $html,
+ # answers => $pg_obj->{answers},
+ # debug => {
+ # perl_warn => $pg_obj->{WARNINGS},
+ # pg_warn => $pg_obj->{warning_messages},
+ # debug => $pg_obj->{debug_messages},
+ # internal => $pg_obj->{internal_debug_messages}
+ # },
+ # problem_result => $pg_obj->{problem_result},
+ # problem_state => $pg_obj->{problem_state},
+ # flags => $pg_obj->{flags},
+ # resources => {
+ # regex => $pg_obj->{pgResources},
+ # alias => $pg_obj->{resources},
+ # js => $pg_obj->{js},
+ # css => $pg_obj->{css},
+ # },
+ # form_data => $inputs_ref,
+ # raw_metadata_text => $pg_obj->{raw_metadata_text},
+ # JWT => {
+ # problem => $inputs_ref->{problemJWT},
+ # session => $pg_obj->{sessionJWT},
+ # answer => $pg_obj->{answerJWT}
+ # },
+ # };
# havoc caused by problemRandomize.pl inserting CODE ref into pg->{flags}
# HACK: remove flags->{problemRandomize} if it exists -- cannot include CODE refs
- delete $json_rh->{flags}{problemRandomize}
- if $json_rh->{flags}{problemRandomize};
+ delete $return_object->{flags}{problemRandomize}
+ if $return_object->{flags}{problemRandomize};
# similar things happen with compoundProblem -- delete CODE refs
- delete $json_rh->{flags}{compoundProblem}{grader}
- if $json_rh->{flags}{compoundProblem}{grader};
+ delete $return_object->{flags}{compoundProblem}{grader}
+ if $return_object->{flags}{compoundProblem}{grader};
- $json_rh->{tags} = WeBWorK::Utils::Tags->new('', $problem->source) if ( $inputs_ref->{includeTags} );
+ $return_object->{tags} = WeBWorK::Utils::Tags->new('', $problem->source) if ( $inputs_ref->{includeTags} );
+ $return_object->{inputs_ref} = $inputs_ref;
my $coder = JSON::XS->new->ascii->pretty->allow_unknown->convert_blessed;
- my $json = $coder->encode($json_rh);
+ my $json = $coder->encode($return_object);
return $json;
}
@@ -212,21 +213,21 @@ sub process_problem {
# Create FormatRenderedProblems object
##################################################
- my $formatter = RenderApp::Controller::FormatRenderedProblem->new(
- return_object => $return_object,
- sourceFilePath => $inputs_ref->{sourceFilePath},
- url => $inputs_ref->{baseURL},
- form_action_url => $inputs_ref->{formURL},
- maketext => sub {return @_},
- inputs_ref => $inputs_ref,
- problem_seed => $inputs_ref->{problemSeed},
- );
+ # my $formatter = RenderApp::Controller::FormatRenderedProblem->new(
+ # return_object => $return_object,
+ # sourceFilePath => $inputs_ref->{sourceFilePath},
+ # url => $inputs_ref->{baseURL},
+ # form_action_url => $inputs_ref->{formURL},
+ # maketext => sub {return @_},
+ # inputs_ref => $inputs_ref,
+ # problem_seed => $inputs_ref->{problemSeed},
+ # );
#######################################################################
# End processing of the pg file
#######################################################################
- return $error_flag, $formatter, $error_string;
+ return $return_object, $error_flag, $error_string;
}
###########################################
@@ -254,7 +255,7 @@ sub standaloneRenderer {
showSolutions => $inputs_ref->{showSolutions},
problemNumber => $inputs_ref->{problemNumber}, # ever even relevant?
num_of_correct_ans => $inputs_ref->{numCorrect} || 0,
- num_of_incorrect_ans => $inputs_ref->{numIncorrect} // 1000,
+ num_of_incorrect_ans => $inputs_ref->{numIncorrect} || 0,
displayMode => $inputs_ref->{displayMode},
useMathQuill => !defined $inputs_ref->{entryAssist} || $inputs_ref->{entryAssist} eq 'MathQuill',
answerPrefix => $inputs_ref->{answerPrefix},
@@ -293,8 +294,8 @@ sub standaloneRenderer {
post_header_text => $pg->{post_header_text},
answers => $pg->{answers},
errors => $pg->{errors},
- WARNINGS => $pg->{warnings},
- PG_ANSWERS_HASH => $pg->{pgcore}->{PG_ANSWERS_HASH},
+ pg_warnings => $pg->{warnings},
+ # PG_ANSWERS_HASH => $pg->{pgcore}->{PG_ANSWERS_HASH},
problem_result => $pg->{result},
problem_state => $pg->{state},
flags => $pg->{flags},
@@ -332,10 +333,10 @@ sub generateJWTs {
# my %correctKeys = qw(correct_value value correct_formula formula correct_ans ans);
# my %messageKeys = qw(ans_message answer error_message error);
# my @resultKeys = qw(score weight);
- my %answers = %{unbless($pg->{answers})};
+ my %answers = %{unbless($pg->{answers})};
# once the correct answers are shown, this setting is permanent
- $sessionHash->{showCorrectAnswers} = 1 if $inputs_ref->{showCorrectAnswers};
+ $sessionHash->{showCorrectAnswers} = 1 if $inputs_ref->{showCorrectAnswers};
# store the current answer/response state for each entry
foreach my $ans (keys %{$pg->{answers}}) {
@@ -351,7 +352,7 @@ sub generateJWTs {
# $scoreHash->{$ans}{message} = { map {exists $answers{$ans}{$_} ? ($messageKeys{$_} => $answers{$ans}{$_}) : ()} keys %messageKeys };
# $scoreHash->{$ans}{result} = { map {exists $answers{$ans}{$_} ? ($_ => $answers{$ans}{$_}) : ()} @resultKeys };
}
- $scoreHash->{answers} = unbless($pg->{answers});
+ $scoreHash->{answers} = unbless($pg->{answers});
# update the number of correct/incorrect submissions if answers were 'submitted'
# but don't update either if the problem was already correct
@@ -379,8 +380,6 @@ sub generateJWTs {
# Can instead use alg => 'PBES2-HS512+A256KW', enc => 'A256GCM' for JWE
my $answerJWT = encode_jwt(payload=>$responseHash, alg => 'HS256', key => $ENV{problemJWTsecret}, auto_iat => 1);
- warn("answerJWT claims: ".encode_json($scoreHash));
-
return ($sessionJWT, $answerJWT);
}
diff --git a/lib/WeBWorK/Utils.pm b/lib/WeBWorK/Utils.pm
index 095e34a5e..ef7e07c7c 100644
--- a/lib/WeBWorK/Utils.pm
+++ b/lib/WeBWorK/Utils.pm
@@ -35,50 +35,120 @@ sub wwRound(@) {
return int($float * $factor + 0.5) / $factor;
}
+my $staticWWAssets;
my $staticPGAssets;
+my $thirdPartyWWDependencies;
+my $thirdPartyPGDependencies;
+
+sub readJSON {
+ my $fileName = shift;
+
+ return unless -r $fileName;
+
+ open(my $fh, "<:encoding(UTF-8)", $fileName) or die "FATAL: Unable to open '$fileName'!";
+ local $/;
+ my $data = <$fh>;
+ close $fh;
+
+ return JSON->new->decode($data);
+}
+
+sub getThirdPartyAssetURL {
+ my ($file, $dependencies, $baseURL, $useCDN) = @_;
+
+ for (keys %$dependencies) {
+ if ($file =~ /^node_modules\/$_\/(.*)$/) {
+ if ($useCDN && $1 !~ /mathquill/) {
+ return
+ "https://cdn.jsdelivr.net/npm/$_\@"
+ . substr($dependencies->{$_}, 1) . '/'
+ . ($1 =~ s/(?:\.min)?\.(js|css)$/.min.$1/gr);
+ } else {
+ return "$baseURL/$file?version=" . ($dependencies->{$_} =~ s/#/@/gr);
+ }
+ }
+ }
+ return;
+}
# Get the url for static assets.
sub getAssetURL {
- my ($language, $file, $isThemeFile) = @_;
+ my ($language, $file) = @_;
# Load the static files list generated by `npm install` the first time this method is called.
- if (!$staticPGAssets) {
+ unless ($staticWWAssets) {
+ my $staticAssetsList = "$ENV{RENDER_ROOT}/public/static-assets.json";
+ $staticWWAssets = readJSON($staticAssetsList);
+ unless ($staticWWAssets) {
+ warn "ERROR: '$staticAssetsList' not found or not readable!\n"
+ . "You may need to run 'npm install' from '$ENV{RENDER_ROOT}/public'.";
+ $staticWWAssets = {};
+ }
+ }
+
+ unless ($staticPGAssets) {
my $staticAssetsList = "$ENV{PG_ROOT}/htdocs/static-assets.json";
- if (-r $staticAssetsList) {
- my $data = do {
- open(my $fh, "<:encoding(UTF-8)", $staticAssetsList)
- or die "FATAL: Unable to open '$staticAssetsList'!";
- local $/;
- <$fh>;
- };
-
- $staticPGAssets = JSON->new->decode($data);
- } else {
- warn "ERROR: '$staticAssetsList' not found!\n"
+ $staticPGAssets = readJSON($staticAssetsList);
+ unless ($staticPGAssets) {
+ warn "ERROR: '$staticAssetsList' not found or not readable!\n"
. "You may need to run 'npm install' from '$ENV{PG_ROOT}/htdocs'.";
+ $staticPGAssets = {};
}
}
+ unless ($thirdPartyWWDependencies) {
+ my $packageJSON = "$ENV{RENDER_ROOT}/public/package.json";
+ my $data = readJSON($packageJSON);
+ warn "ERROR: '$packageJSON' not found or not readable!\n" unless $data && defined $data->{dependencies};
+ $thirdPartyWWDependencies = $data->{dependencies} // {};
+ }
+
+ unless ($thirdPartyPGDependencies) {
+ my $packageJSON = "$ENV{PG_ROOT}/htdocs/package.json";
+ my $data = readJSON($packageJSON);
+ warn "ERROR: '$packageJSON' not found or not readable!\n" unless $data && defined $data->{dependencies};
+ $thirdPartyPGDependencies = $data->{dependencies} // {};
+ }
+
+ # Check to see if this is a third party asset file in node_modules (either in webwork2/htdocs or pg/htdocs).
+ # If so, then either serve it from a CDN if requested, or serve it directly with the library version
+ # appended as a URL parameter.
+ if ($file =~ /^node_modules/) {
+ my $wwFile = getThirdPartyAssetURL(
+ $file, $thirdPartyWWDependencies,
+ '',
+ 0
+ );
+ return $wwFile if $wwFile;
+
+ my $pgFile =
+ getThirdPartyAssetURL($file, $thirdPartyPGDependencies, '/pg_files', 1);
+ return $pgFile if $pgFile;
+ }
+
# If a right-to-left language is enabled (Hebrew or Arabic) and this is a css file that is not a third party asset,
# then determine the rtl varaint file name. This will be looked for first in the asset lists.
- my $rtlfile = $file =~ s/\.css$/.rtl.css/r
- if ($language =~ /^(he|ar)/ && $file !~ /node_modules/ && $file =~ /\.css$/);
+ my $rtlfile =
+ ($language =~ /^(he|ar)/ && $file !~ /node_modules/ && $file =~ /\.css$/)
+ ? $file =~ s/\.css$/.rtl.css/r
+ : undef;
+
+ # First check to see if this is a file in the webwork htdocs location with a rtl variant.
+ return "/$staticWWAssets->{$rtlfile}"
+ if defined $rtlfile && defined $staticWWAssets->{$rtlfile};
+
+ # Next check to see if this is a file in the webwork htdocs location.
+ return "/$staticWWAssets->{$file}" if defined $staticWWAssets->{$file};
# Now check to see if this is a file in the pg htdocs location with a rtl variant.
- # These also can only be local files.
return "/pg_files/$staticPGAssets->{$rtlfile}" if defined $rtlfile && defined $staticPGAssets->{$rtlfile};
# Next check to see if this is a file in the pg htdocs location.
- if (defined $staticPGAssets->{$file}) {
- # File served by cdn.
- return $staticPGAssets->{$file} if $staticPGAssets->{$file} =~ /^https?:\/\//;
- # File served locally.
- return "/pg_files/$staticPGAssets->{$file}";
- }
+ return "/pg_files/$staticPGAssets->{$file}" if defined $staticPGAssets->{$file};
- # If the file was not found in the lists, then just use the given file and assume its path is relative to the pg
- # htdocs location.
- return "/pg_files/$file";
+ # If the file was not found in the lists, then just use the given file and assume its path is relative to the
+ # render app public folder.
+ return "/$file";
}
1;
diff --git a/lib/WeBWorK/Utils/AttemptsTable.pm b/lib/WeBWorK/Utils/AttemptsTable.pm
deleted file mode 100644
index 344d7e6a8..000000000
--- a/lib/WeBWorK/Utils/AttemptsTable.pm
+++ /dev/null
@@ -1,455 +0,0 @@
-#!/usr/bin/perl -w
-use 5.010;
-
-################################################################################
-# WeBWorK Online Homework Delivery System
-# 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
-# Artistic License for more details.
-################################################################################
-
-=head1 NAME
-
- AttemptsTable
-
-=head1 SYNPOSIS
-
- my $tbl = WeBWorK::Utils::AttemptsTable->new(
- $answers,
- answersSubmitted => 1,
- answerOrder => $pg->{flags}->{ANSWER_ENTRY_ORDER},
- displayMode => 'MathJax',
- showAnswerNumbers => 0,
- showAttemptAnswers => $showAttemptAnswers && $showEvaluatedAnswers,
- showAttemptPreviews => $showAttemptPreview,
- showAttemptResults => $showAttemptResults,
- showCorrectAnswers => $showCorrectAnswers,
- showMessages => $showAttemptAnswers, # internally checks for messages
- showSummary => $showSummary,
- imgGen => $imgGen, # not needed if ce is present ,
- ce => '', # not needed if $imgGen is present
- maketext => WeBWorK::Localize::getLoc("en"),
- );
- $tbl->{imgGen}->render(refresh => 1) if $tbl->displayMode eq 'images';
- my $answerTemplate = $tbl->answerTemplate;
- # this also collects the correct_ids and incorrect_ids
- $self->{correct_ids} = $tbl->correct_ids;
- $self->{incorrect_ids} = $tbl->incorrect_ids;
-
-
-=head1 DESCRIPTION
-This module handles the formatting of the table which presents the results of analyzing a student's
-answer to a WeBWorK problem. It is used in Problem.pm, OpaqueServer.pm, standAlonePGproblemRender
-
-=head2 new
-
- my $tbl = WeBWorK::Utils::AttemptsTable->new(
- $answers,
- answersSubmitted => 1,
- answerOrder => $pg->{flags}->{ANSWER_ENTRY_ORDER},
- displayMode => 'MathJax',
- showHeadline => 1,
- showAnswerNumbers => 0,
- showAttemptAnswers => $showAttemptAnswers && $showEvaluatedAnswers,
- showAttemptPreviews => $showAttemptPreview,
- showAttemptResults => $showAttemptResults,
- showCorrectAnswers => $showCorrectAnswers,
- showMessages => $showAttemptAnswers, # internally checks for messages
- showSummary => $showSummary,
- imgGen => $imgGen, # not needed if ce is present ,
- ce => '', # not needed if $imgGen is present
- maketext => WeBWorK::Localize::getLoc("en"),
- summary =>'',
- );
-
- $answers -- a hash of student answers e.g. $pg->{answers}
- answersSubmitted if 0 then then the attemptsTable is not displayed (???)
- answerOrder -- an array indicating the order the answers appear on the page.
- displayMode 'MathJax' and 'images' are the most common
-
- showHeadline Show the header line 'Results for this submission'
-
- showAnswerNumbers, showAttemptAnswers, showAttemptPreviews,showAttemptResults,
- showCorrectAnswers and showMessages control the display of each column in the table.
-
- attemptAnswers the student's typed in answer (possibly simplified numerically)
- attemptPreview the student's answer after typesetting
- attemptResults "correct", "_% correct", "incorrect" or "ungraded"- links to the answer blank
- correctAnswers typeset version (untypeset versions are available via popups)
- messages warns of formatting typos in the answer, or
- more detailed messages about a wrong answer
- summary is obtained from $pg->{result}->{summary}.
- If this is empty then a (localized)
- version of "all answers are correct"
- or "at least one answer is not coorrect"
- imgGen points to a prebuilt image generator objectfor "images" mode
- ce points to the CourseEnvironment -- it is needed if AttemptsTable
- is required to build its own imgGen object
- maketext points to a localization subroutine
-
-
-
-
-=head2 Methods
-
-=over 4
-
-=item answerTemplate
-
-Returns HTML which formats the analysis of the student's answers to the problem.
-
-=back
-
-=head2 Read/Write Properties
-
-=over 4
-
-=item correct_ids, incorrect_ids,
-
-These are references to lists of the ids of the correct answers and the incorrect answers respectively.
-
-=item showMessages,
-
-This can be switched on or off before exporting the answerTemplate, perhaps under instructions
- from the PG problem.
-
-=item summary
-
-The contents of the summary can be defined when the attemptsTable object is created.
-
-The summary can be defined by the PG problem grader
-usually returned as $pg->{result}->{summary}.
-
-If the summary is not explicitly defined then (localized) versions
-of the default summaries are created:
-
- "The answer above is correct.",
- "Some answers will be graded later.",
- "All of the [gradeable] answers above are correct.",
- "[N] of the questions remain unanswered.",
- "At least one of the answers above is NOT [fully] correct.',
-
-=back
-
-=cut
-
-package WeBWorK::Utils::AttemptsTable;
-use base qw(Class::Accessor);
-
-use strict;
-use warnings;
-
-use Scalar::Util 'blessed';
-use WeBWorK::Utils 'wwRound';
-use WeBWorK::PG::Environment;
-use CGI;
-
-# Object contains hash of answer results
-# Object contains display mode
-# Object contains or creates Image generator
-# object returns table
-
-sub new {
- my $class = shift;
- $class = (ref($class))? ref($class) : $class; # create a new object of the same class
- my $rh_answers = shift;
- ref($rh_answers) =~/HASH/ or die "The first entry to AttemptsTable must be a hash of answers";
- my %options = @_; # optional: displayMode=>, submitted=>, imgGen=>, ce=>
- my $self = {
- answers => $rh_answers // {},
- answerOrder => $options{answerOrder} // [],
- answersSubmitted => $options{answersSubmitted} // 0,
- summary => $options{summary} // '', # summary provided by problem grader
- displayMode => $options{displayMode} || "MathJax",
- showHeadline => $options{showHeadline} // 1,
- showAnswerNumbers => $options{showAnswerNumbers} // 1,
- showAttemptAnswers => $options{showAttemptAnswers} // 1, # show student answer as entered and simplified
- # (e.g numerical formulas are calculated to produce numbers)
- showAttemptPreviews => $options{showAttemptPreviews} // 1, # show preview of student answer
- showAttemptResults => $options{showAttemptResults} // 1, # show whether student answer is correct
- showMessages => $options{showMessages} // 1, # show any messages generated by evaluation
- showCorrectAnswers => $options{showCorrectAnswers} // 1, # show the correct answers
- showSummary => $options{showSummary} // 1, # show summary to students
- maketext => $options{maketext} // sub {return @_}, # pointer to the maketext subroutine
- imgGen => undef, # created in _init method
- };
- bless $self, $class;
- # create read only accessors/mutators
- $self->mk_ro_accessors(qw(answers answerOrder answersSubmitted displayMode imgGen maketext));
- $self->mk_ro_accessors(qw(showAnswerNumbers showAttemptAnswers showHeadline
- showAttemptPreviews showAttemptResults
- showCorrectAnswers showSummary));
- $self->mk_accessors(qw(correct_ids incorrect_ids showMessages summary));
- # sanity check and initialize imgGenerator.
- _init($self, %options);
- return $self;
-}
-
-sub _init {
- # verify display mode
- # build imgGen
- my $self = shift;
- my %options = @_;
- $self->{submitted}=$options{submitted}//0;
- $self->{displayMode} = $options{displayMode} || "MathJax";
- # only show message column if there is at least one message:
- my @reallyShowMessages = grep { $self->answers->{$_}->{ans_message} } @{$self->answerOrder};
- $self->showMessages( $self->showMessages && !!@reallyShowMessages );
- # (!! forces boolean scalar environment on list)
- # only used internally -- don't need accessors.
- $self->{numCorrect}=0;
- $self->{numBlanks}=0;
- $self->{numEssay}=0;
-
- if ($self->displayMode eq 'images') {
- my $pg_envir = WeBWorK::PG::Environment->new;
-
- $self->{imgGen} = WeBWorK::PG::ImageGenerator->new(
- tempDir => $pg_envir->{directories}{tmp},
- latex => $pg_envir->{externalPrograms}{latex},
- dvipng => $pg_envir->{externalPrograms}{dvipng},
- useCache => 1,
- cacheDir => $pg_envir->{directories}{equationCache},
- cacheURL => $pg_envir->{URLs}{equationCache},
- cacheDB => $pg_envir->{equationCacheDB},
- useMarkers => 1,
- dvipng_align => $pg_envir->{displayModeOptions}{images}{dvipng_align},
- dvipng_depth_db => $pg_envir->{displayModeOptions}{images}{dvipng_depth_db},
- );
- }
-}
-
-sub maketext {
- my $self = shift;
-# Uncomment to check that strings are run through maketext
-# return 'xXx'.&{$self->{maketext}}(@_).'xXx';
- return &{$self->{maketext}}(@_);
-}
-sub formatAnswerRow {
- my $self = shift;
- my $rh_answer = shift;
- my $ans_id = shift;
- my $answerNumber = shift;
- my $answerString = $rh_answer->{student_ans}//'';
- # use student_ans and not original_student_ans above. student_ans has had HTML entities translated to prevent XSS.
- my $answerPreview = $self->previewAnswer($rh_answer)//' ';
- my $correctAnswer = $rh_answer->{correct_ans}//'';
- my $correctAnswerPreview = $self->previewCorrectAnswer($rh_answer)//' ';
-
- my $answerMessage = $rh_answer->{ans_message}//'';
- $answerMessage =~ s/\n/ /g;
- my $answerScore = $rh_answer->{score}//0;
- $self->{numCorrect} += $answerScore >=1;
- $self->{numEssay} += ($rh_answer->{type}//'') eq 'essay';
- $self->{numBlanks}++ unless $answerString =~/\S/ || $answerScore >= 1;
-
- my $feedbackMessageClass = ($answerMessage eq "") ? "" : $self->maketext("FeedbackMessage");
-
- my (@correct_ids, @incorrect_ids);
- my $resultString;
- my $resultStringClass;
- if ($answerScore >= 1) {
- $resultString = $self->maketext("correct");
- $resultStringClass = "ResultsWithoutError";
- } elsif (($rh_answer->{type} // '') eq 'essay') {
- $resultString = $self->maketext("Ungraded");
- $self->{essayFlag} = 1;
- } elsif (defined($answerScore) and $answerScore == 0) {
- $resultStringClass = "ResultsWithError";
- $resultString = $self->maketext("incorrect");
- } else {
- $resultString = $self->maketext("[_1]% correct", wwRound(0, $answerScore * 100));
- }
- my $attemptResults = CGI::td({ class => $resultStringClass },
- CGI::a({ href => '#', data_answer_id => $ans_id }, $self->nbsp($resultString)));
-
- my $row = join('',
- ($self->showAnswerNumbers) ? CGI::td({},$answerNumber):'',
- ($self->showAttemptAnswers) ? CGI::td({dir=>"auto"},$self->nbsp($answerString)):'' , # student original answer
- ($self->showAttemptPreviews)? $self->formatToolTip($answerString, $answerPreview):"" ,
- ($self->showAttemptResults)? $attemptResults : '' ,
- ($self->showCorrectAnswers)? $self->formatToolTip($correctAnswer,$correctAnswerPreview):"" ,
- ($self->showMessages)? CGI::td({class=>$feedbackMessageClass},$self->nbsp($answerMessage)):"",
- "\n"
- );
- $row;
-}
-
-#####################################################
-# determine whether any answers were submitted
-# and create answer template if they have been
-#####################################################
-
-sub answerTemplate {
- my $self = shift;
- my $rh_answers = $self->{answers};
- my @tableRows;
- my @correct_ids;
- my @incorrect_ids;
-
- push @tableRows,CGI::Tr(
- ($self->showAnswerNumbers) ? CGI::th("#"):'',
- ($self->showAttemptAnswers)? CGI::th($self->maketext("Entered")):'', # student original answer
- ($self->showAttemptPreviews)? CGI::th($self->maketext("Answer Preview")):'',
- ($self->showAttemptResults)? CGI::th($self->maketext("Result")):'',
- ($self->showCorrectAnswers)? CGI::th($self->maketext("Correct Answer")):'',
- ($self->showMessages)? CGI::th($self->maketext("Message")):'',
- );
-
- my $answerNumber = 1;
- foreach my $ans_id (@{ $self->answerOrder() }) {
- push @tableRows, CGI::Tr($self->formatAnswerRow($rh_answers->{$ans_id}, $ans_id, $answerNumber++));
- push @correct_ids, $ans_id if ($rh_answers->{$ans_id}->{score}//0) >= 1;
- push @incorrect_ids, $ans_id if ($rh_answers->{$ans_id}->{score}//0) < 1;
- #$self->{essayFlag} = 1;
- }
- my $answerTemplate = "";
- $answerTemplate .= CGI::h3({ class => 'attemptResultsHeader' }, $self->maketext("Results for this submission"))
- if $self->showHeadline;
- $answerTemplate .= CGI::table({ class => 'attemptResults table table-sm table-bordered' }, @tableRows);
- ### "results for this submission" is better than "attempt results" for a headline
- $answerTemplate .= ($self->showSummary)? $self->createSummary() : '';
- $answerTemplate = "" unless $self->answersSubmitted; # only print if there is at least one non-blank answer
- $self->correct_ids(\@correct_ids);
- $self->incorrect_ids(\@incorrect_ids);
- $answerTemplate;
-}
-#################################################
-
-sub previewAnswer {
- my $self =shift;
- my $answerResult = shift;
- my $displayMode = $self->displayMode;
- my $imgGen = $self->imgGen;
-
- # note: right now, we have to do things completely differently when we are
- # rendering math from INSIDE the translator and from OUTSIDE the translator.
- # so we'll just deal with each case explicitly here. there's some code
- # duplication that can be dealt with later by abstracting out dvipng/etc.
-
- my $tex = $answerResult->{preview_latex_string};
-
- return "" unless defined $tex and $tex ne "";
-
- return $tex if $answerResult->{non_tex_preview};
-
- if ($displayMode eq "plainText") {
- return $tex;
- } elsif (($answerResult->{type}//'') eq 'essay') {
- return $tex;
- } elsif ($displayMode eq "images") {
- $imgGen->add($tex);
- } elsif ($displayMode eq "MathJax") {
- return '';
- }
-}
-
-sub previewCorrectAnswer {
- my $self =shift;
- my $answerResult = shift;
- my $displayMode = $self->displayMode;
- my $imgGen = $self->imgGen;
-
- my $tex = $answerResult->{correct_ans_latex_string};
- return $answerResult->{correct_ans} unless defined $tex and $tex=~/\S/; # some answers don't have latex strings defined
- # return "" unless defined $tex and $tex ne "";
-
- return $tex if $answerResult->{non_tex_preview};
-
- if ($displayMode eq "plainText") {
- return $tex;
- } elsif ($displayMode eq "images") {
- $imgGen->add($tex);
- # warn "adding $tex";
- } elsif ($displayMode eq "MathJax") {
- return '';
- }
-}
-
-###########################################
-# Create summary
-###########################################
-sub createSummary {
- my $self = shift;
- my $summary = "";
- my $numCorrect = $self->{numCorrect};
- my $numBlanks = $self->{numBlanks};
- my $numEssay = $self->{numEssay};
-
- unless (defined($self->summary) and $self->summary =~ /\S/) {
- my @answerNames = @{ $self->answerOrder() };
- if (scalar @answerNames == 1) { #default messages
- if ($numCorrect == scalar @answerNames) {
- $summary .=
- CGI::div({ class => 'ResultsWithoutError mb-2' }, $self->maketext('The answer above is correct.'));
- } elsif ($self->{essayFlag}) {
- $summary .= CGI::div($self->maketext('Some answers will be graded later.'));
- } else {
- $summary .=
- CGI::div({ class => 'ResultsWithError mb-2' }, $self->maketext('The answer above is NOT correct.'));
- }
- } else {
- if ($numCorrect + $numEssay == scalar @answerNames) {
- if ($numEssay) {
- $summary .= CGI::div({ class => 'ResultsWithoutError mb-2' },
- $self->maketext('All of the gradeable answers above are correct.'));
- } else {
- $summary .= CGI::div({ class => 'ResultsWithoutError mb-2' },
- $self->maketext('All of the answers above are correct.'));
- }
- } elsif ($numBlanks + $numEssay != scalar(@answerNames)) {
- $summary .= CGI::div({ class => 'ResultsWithError mb-2' },
- $self->maketext('At least one of the answers above is NOT correct.'));
- }
- if ($numBlanks > $numEssay) {
- my $s = ($numBlanks > 1) ? '' : 's';
- $summary .= CGI::div(
- { class => 'ResultsAlert mb-2' },
- $self->maketext(
- '[quant,_1,of the questions remains,of the questions remain] unanswered.', $numBlanks
- )
- );
- }
- }
- } else {
- $summary = $self->summary; # summary has been defined by grader
- }
- $summary = CGI::div({role=>"alert", class=>"attemptResultsSummary"},
- $summary);
- $self->summary($summary);
- return $summary; # return formatted version of summary in class "attemptResultsSummary" div
-}
-################################################
-
-############################################
-# utility subroutine -- prevents unwanted line breaks
-############################################
-sub nbsp {
- my ($self, $str) = @_;
- return (defined $str && $str =~/\S/) ? $str : " ";
-}
-
-# note that formatToolTip output includes CGI::td wrapper
-sub formatToolTip {
- my $self = shift;
- my $answer = shift;
- my $formattedAnswer = shift;
- return CGI::td(CGI::span({
- class => "answer-preview",
- data_bs_toggle => "popover",
- data_bs_content => $answer,
- data_bs_placement => "bottom",
- },
- $self->nbsp($formattedAnswer))
- );
-}
-
-1;
diff --git a/public/css/bootstrap.scss b/public/css/bootstrap.scss
new file mode 100644
index 000000000..bc6ea82a9
--- /dev/null
+++ b/public/css/bootstrap.scss
@@ -0,0 +1,100 @@
+/* WeBWorK Online Homework Delivery System
+ * 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
+ * Artistic License for more details.
+ */
+
+// Include functions first (so you can manipulate colors, SVGs, calc, etc)
+@import "../node_modules/bootstrap/scss/functions";
+
+// Variable overrides
+
+// Enable shadows and gradients. These are disabled by default.
+$enable-shadows: true;
+
+// Use a smaller grid gutter width. The default is 1.5rem.
+$grid-gutter-width: 1rem;
+
+// Fonts
+$font-size-base: 0.85rem;
+$headings-font-weight: 600;
+
+// Links
+$link-decoration: none;
+$link-hover-decoration: underline;
+
+// Make breadcrumb dividers and active items a bit darker.
+$breadcrumb-divider-color: #495057;
+$breadcrumb-active-color: #495057;
+
+@import "./theme-colors";
+
+// Include the remainder of bootstrap's scss configuration
+@import "../node_modules/bootstrap/scss/variables";
+@import "../node_modules/bootstrap/scss/maps";
+@import "../node_modules/bootstrap/scss/mixins";
+@import "../node_modules/bootstrap/scss/utilities";
+
+// Layout & components
+@import "../node_modules/bootstrap/scss/root";
+@import "../node_modules/bootstrap/scss/reboot";
+@import "../node_modules/bootstrap/scss/type";
+@import "../node_modules/bootstrap/scss/images";
+@import "../node_modules/bootstrap/scss/containers";
+@import "../node_modules/bootstrap/scss/grid";
+@import "../node_modules/bootstrap/scss/tables";
+@import "../node_modules/bootstrap/scss/forms";
+@import "../node_modules/bootstrap/scss/buttons";
+@import "../node_modules/bootstrap/scss/transitions";
+@import "../node_modules/bootstrap/scss/dropdown";
+@import "../node_modules/bootstrap/scss/button-group";
+@import "../node_modules/bootstrap/scss/nav";
+@import "../node_modules/bootstrap/scss/navbar";
+@import "../node_modules/bootstrap/scss/card";
+@import "../node_modules/bootstrap/scss/accordion";
+@import "../node_modules/bootstrap/scss/breadcrumb";
+@import "../node_modules/bootstrap/scss/pagination";
+@import "../node_modules/bootstrap/scss/badge";
+@import "../node_modules/bootstrap/scss/alert";
+@import "../node_modules/bootstrap/scss/placeholders";
+@import "../node_modules/bootstrap/scss/progress";
+@import "../node_modules/bootstrap/scss/list-group";
+@import "../node_modules/bootstrap/scss/close";
+@import "../node_modules/bootstrap/scss/toasts";
+@import "../node_modules/bootstrap/scss/modal";
+@import "../node_modules/bootstrap/scss/tooltip";
+@import "../node_modules/bootstrap/scss/popover";
+@import "../node_modules/bootstrap/scss/carousel";
+@import "../node_modules/bootstrap/scss/spinners";
+@import "../node_modules/bootstrap/scss/offcanvas";
+
+// Helpers
+@import "../node_modules/bootstrap/scss/helpers";
+
+// Utilities
+@import "../node_modules/bootstrap/scss/utilities/api";
+
+// WeBWorK specific colors
+:root {
+ --ww-logo-background-color: #{$ww-logo-background-color};
+ --ww-primary-foreground-color: #{color-contrast($primary)};
+ --ww-achievement-level-color: #{$ww-achievement-level-color};
+}
+
+// Overrides
+a:not(.btn):focus {
+ color: $link-hover-color;
+ outline-style: solid;
+ outline-color: $link-hover-color;
+ outline-width: 1px;
+}
+
+@import "theme-overrides";
diff --git a/public/css/rtl.css b/public/css/rtl.css
new file mode 100644
index 000000000..979d21a46
--- /dev/null
+++ b/public/css/rtl.css
@@ -0,0 +1,20 @@
+/* WeBWorK Online Homework Delivery System
+ * Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of either: (a) the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any later
+ * version, or (b) the "Artistic License" which comes with this package.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
+ * Artistic License for more details.
+ */
+
+/* --- Modify some CSS for Right to left courses/problems --- */
+
+/* The changes which were needed here in WeBWorK 2.16 are no
+ * longer needed in WeBWorK 2.17. The file is being retained
+ * for potential future use. */
+
diff --git a/public/generate-assets.js b/public/generate-assets.js
new file mode 100755
index 000000000..fa9a061ce
--- /dev/null
+++ b/public/generate-assets.js
@@ -0,0 +1,219 @@
+#!/usr/bin/env node
+
+/* eslint-env node */
+
+const yargs = require('yargs');
+const chokidar = require('chokidar');
+const path = require('path');
+const { minify } = require('terser');
+const fs = require('fs');
+const crypto = require('crypto');
+const sass = require('sass');
+const autoprefixer = require('autoprefixer');
+const postcss = require('postcss');
+const rtlcss = require('rtlcss');
+const cssMinify = require('cssnano');
+
+const argv = yargs
+ .usage('$0 Options').version(false).alias('help', 'h').wrap(100)
+ .option('enable-sourcemaps', {
+ alias: 's',
+ description: 'Generate source maps. (Not for use in production!)',
+ type: 'boolean'
+ })
+ .option('watch-files', {
+ alias: 'w',
+ description: 'Continue to watch files for changes. (Developer tool)',
+ type: 'boolean'
+ })
+ .option('clean', {
+ alias: 'd',
+ description: 'Delete all generated files.',
+ type: 'boolean'
+ })
+ .argv;
+
+const assetFile = path.resolve(__dirname, 'static-assets.json');
+const assets = {};
+
+const cleanDir = (dir) => {
+ for (const file of fs.readdirSync(dir, { withFileTypes: true })) {
+ if (file.isDirectory()) {
+ cleanDir(path.resolve(dir, file.name));
+ } else {
+ if (/.[a-z0-9]{8}.min.(css|js)$/.test(file.name)) {
+ const fullPath = path.resolve(dir, file.name);
+ console.log(`\x1b[34mRemoving ${fullPath} from previous build.\x1b[0m`);
+ fs.unlinkSync(fullPath);
+ }
+ }
+ }
+}
+
+// The is set to true after all files are processed for the first time.
+let ready = false;
+
+const processFile = async (file, _details) => {
+ if (file) {
+ const baseName = path.basename(file);
+
+ if (/(? {
+ // If a file is deleted, then also delete the corresponding generated file.
+ if (assets[file]) {
+ console.log(`\x1b[34mDeleting minified file for ${file}.\x1b[0m`);
+ fs.unlinkSync(path.resolve(__dirname, assets[file]));
+ delete assets[file];
+ }
+ })
+ .on('error', (error) => console.log(`\x1b[32m${error}\x1b[0m`));
diff --git a/public/images/favicon.ico b/public/images/favicon.ico
new file mode 100644
index 000000000..92fc9bea9
Binary files /dev/null and b/public/images/favicon.ico differ
diff --git a/public/js/apps/CSSMessage/css-message.js b/public/js/apps/CSSMessage/css-message.js
new file mode 100644
index 000000000..f3adc5861
--- /dev/null
+++ b/public/js/apps/CSSMessage/css-message.js
@@ -0,0 +1,45 @@
+window.document.getElementsByName('JWTanswerURLstatus').forEach(e => {
+ console.log("response message ", JSON.parse(e.value));
+ window.parent.postMessage(e.value, '*');
+});
+
+window.addEventListener('message', event => {
+ let message;
+ try {
+ message = JSON.parse(event.data);
+ }
+ catch (e) {
+ return;
+ }
+
+ if (message.hasOwnProperty('elements')) {
+ message.elements.forEach((incoming) => {
+ let elements;
+ if (incoming.hasOwnProperty('selector')) {
+ elements = window.document.querySelectorAll(incoming.selector);
+ if (incoming.hasOwnProperty('style')) {
+ elements.forEach(el => { el.style.cssText = incoming.style });
+ }
+ if (incoming.hasOwnProperty('class')) {
+ elements.forEach(el => { el.className = incoming.class });
+ }
+ }
+ });
+ event.source.postMessage('updated elements', event.origin);
+ }
+
+ if (message.hasOwnProperty('templates')) {
+ message.templates.forEach((cssString) => {
+ const element = document.createElement('style');
+ element.innerText = cssString;
+ document.head.insertAdjacentElement('beforeend', element);
+ });
+ event.source.postMessage('updated templates', event.origin);
+ }
+
+ if (message.hasOwnProperty('showSolutions')) {
+ const elements = Array.from(window.document.querySelectorAll('.knowl[data-type="solution"]'));
+ const solutions = elements.map(el => el.dataset.knowlContents);
+ event.source.postMessage(JSON.stringify({ solutions: solutions }), event.origin);
+ }
+});
diff --git a/public/Problem/mathjax-config.js b/public/js/apps/MathJaxConfig/mathjax-config.js
similarity index 81%
rename from public/Problem/mathjax-config.js
rename to public/js/apps/MathJaxConfig/mathjax-config.js
index 73ba8128b..3ef132bef 100644
--- a/public/Problem/mathjax-config.js
+++ b/public/js/apps/MathJaxConfig/mathjax-config.js
@@ -1,11 +1,12 @@
if (!window.MathJax) {
window.MathJax = {
tex: {
- packages: { '[+]': ['noerrors'] }
+ packages: {'[+]': ['noerrors']},
+ processEscapes: false,
},
loader: { load: ['input/asciimath', '[tex]/noerrors'] },
startup: {
- ready: function () {
+ ready: function() {
var AM = MathJax.InputJax.AsciiMath.AM;
for (var i = 0; i < AM.symbols.length; i++) {
if (AM.symbols[i].input == '**') {
@@ -18,13 +19,13 @@ if (!window.MathJax) {
options: {
renderActions: {
findScript: [10, function (doc) {
- document.querySelectorAll('script[type^="math/tex"]').forEach(function (node) {
+ document.querySelectorAll('script[type^="math/tex"]').forEach(function(node) {
var display = !!node.type.match(/; *mode=display/);
var math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display);
var text = document.createTextNode('');
node.parentNode.replaceChild(text, node);
- math.start = { node: text, delim: '', n: 0 };
- math.end = { node: text, delim: '', n: 0 };
+ math.start = {node: text, delim: '', n: 0};
+ math.end = {node: text, delim: '', n: 0};
doc.math.push(math);
});
}, '']
diff --git a/public/js/apps/PGCodeMirror/PG.js b/public/js/apps/PGCodeMirror/PG.js
new file mode 100644
index 000000000..62de45cfd
--- /dev/null
+++ b/public/js/apps/PGCodeMirror/PG.js
@@ -0,0 +1,1460 @@
+// Bassed off of CodeMirror mode perl file:
+// https://github.com/codemirror/CodeMirror/blob/master/mode/perl/perl.js
+
+'use strict';
+
+(() => {
+ CodeMirror.defineMode("PG",function(){
+ // http://perldoc.perl.org
+ const PERL={ // null - magic touch
+ // 1 - keyword
+ // 2 - def
+ // 3 - atom
+ // 4 - operator
+ // 5 - variable-2 (predefined)
+ // [x,y] - x=1,2,3; y=must be defined if x{...}
+ // PERL operators
+ '->' : 4,
+ '++' : 4,
+ '--' : 4,
+ '**' : 4,
+ // ! ~ \ and unary + and -
+ '=~' : 4,
+ '!~' : 4,
+ '*' : 4,
+ '/' : 4,
+ '%' : 4,
+ 'x' : 4,
+ '+' : 4,
+ '-' : 4,
+ '.' : 4,
+ '<<' : 4,
+ '>>' : 4,
+ // named unary operators
+ '<' : 4,
+ '>' : 4,
+ '<=' : 4,
+ '>=' : 4,
+ 'lt' : 4,
+ 'gt' : 4,
+ 'le' : 4,
+ 'ge' : 4,
+ '==' : 4,
+ '!=' : 4,
+ '<=>' : 4,
+ 'eq' : 4,
+ 'ne' : 4,
+ 'cmp' : 4,
+ '~~' : 4,
+ '&' : 4,
+ '|' : 4,
+ '^' : 4,
+ '&&' : 4,
+ '||' : 4,
+ '//' : 4,
+ '..' : 4,
+ '...' : 4,
+ '?' : 4,
+ ':' : 4,
+ '=' : 4,
+ '+=' : 4,
+ '-=' : 4,
+ '*=' : 4, // etc. ???
+ ',' : 4,
+ '=>' : 4,
+ '::' : 4,
+ // list operators (rightward)
+ 'not' : 4,
+ 'and' : 4,
+ 'or' : 4,
+ 'xor' : 4,
+ // PERL predefined variables (I know, what this is a paranoid idea, but may be needed for people, who learn PERL, and for me as well, ...and may be for you?;)
+ 'BEGIN' : [5,1],
+ 'END' : [5,1],
+ 'PRINT' : [5,1],
+ 'PRINTF' : [5,1],
+ 'GETC' : [5,1],
+ 'READ' : [5,1],
+ 'READLINE' : [5,1],
+ 'DESTROY' : [5,1],
+ 'TIE' : [5,1],
+ 'TIEHANDLE' : [5,1],
+ 'UNTIE' : [5,1],
+ 'STDIN' : 5,
+ 'STDIN_TOP' : 5,
+ 'STDOUT' : 5,
+ 'STDOUT_TOP' : 5,
+ 'STDERR' : 5,
+ 'STDERR_TOP' : 5,
+ '$ARG' : 5,
+ '$_' : 5,
+ '@ARG' : 5,
+ '@_' : 5,
+ '$LIST_SEPARATOR' : 5,
+ '$"' : 5,
+ '$PROCESS_ID' : 5,
+ '$PID' : 5,
+ '$$' : 5,
+ '$REAL_GROUP_ID' : 5,
+ '$GID' : 5,
+ '$(' : 5,
+ '$EFFECTIVE_GROUP_ID' : 5,
+ '$EGID' : 5,
+ '$)' : 5,
+ '$PROGRAM_NAME' : 5,
+ '$0' : 5,
+ '$SUBSCRIPT_SEPARATOR' : 5,
+ '$SUBSEP' : 5,
+ '$;' : 5,
+ '$REAL_USER_ID' : 5,
+ '$UID' : 5,
+ '$<' : 5,
+ '$EFFECTIVE_USER_ID' : 5,
+ '$EUID' : 5,
+ '$>' : 5,
+// '$a' : 5,
+// '$b' : 5,
+ '$COMPILING' : 5,
+ '$^C' : 5,
+ '$DEBUGGING' : 5,
+ '$^D' : 5,
+ '${^ENCODING}' : 5,
+ '$ENV' : 5,
+ '%ENV' : 5,
+ '$SYSTEM_FD_MAX' : 5,
+ '$^F' : 5,
+ '@F' : 5,
+ '${^GLOBAL_PHASE}' : 5,
+ '$^H' : 5,
+ '%^H' : 5,
+ '@INC' : 5,
+ '%INC' : 5,
+ '$INPLACE_EDIT' : 5,
+ '$^I' : 5,
+ '$^M' : 5,
+ '$OSNAME' : 5,
+ '$^O' : 5,
+ '${^OPEN}' : 5,
+ '$PERLDB' : 5,
+ '$^P' : 5,
+ '$SIG' : 5,
+ '%SIG' : 5,
+ '$BASETIME' : 5,
+ '$^T' : 5,
+ '${^TAINT}' : 5,
+ '${^UNICODE}' : 5,
+ '${^UTF8CACHE}' : 5,
+ '${^UTF8LOCALE}' : 5,
+ '$PERL_VERSION' : 5,
+ '$^V' : 5,
+ '${^WIN32_SLOPPY_STAT}' : 5,
+ '$EXECUTABLE_NAME' : 5,
+ '$^X' : 5,
+ '$1' : 5, // - regexp $1, $2...
+ '$MATCH' : 5,
+ '$&' : 5,
+ '${^MATCH}' : 5,
+ '$PREMATCH' : 5,
+ '$`' : 5,
+ '${^PREMATCH}' : 5,
+ '$POSTMATCH' : 5,
+ "$'" : 5,
+ '${^POSTMATCH}' : 5,
+ '$LAST_PAREN_MATCH' : 5,
+ '$+' : 5,
+ '$LAST_SUBMATCH_RESULT' : 5,
+ '$^N' : 5,
+ '@LAST_MATCH_END' : 5,
+ '@+' : 5,
+ '%LAST_PAREN_MATCH' : 5,
+ '%+' : 5,
+ '@LAST_MATCH_START' : 5,
+ '@-' : 5,
+ '%LAST_MATCH_START' : 5,
+ '%-' : 5,
+ '$LAST_REGEXP_CODE_RESULT' : 5,
+ '$^R' : 5,
+ '${^RE_DEBUG_FLAGS}' : 5,
+ '${^RE_TRIE_MAXBUF}' : 5,
+ '$ARGV' : 5,
+ '@ARGV' : 5,
+ 'ARGV' : 5,
+ 'ARGVOUT' : 5,
+ '$OUTPUT_FIELD_SEPARATOR' : 5,
+ '$OFS' : 5,
+ '$,' : 5,
+ '$INPUT_LINE_NUMBER' : 5,
+ '$NR' : 5,
+ '$.' : 5,
+ '$INPUT_RECORD_SEPARATOR' : 5,
+ '$RS' : 5,
+ '$/' : 5,
+ '$OUTPUT_RECORD_SEPARATOR' : 5,
+ '$ORS' : 5,
+ '$\\' : 5,
+ '$OUTPUT_AUTOFLUSH' : 5,
+ '$|' : 5,
+ '$ACCUMULATOR' : 5,
+ '$^A' : 5,
+ '$FORMAT_FORMFEED' : 5,
+ '$^L' : 5,
+ '$FORMAT_PAGE_NUMBER' : 5,
+ '$%' : 5,
+ '$FORMAT_LINES_LEFT' : 5,
+ '$-' : 5,
+ '$FORMAT_LINE_BREAK_CHARACTERS' : 5,
+ '$:' : 5,
+ '$FORMAT_LINES_PER_PAGE' : 5,
+ '$=' : 5,
+ '$FORMAT_TOP_NAME' : 5,
+ '$^' : 5,
+ '$FORMAT_NAME' : 5,
+ '$~' : 5,
+ '${^CHILD_ERROR_NATIVE}' : 5,
+ '$EXTENDED_OS_ERROR' : 5,
+ '$^E' : 5,
+ '$EXCEPTIONS_BEING_CAUGHT' : 5,
+ '$^S' : 5,
+ '$WARNING' : 5,
+ '$^W' : 5,
+ '${^WARNING_BITS}' : 5,
+ '$OS_ERROR' : 5,
+ '$ERRNO' : 5,
+ '$!' : 5,
+ '%OS_ERROR' : 5,
+ '%ERRNO' : 5,
+ '%!' : 5,
+ '$CHILD_ERROR' : 5,
+ '$?' : 5,
+ '$EVAL_ERROR' : 5,
+ '$@' : 5,
+ '$OFMT' : 5,
+ '$#' : 5,
+ '$*' : 5,
+ '$ARRAY_BASE' : 5,
+ '$[' : 5,
+ '$OLD_PERL_VERSION' : 5,
+ '$]' : 5,
+ // PERL blocks
+ 'if' :[1,1],
+ elsif :[1,1],
+ 'else' :[1,1],
+ 'while' :[1,1],
+ unless :[1,1],
+ until :[1,1],
+ 'for' :[1,1],
+ foreach :[1,1],
+ // PERL functions
+ 'abs' :1, // - absolute value function
+ accept :1, // - accept an incoming socket connect
+ alarm :1, // - schedule a SIGALRM
+ 'atan2' :1, // - arctangent of Y/X in the range -PI to PI
+ bind :1, // - binds an address to a socket
+ binmode :1, // - prepare binary files for I/O
+ bless :1, // - create an object
+ bootstrap :1, //
+ 'break' :1, // - break out of a "given" block
+ caller :1, // - get context of the current subroutine call
+ chdir :1, // - change your current working directory
+ chmod :1, // - changes the permissions on a list of files
+ chomp :1, // - remove a trailing record separator from a string
+ chop :1, // - remove the last character from a string
+ chown :1, // - change the ownership on a list of files
+ chr :1, // - get character this number represents
+ chroot :1, // - make directory new root for path lookups
+ close :1, // - close file (or pipe or socket) handle
+ closedir :1, // - close directory handle
+ connect :1, // - connect to a remote socket
+ 'continue' :[1,1], // - optional trailing block in a while or foreach
+ 'cos' :1, // - cosine function
+ crypt :1, // - one-way passwd-style encryption
+ dbmclose :1, // - breaks binding on a tied dbm file
+ dbmopen :1, // - create binding on a tied dbm file
+ 'default' :1, //
+ defined :1, // - test whether a value, variable, or function is defined
+ 'delete' :1, // - deletes a value from a hash
+ die :1, // - raise an exception or bail out
+ 'do' :1, // - turn a BLOCK into a TERM
+ dump :1, // - create an immediate core dump
+ each :1, // - retrieve the next key/value pair from a hash
+ endgrent :1, // - be done using group file
+ endhostent :1, // - be done using hosts file
+ endnetent :1, // - be done using networks file
+ endprotoent :1, // - be done using protocols file
+ endpwent :1, // - be done using passwd file
+ endservent :1, // - be done using services file
+ eof :1, // - test a filehandle for its end
+ 'eval' :1, // - catch exceptions or compile and run code
+ 'exec' :1, // - abandon this program to run another
+ exists :1, // - test whether a hash key is present
+ exit :1, // - terminate this program
+ 'exp' :1, // - raise I to a power
+ fcntl :1, // - file control system call
+ fileno :1, // - return file descriptor from filehandle
+ flock :1, // - lock an entire file with an advisory lock
+ fork :1, // - create a new process just like this one
+ format :1, // - declare a picture format with use by the write() function
+ formline :1, // - internal function used for formats
+ getc :1, // - get the next character from the filehandle
+ getgrent :1, // - get next group record
+ getgrgid :1, // - get group record given group user ID
+ getgrnam :1, // - get group record given group name
+ gethostbyaddr :1, // - get host record given its address
+ gethostbyname :1, // - get host record given name
+ gethostent :1, // - get next hosts record
+ getlogin :1, // - return who logged in at this tty
+ getnetbyaddr :1, // - get network record given its address
+ getnetbyname :1, // - get networks record given name
+ getnetent :1, // - get next networks record
+ getpeername :1, // - find the other end of a socket connection
+ getpgrp :1, // - get process group
+ getppid :1, // - get parent process ID
+ getpriority :1, // - get current nice value
+ getprotobyname :1, // - get protocol record given name
+ getprotobynumber :1, // - get protocol record numeric protocol
+ getprotoent :1, // - get next protocols record
+ getpwent :1, // - get next passwd record
+ getpwnam :1, // - get passwd record given user login name
+ getpwuid :1, // - get passwd record given user ID
+ getservbyname :1, // - get services record given its name
+ getservbyport :1, // - get services record given numeric port
+ getservent :1, // - get next services record
+ getsockname :1, // - retrieve the sockaddr for a given socket
+ getsockopt :1, // - get socket options on a given socket
+ given :1, //
+ glob :1, // - expand filenames using wildcards
+ gmtime :1, // - convert UNIX time into record or string using Greenwich time
+ 'goto' :1, // - create spaghetti code
+ grep :1, // - locate elements in a list test true against a given criterion
+ hex :1, // - convert a string to a hexadecimal number
+ 'import' :1, // - patch a module's namespace into your own
+ index :1, // - find a substring within a string
+ 'int' :1, // - get the integer portion of a number
+ ioctl :1, // - system-dependent device control system call
+ 'join' :1, // - join a list into a string using a separator
+ keys :1, // - retrieve list of indices from a hash
+ kill :1, // - send a signal to a process or process group
+ last :1, // - exit a block prematurely
+ lc :1, // - return lower-case version of a string
+ lcfirst :1, // - return a string with just the next letter in lower case
+ length :1, // - return the number of bytes in a string
+ 'link' :1, // - create a hard link in the filesystem
+ listen :1, // - register your socket as a server
+ local : 2, // - create a temporary value for a global variable (dynamic scoping)
+ localtime :1, // - convert UNIX time into record or string using local time
+ lock :1, // - get a thread lock on a variable, subroutine, or method
+ 'log' :1, // - retrieve the natural logarithm for a number
+ lstat :1, // - stat a symbolic link
+ m :null, // - match a string with a regular expression pattern
+ map :1, // - apply a change to a list to get back a new list with the changes
+ mkdir :1, // - create a directory
+ msgctl :1, // - SysV IPC message control operations
+ msgget :1, // - get SysV IPC message queue
+ msgrcv :1, // - receive a SysV IPC message from a message queue
+ msgsnd :1, // - send a SysV IPC message to a message queue
+ my : 2, // - declare and assign a local variable (lexical scoping)
+ 'new' :1, //
+ next :1, // - iterate a block prematurely
+ no :1, // - unimport some module symbols or semantics at compile time
+ oct :1, // - convert a string to an octal number
+ open :1, // - open a file, pipe, or descriptor
+ opendir :1, // - open a directory
+ ord :1, // - find a character's numeric representation
+ our : 2, // - declare and assign a package variable (lexical scoping)
+ pack :1, // - convert a list into a binary representation
+ 'package' :1, // - declare a separate global namespace
+ pipe :1, // - open a pair of connected filehandles
+ pop :1, // - remove the last element from an array and return it
+ pos :1, // - find or set the offset for the last/next m//g search
+ print :1, // - output a list to a filehandle
+ printf :1, // - output a formatted list to a filehandle
+ prototype :1, // - get the prototype (if any) of a subroutine
+ push :1, // - append one or more elements to an array
+ q :null, // - singly quote a string
+ qq :null, // - doubly quote a string
+ qr :null, // - Compile pattern
+ quotemeta :null, // - quote regular expression magic characters
+ qw :null, // - quote a list of words
+ qx :null, // - backquote quote a string
+ rand :1, // - retrieve the next pseudorandom number
+ read :1, // - fixed-length buffered input from a filehandle
+ readdir :1, // - get a directory from a directory handle
+ readline :1, // - fetch a record from a file
+ readlink :1, // - determine where a symbolic link is pointing
+ readpipe :1, // - execute a system command and collect standard output
+ recv :1, // - receive a message over a Socket
+ redo :1, // - start this loop iteration over again
+ ref :1, // - find out the type of thing being referenced
+ rename :1, // - change a filename
+ require :1, // - load in external functions from a library at runtime
+ reset :1, // - clear all variables of a given name
+ 'return' :1, // - get out of a function early
+ reverse :1, // - flip a string or a list
+ rewinddir :1, // - reset directory handle
+ rindex :1, // - right-to-left substring search
+ rmdir :1, // - remove a directory
+ s :null, // - replace a pattern with a string
+ say :1, // - print with newline
+ scalar :1, // - force a scalar context
+ seek :1, // - reposition file pointer for random-access I/O
+ seekdir :1, // - reposition directory pointer
+ select :1, // - reset default output or do I/O multiplexing
+ semctl :1, // - SysV semaphore control operations
+ semget :1, // - get set of SysV semaphores
+ semop :1, // - SysV semaphore operations
+ send :1, // - send a message over a socket
+ setgrent :1, // - prepare group file for use
+ sethostent :1, // - prepare hosts file for use
+ setnetent :1, // - prepare networks file for use
+ setpgrp :1, // - set the process group of a process
+ setpriority :1, // - set a process's nice value
+ setprotoent :1, // - prepare protocols file for use
+ setpwent :1, // - prepare passwd file for use
+ setservent :1, // - prepare services file for use
+ setsockopt :1, // - set some socket options
+ shift :1, // - remove the first element of an array, and return it
+ shmctl :1, // - SysV shared memory operations
+ shmget :1, // - get SysV shared memory segment identifier
+ shmread :1, // - read SysV shared memory
+ shmwrite :1, // - write SysV shared memory
+ shutdown :1, // - close down just half of a socket connection
+ 'sin' :1, // - return the sine of a number
+ sleep :1, // - block for some number of seconds
+ socket :1, // - create a socket
+ socketpair :1, // - create a pair of sockets
+ 'sort' :1, // - sort a list of values
+ splice :1, // - add or remove elements anywhere in an array
+ 'split' :1, // - split up a string using a regexp delimiter
+ sprintf :1, // - formatted print into a string
+ 'sqrt' :1, // - square root function
+ srand :1, // - seed the random number generator
+ stat :1, // - get a file's status information
+ state :1, // - declare and assign a state variable (persistent lexical scoping)
+ study :1, // - optimize input data for repeated searches
+ 'sub' :1, // - declare a subroutine, possibly anonymously
+ 'substr' :1, // - get or alter a portion of a string
+ symlink :1, // - create a symbolic link to a file
+ syscall :1, // - execute an arbitrary system call
+ sysopen :1, // - open a file, pipe, or descriptor
+ sysread :1, // - fixed-length unbuffered input from a filehandle
+ sysseek :1, // - position I/O pointer on handle used with sysread and syswrite
+ system :1, // - run a separate program
+ syswrite :1, // - fixed-length unbuffered output to a filehandle
+ tell :1, // - get current seekpointer on a filehandle
+ telldir :1, // - get current seekpointer on a directory handle
+ tie :1, // - bind a variable to an object class
+ tied :1, // - get a reference to the object underlying a tied variable
+ time :1, // - return number of seconds since 1970
+ times :1, // - return elapsed time for self and child processes
+ tr :null, // - transliterate a string
+ truncate :1, // - shorten a file
+ uc :1, // - return upper-case version of a string
+ ucfirst :1, // - return a string with just the next letter in upper case
+ umask :1, // - set file creation mode mask
+ undef :1, // - remove a variable or function definition
+ unlink :1, // - remove one link to a file
+ unpack :1, // - convert binary structure into normal perl variables
+ unshift :1, // - prepend more elements to the beginning of a list
+ untie :1, // - break a tie binding to a variable
+ use :1, // - load in a module at compile time
+ utime :1, // - set a file's last access and modify times
+ values :1, // - return a list of the values in a hash
+ vec :1, // - test or set particular bits in a string
+ wait :1, // - wait for any child process to die
+ waitpid :1, // - wait for a particular child process to die
+ wantarray :1, // - get void vs scalar vs list context of current subroutine call
+ warn :1, // - print debugging info
+ when :1, //
+ write :1, // - print a picture record
+ y :null, // - transliterate a string
+ };
+ // PG Keywords and Variables
+ const PGstyle = 'atom';
+ const PGkeyword = 'keyword';
+ const PGcmds = new Set([
+ 'DOCUMENT',
+ 'ENDDOCUMENT',
+ 'loadMacros',
+ 'TEXT',
+ 'SOLUTION',
+ 'HINT',
+ 'STATEMENT',
+ 'COMMENT',
+ 'MODES',
+ 'htmlLink',
+ 'helpLink',
+ 'knowlLink',
+ 'image',
+ 'Context',
+ 'Compute',
+ 'Real',
+ 'Formula',
+ 'String',
+ 'List',
+ 'Complex',
+ 'Point',
+ 'Vector',
+ 'Matrix',
+ 'Interval',
+ 'Set',
+ 'Fraction',
+ 'ANS',
+ 'NAMED_ANS',
+ 'WEIGHTED_ANS',
+ 'MultiAnswer',
+ 'Value',
+ 'random',
+ 'list_random',
+ 'non_zero_random',
+ 'NchooseK',
+ ]);
+ const PGvars = new Set([
+ 'BR',
+ 'RBR',
+ 'PAR',
+ 'LQ',
+ 'RQ',
+ 'BM',
+ 'EM',
+ 'BDM',
+ 'EDM',
+ 'LTS',
+ 'GTS',
+ 'LTE',
+ 'GTE',
+ 'BEGIN_ONE_COLUMN',
+ 'END_ONE_COLUMN',
+ 'SOL',
+ 'SOLUTION',
+ 'HINT',
+ 'COMMENT',
+ 'US',
+ 'SPACE',
+ 'NBSP',
+ 'NDASH',
+ 'MDASH',
+ 'BLABEL',
+ 'ELABEL',
+ 'BBOLD',
+ 'EBOLD',
+ 'BITALIC',
+ 'EITALIC',
+ 'BUL',
+ 'EUL',
+ 'BCENTER',
+ 'ECENTER',
+ 'BLTR',
+ 'ELTR',
+ 'BKBD',
+ 'EKBD',
+ 'HR',
+ 'LBRACE',
+ 'RBRACE',
+ 'LB',
+ 'RB',
+ 'DOLLAR',
+ 'PERCENT',
+ 'CARET',
+ 'PI',
+ 'E',
+ 'LATEX',
+ 'TEX',
+ 'APOS',
+ 'showPartialCorrectAnswers',
+ 'refreshCachedImages',
+ 'ITEM',
+ 'ITEMSEP'
+ ]);
+
+ const RXstyle="string-2";
+ const RXmodifiers=/[goseximacplud]/; // NOTE: "m", "s", "y" and "tr" need to correct real modifiers for each regexp type
+
+ function tokenChain(stream,state,chain,style,tail,tokener){ // NOTE: chain.length > 2 is not working now (it's for s[...][...]geos;)
+ state.chain=null; // 12 3tail
+ state.style=null;
+ state.tail=null;
+ state.tokenize=function(stream,state){
+ var e=false,c,i=0;
+ while(c=stream.next()){
+ if(c===chain[i]&&!e){
+ if(chain[++i]!==undefined){
+ state.chain=chain[i];
+ state.style=style;
+ state.tail=tail;}
+ else if(tail)
+ stream.eatWhile(tail);
+ state.tokenize=tokener || tokenPerl;
+ return style;}
+ e=!e&&c=="\\";}
+ return style;};
+ return state.tokenize(stream,state);}
+
+ function tokenSOMETHING(stream,state,string){
+ state.tokenize=function(stream,state){
+ if(stream.string==string)
+ state.tokenize=tokenPerl;
+ stream.skipToEnd();
+ return "string";};
+ return state.tokenize(stream,state);}
+
+ // EV3 block formatting
+ function tokenEV3(stream,state,string,style,prevState){
+ state.tokenize = function(stream,state) {
+ if (prevState && prevState.mode == "math") {
+ var reg = new RegExp("^\\\\"+string);
+ } else {
+ var reg = new RegExp("^"+string);
+ }
+ if (stream.match(reg)) {
+ if (!prevState) {
+ state.tokenize = tokenPerl;
+ return PGstyle;
+ } else {
+ state.tokenize = function(stream,state) {
+ return tokenEV3(stream,state,prevState.string,
+ prevState.style,prevState.prevState);
+ }
+ }
+ if (string.includes("BOLD"))
+ return PGstyle + " strong";
+ if (string.includes("ITALIC"))
+ return PGstyle + " em";
+ if (prevState.endstyle)
+ return prevState.endstyle;
+ return style;
+ } else {
+ state.tokenize = function(stream,state) {
+ return tokenEV3(stream,state,string,style,prevState);
+ }
+ }
+
+ const newPrevState = {};
+ if (prevState) {
+ newPrevState.prevState = JSON.parse(JSON.stringify(prevState));
+ } else {
+ newPrevState.prevState = null;
+ }
+ newPrevState.style = style;
+ newPrevState.string = string;
+
+ if (prevState && prevState.mode == "cmd") { // Some additional formatting for perl code blocks
+ newPrevState.mode = "cmd";
+ if (stream.match(/^[$@%]{/)) { // ${, @{, %{ nested blocks
+ style = "variable";
+ state.tokenize = function(stream,state) {
+ return tokenEV3(stream,state,"\\}",style,newPrevState);
+ }
+ return style;
+ }
+ if (stream.match(/^\(/)) { // Nested ( ) blocks
+ newPrevState.endstyle = "variable";
+ state.tokenize = function(stream,state) {
+ return tokenEV3(stream,state,"\\)",style,newPrevState);
+ }
+ return "variable";
+ }
+ if (stream.match(/^\[/)) { // Nested [ ] blocks
+ newPrevState.endstyle = "variable";
+ state.tokenize = function(stream,state) {
+ return tokenEV3(stream,state,"\\]",style,newPrevState);
+ }
+ return "variable";
+ }
+ if (stream.match(/^\{/)) { // Nested { } blocks
+ newPrevState.endstyle = "variable";
+ state.tokenize = function(stream,state) {
+ return tokenEV3(stream,state,"\\}",style,newPrevState);
+ }
+ return "variable";
+ }
+ if (stream.match(/^\w+/)) { // Check for PG keywords
+ if (PGcmds.has(stream.current()))
+ return PGkeyword;
+ else
+ return style;
+ }
+ if (stream.match(/^['"]/)) { // Quotes
+ return tokenChain(stream,state,[stream.current()],"string",null,function(stream,state) {
+ tokenEV3(stream,state,string,style,prevState) });
+ }
+ if (stream.match(/^[=,;/\*><%&|.~?:+/-]/)) { // Catch some perl operators
+ return "variable";
+ }
+ }
+
+ if (stream.match(/^\\\(/)) { // \(...\) TeX block
+ if (prevState) {
+ style = "error";
+ } else {
+ style = "comment";
+ }
+ state.tokenize = function(stream,state) {
+ newPrevState.mode = "math";
+ return tokenEV3(stream,state,"\\)",style,newPrevState);
+ }
+ } else if (stream.match(/^\\\[/)) { // \[...\] TeX block
+ if (prevState) {
+ style = "error";
+ } else {
+ style = "comment";
+ }
+ state.tokenize = function(stream,state) {
+ newPrevState.mode = "math";
+ return tokenEV3(stream,state,"\\]",style,newPrevState);
+ }
+ } else if (stream.match(/^\\{/)) { // \{...\} Perl code block
+ if (prevState) {
+ style = "error";
+ } else {
+ style = "variable-2";
+ }
+ state.tokenize = function(stream,state) {
+ newPrevState.mode = "cmd";
+ return tokenEV3(stream,state,"\\\\}",style,newPrevState);
+ }
+ } else if (stream.match(/^``/)) { // ``...`` math object math
+ if (prevState && prevState.mode != "math") {
+ style = "error";
+ } else {
+ style = "variable-3";
+ }
+ state.tokenize = function(stream,state) {
+ return tokenEV3(stream,state,"``",style,newPrevState);
+ }
+ } else if (stream.match(/^`/)) { // `...` math object math
+ if (prevState && prevState.mode != "math") {
+ style = "error";
+ } else {
+ style = "variable-3";
+ }
+ state.tokenize = function(stream,state) {
+ return tokenEV3(stream,state,"`([^`]|$)",style,newPrevState,"tick");
+ }
+ } else if (stream.match(/^(\$BBOLD|\${BBOLD})/)) { // Bold
+ style = style = " strong";
+ state.tokenize = function(stream,state) {
+ return tokenEV3(stream,state,"(\\$EBOLD|\\${EBOLD})",style,newPrevState);
+ }
+ return PGstyle + " strong";
+ } else if (stream.match(/^(\$BITALIC|\${BITALIC})/)) { // Italic
+ style = style + " em";
+ state.tokenize = function(stream,state) {
+ return tokenEV3(stream,state,"(\\$EITALIC|\\${EITALIC})",style,newPrevState);
+ }
+ return PGstyle + " em";
+ } else if (stream.match(/^[$@%]\w+/)) { // PG Variables
+ if (PGvars.has(stream.current().substring(1)))
+ return PGstyle;
+ return "variable";
+ } else if (stream.match(/^[$@%]{\w+}/)) { // ${foo} PG variables
+ if (PGvars.has(stream.current().slice(2,-1)))
+ return PGstyle;
+ return "variable";
+ } else if (stream.match(/^ +$/)) { // Trailing white space
+ return "trailingspace";
+ } else if (stream.match(/^[\[\]\\ (){}$@%`]/)) { // Advance a single character if special
+ return style;
+ } else { // Otherwise advance through all non special characters
+ if (prevState && prevState.mode == "cmd") { // Only eat through words in perl code mode
+ if (stream.match(/\w+/))
+ return style;
+ else
+ stream.next();
+ } else {
+ stream.eatWhile(/[^\[\]\\ (){}$@%`]/);
+ }
+ }
+ return style;
+ }
+ return state.tokenize(stream,state);
+ }
+
+ // No additional formatting inside comment block, only looks for end string.
+ // Currently only used for comments and ``` code blocks.
+ // The final stream.match and stream.eatWhile may need updated if used for other blocks.
+ function tokenPGMLComment(stream,state,string,style,prevState){
+ state.tokenize = function(stream,state) {
+ var reg = new RegExp("^"+string);
+ if (stream.match(reg)) {
+ state.tokenize = function(stream,state) {
+ return tokenPGML(stream,state,prevState.string,
+ prevState.style,prevState.prevState);
+ }
+ return style;
+ } else {
+ state.tokenize = function(stream,state) {
+ return tokenPGMLComment(stream,state,string,style,prevState);
+ }
+ }
+ if (stream.match(/^[\]%`]/))
+ return style;
+ stream.eatWhile(/[^\]%`]/);
+ return style;
+ }
+ return state.tokenize(stream,state);
+ }
+
+ // PGML subblock which has limited formatting options compared to main block.
+ // This block nests {} and [] blocks, for correct pairing in variables and commands.
+ function tokenPGMLSubBlock(stream,state,string,style,prevState){
+ state.tokenize = function(stream,state) {
+ var reg = new RegExp("^"+string);
+ if (stream.match(reg)) {
+ // Needed to ensure ': ' verbatim lines exit out if ended with a secondary subblock.
+ if (stream.eol() && prevState.subblock && prevState.prevState && prevState.prevState.stopeol) {
+ state.tokenize = function(stream,state) {
+ return tokenPGMLSubBlock(stream,state,prevState.prevState.string,
+ prevState.prevState.style,prevState.prevState.prevState);
+ }
+ } else if (stream.eol() && prevState.prevState && prevState.prevState.stopeol) {
+ state.tokenize = function(stream,state) {
+ return tokenPGML(stream,state,prevState.prevState.string,
+ prevState.prevState.style,prevState.prevState.prevState);
+ }
+ } else if (prevState.subblock){
+ state.tokenize = function(stream,state) {
+ return tokenPGMLSubBlock(stream,state,prevState.string,
+ prevState.style,prevState.prevState);
+ }
+ } else {
+ state.tokenize = function(stream,state) {
+ return tokenPGML(stream,state,prevState.string,
+ prevState.style,prevState.prevState);
+ }
+ }
+ if (prevState.mode == "var" || prevState.mode == "cmd")
+ stream.match(/^\*{1,3}([^\*]|$)/);
+ if (prevState.mode == "calc") {
+ if (!stream.match(/^\*(\s|$)/))
+ stream.match(/^\{.+\}/);
+ }
+ if (prevState.endstyle)
+ return prevState.endstyle;
+ return style;
+ } else {
+ state.tokenize = function(stream,state) {
+ return tokenPGMLSubBlock(stream,state,string,style,prevState);
+ }
+ }
+
+ var newPrevState = {};
+ if (prevState) {
+ newPrevState.prevState = JSON.parse(JSON.stringify(prevState));
+ } else {
+ newPrevState.prevState = null;
+ }
+ newPrevState.style = style;
+ newPrevState.string = string;
+ newPrevState.subblock = true;
+ if (prevState.mode)
+ newPrevState.mode = prevState.mode;
+
+ if (prevState.mode == "cmd") { // Some formatting for [@ ... @] blocks
+ if (stream.match(/^[$@%]\w+/)) { // $, @, % variables
+ if (PGvars.has(stream.current().substring(1)))
+ return PGstyle;
+ else
+ return "variable";
+ }
+ if (stream.match(/^[$@%]{\w+}/)) { // ${foo}, @{foo}, %{foo} variables
+ if (PGvars.has(stream.current().slice(2,-1)))
+ return PGstyle;
+ else
+ return "variable";
+ }
+ if (stream.match(/^[$@%]{/)) { // ${, @{, %{ nested blocks
+ style = "variable";
+ state.tokenize = function(stream,state) {
+ return tokenPGMLSubBlock(stream,state,"\\}",style,newPrevState);
+ }
+ return style;
+ }
+ if (stream.match(/^\(/)) { // Nested ( ) blocks
+ newPrevState.endstyle = "variable";
+ state.tokenize = function(stream,state) {
+ return tokenPGMLSubBlock(stream,state,"\\)",style,newPrevState);
+ }
+ return "variable";
+ }
+ if (stream.match(/^\w+/)) { // Check for PG keywords
+ if (PGcmds.has(stream.current()))
+ return PGkeyword;
+ else
+ return style;
+ }
+ if (stream.match(/^['"]/)) { // Quotes
+ return tokenChain(stream,state,[stream.current()],"string",null,function(stream,state) {
+ tokenPGMLSubBlock(stream,state,string,style,prevState) });
+ }
+ if (stream.match(/^[=,;/\*><%$&|.~?:]/)) // Catch some perl operators
+ return "variable";
+ }
+
+ if (stream.match(/^\[\$/)) { // Variable
+ const p = stream.pos;
+ if (stream.match(/^\w+/) && PGvars.has(stream.current().substring(2)) && stream.eat(']')) {
+ stream.match(/^\*{1,3}/);
+ return PGstyle;
+ } else {
+ stream.pos = p;
+ }
+ style = "variable";
+ state.tokenize = function(stream,state) {
+ newPrevState.mode = "var";
+ return tokenPGMLSubBlock(stream,state,"\\]",style,newPrevState);
+ }
+ } else if (stream.match(/^\[/)) { // Nested [ ] blocks
+ if (prevState.mode == "cmd")
+ newPrevState.endstyle = "variable";
+ state.tokenize = function(stream,state) {
+ return tokenPGMLSubBlock(stream,state,"\\]",style,newPrevState);
+ }
+ if (prevState.mode == "cmd")
+ return "variable";
+ } else if (stream.match(/^\{/)) { // Nested { } blocks
+ if (prevState.mode == "cmd")
+ newPrevState.endstyle = "variable";
+ state.tokenize = function(stream,state) {
+ return tokenPGMLSubBlock(stream,state,"\\}",style,newPrevState);
+ }
+ if (prevState.mode == "cmd")
+ return "variable";
+ } else if (stream.match(/^\w+\s*/)) { // Grab next word before going forward
+ return style;
+ } else { // Catchall to advanced one character if no match was found.
+ stream.eat(/./);
+ }
+ return style;
+ }
+ return state.tokenize(stream,state);
+ }
+
+ // Main PGML block. Can nest to allow subblocks with PGML formatting in them.
+ function tokenPGML(stream,state,string,style,prevState){
+ state.tokenize = function(stream,state) {
+ var reg = new RegExp("^"+string);
+ if (stream.match(reg)) {
+ if (!prevState) {
+ state.tokenize = tokenPerl;
+ return PGkeyword;
+ // Needed to ensure ': ' verbatim lines exit out if ended with a secondary block.
+ } else if (stream.eol() && prevState.prevState && prevState.prevState.stopeol) {
+ state.tokenize = function(stream,state) {
+ return tokenPGML(stream,state,prevState.prevState.string,
+ prevState.prevState.style,prevState.prevState.prevState);
+ }
+ } else {
+ state.tokenize = function(stream,state) {
+ return tokenPGML(stream,state,prevState.string,
+ prevState.style,prevState.prevState);
+ }
+ }
+ return style;
+ } else {
+ state.tokenize = function(stream,state) {
+ return tokenPGML(stream,state,string,style,prevState);
+ }
+ }
+
+ var newPrevState = {};
+ if (prevState) {
+ newPrevState.prevState = JSON.parse(JSON.stringify(prevState));
+ } else {
+ newPrevState.prevState = null;
+ }
+ newPrevState.style = style;
+ newPrevState.string = string;
+
+ if (stream.sol()) {
+ if (stream.match(/^ *(>> +)?[ivxlIVXL]+[.)] /) ||
+ stream.match(/^ *(>> +)?\d+[.)] /) ||
+ stream.match(/^ *(>> +)?\w[.)] /) ||
+ stream.match(/^ *(>> +)?[*\-+o] /)) { // Lists
+ return "atom strong";
+ }
+ if (stream.match(/^ *[\-=]{3,}/)) { // Rules
+ stream.match(/^\{[^}]*\}/);
+ stream.match(/^\{[^}]*\}/);
+ return "hr";
+ }
+ if (stream.match(/^ *(>> +)?#{1,}.*$/)) // Headers
+ return "header";
+ if (stream.match(/^ *>> /)) // Justification
+ return "atom strong";
+ if (stream.match(/^ *: /)) { // Single line verbatim
+ style = "tag";
+ state.tokenize = function(stream,state) {
+ newPrevState.stopeol = true;
+ return tokenPGML(stream,state,".$",style,newPrevState);
+ }
+ return style;
+ }
+ }
+
+ if (stream.match(/^\[:{1,3}/)) { // Algebra notation math
+ style = "variable-3";
+ const endstring = stream.current().substring(1) + "\\]";
+ state.tokenize = function(stream,state) {
+ newPrevState.mode = "calc";
+ return tokenPGMLSubBlock(stream,state,endstring,style,newPrevState);
+ }
+ } else if (stream.match(/^\[`{1,3}/)) { // TeX notation math
+ style = "comment";
+ const endstring = stream.current().substring(1) + "\\]";
+ state.tokenize = function(stream,state) {
+ newPrevState.mode = "tex";
+ return tokenPGMLSubBlock(stream,state,endstring,style,newPrevState);
+ }
+ } else if (stream.match(/^\[\|+/)) { // Verbatim
+ style = "tag";
+ const endstring = stream.current().substring(1) + "\\]";
+ state.tokenize = function(stream,state) {
+ return tokenPGML(stream,state,endstring,style,newPrevState);
+ }
+ } else if (!prevState && stream.match(/^```/)) { // Multiline verbatim / code
+ style = "tag";
+ state.tokenize = function(stream,state) {
+ return tokenPGMLComment(stream,state,"```",style,newPrevState);
+ }
+ } else if (stream.match(/^\[%/)) { // Comment
+ style = "bracket";
+ state.tokenize = function(stream,state) {
+ return tokenPGMLComment(stream,state,"%\\]",style,newPrevState);
+ }
+ } else if (stream.match(/^\[@/)) { // Perl code
+ style = "variable-2";
+ state.tokenize = function(stream,state) {
+ newPrevState.mode = "cmd";
+ return tokenPGMLSubBlock(stream,state,"@\\]",style,newPrevState);
+ }
+ } else if (stream.match(/^\[\$/)) { // Variable
+ const p = stream.pos;
+ if (stream.match(/^[\w\d_]+/) && PGvars.has(stream.current().substring(2)) && stream.eat(']')) {
+ stream.match(/^\*{1,3}/);
+ return PGstyle;
+ } else {
+ stream.pos = p;
+ }
+ style = "variable";
+ state.tokenize = function(stream,state) {
+ newPrevState.mode = "var";
+ return tokenPGMLSubBlock(stream,state,"\\]",style,newPrevState);
+ }
+ } else if (stream.match(/^\[_+\]/)) { // Answer blank
+ if (stream.match(/^\*?\{/)) {
+ state.tokenize = function(stream,state) {
+ return tokenPGMLSubBlock(stream,state,"\\}","builtin",newPrevState);
+ }
+ }
+ return "builtin";
+ } else if (stream.match(/<< *$/)) { // Justification
+ return "atom strong";
+ } else if (stream.match(/^(\*_|_\*)\w/)) { // Bold and italic
+ style = style + " strong em";
+ const endstring = (stream.current().charAt(1) + stream.current().charAt(0)).replace(/\*/, "\\*");
+ state.tokenize = function(stream,state) {
+ return tokenPGML(stream,state,endstring,style,newPrevState);
+ }
+ } else if (stream.match(/^\*{1,3}\w/)) { // Bold
+ style = style + " strong";
+ const endstring = stream.current().slice(0,-1).replace(/\*/g, "\\*");
+ state.tokenize = function(stream,state) {
+ return tokenPGML(stream,state,endstring,style,newPrevState);
+ }
+ } else if (stream.match(/^_{1,3}\w/)) { // Italic
+ style = style + " em";
+ const endstring = stream.current().slice(0,-1);
+ state.tokenize = function(stream,state) {
+ return tokenPGML(stream,state,endstring,style,newPrevState);
+ }
+ } else if (stream.match(/^ +$/)) { // Trailing whitespace
+ return "trailingspace";
+ } else if (stream.match(/[A-Za-z0-9]+\s*/)) { // Grab next word before going forward
+ return style;
+ } else { // Catchall to advanced one character if no match was found.
+ stream.eat(/./);
+ }
+ return style;
+ };
+
+ return state.tokenize(stream,state);
+ }
+
+ function tokenPerl(stream,state){
+ if(stream.eatSpace())
+ return null;
+ if(state.chain)
+ return tokenChain(stream,state,state.chain,state.style,state.tail);
+ if(stream.match(/^(\-?((\d[\d_]*)?\.\d+(e[+-]?\d+)?|\d+\.\d*)|0x[\da-fA-F_]+|0b[01_]+|\d[\d_]*(e[+-]?\d+)?)/))
+ return 'number';
+ if(stream.match(/^<<(?=[_a-zA-Z])/)){ // NOTE: <"],RXstyle,RXmodifiers);}
+ if(/[\^'"!~\/]/.test(c)){
+ eatSuffix(stream, 1);
+ return tokenChain(stream,state,[stream.eat(c)],RXstyle,RXmodifiers);}}
+ else if(c=="q"){
+ c=look(stream, 1);
+ if(c=="("){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,[")"],"string");}
+ if(c=="["){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,["]"],"string");}
+ if(c=="{"){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,["}"],"string");}
+ if(c=="<"){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,[">"],"string");}
+ if(/[\^'"!~\/]/.test(c)){
+ eatSuffix(stream, 1);
+ return tokenChain(stream,state,[stream.eat(c)],"string");}}
+ else if(c=="w"){
+ c=look(stream, 1);
+ if(c=="("){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,[")"],"bracket");}
+ if(c=="["){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,["]"],"bracket");}
+ if(c=="{"){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,["}"],"bracket");}
+ if(c=="<"){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,[">"],"bracket");}
+ if(/[\^'"!~\/]/.test(c)){
+ eatSuffix(stream, 1);
+ return tokenChain(stream,state,[stream.eat(c)],"bracket");}}
+ else if(c=="r"){
+ c=look(stream, 1);
+ if(c=="("){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,[")"],RXstyle,RXmodifiers);}
+ if(c=="["){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,["]"],RXstyle,RXmodifiers);}
+ if(c=="{"){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,["}"],RXstyle,RXmodifiers);}
+ if(c=="<"){
+ eatSuffix(stream, 2);
+ return tokenChain(stream,state,[">"],RXstyle,RXmodifiers);}
+ if(/[\^'"!~\/]/.test(c)){
+ eatSuffix(stream, 1);
+ return tokenChain(stream,state,[stream.eat(c)],RXstyle,RXmodifiers);}}
+ else if(/[\^'"!~\/(\[{<]/.test(c)){
+ if(c=="("){
+ eatSuffix(stream, 1);
+ return tokenChain(stream,state,[")"],"string");}
+ if(c=="["){
+ eatSuffix(stream, 1);
+ return tokenChain(stream,state,["]"],"string");}
+ if(c=="{"){
+ eatSuffix(stream, 1);
+ return tokenChain(stream,state,["}"],"string");}
+ if(c=="<"){
+ eatSuffix(stream, 1);
+ return tokenChain(stream,state,[">"],"string");}
+ if(/[\^'"!~\/]/.test(c)){
+ return tokenChain(stream,state,[stream.eat(c)],"string");}}}}
+ if(ch=="m"){
+ var c=look(stream, -2);
+ if(!(c&&/\w/.test(c))){
+ c=stream.eat(/[(\[{<\^'"!~\/]/);
+ if(c){
+ if(/[\^'"!~\/]/.test(c)){
+ return tokenChain(stream,state,[c],RXstyle,RXmodifiers);}
+ if(c=="("){
+ return tokenChain(stream,state,[")"],RXstyle,RXmodifiers);}
+ if(c=="["){
+ return tokenChain(stream,state,["]"],RXstyle,RXmodifiers);}
+ if(c=="{"){
+ return tokenChain(stream,state,["}"],RXstyle,RXmodifiers);}
+ if(c=="<"){
+ return tokenChain(stream,state,[">"],RXstyle,RXmodifiers);}}}}
+ if(ch=="s"){
+ var c=/[\/>\]})\w]/.test(look(stream, -2));
+ if(!c){
+ c=stream.eat(/[(\[{<\^'"!~\/]/);
+ if(c){
+ if(c=="[")
+ return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers);
+ if(c=="{")
+ return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers);
+ if(c=="<")
+ return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers);
+ if(c=="(")
+ return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers);
+ return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers);}}}
+ if(ch=="y"){
+ var c=/[\/>\]})\w]/.test(look(stream, -2));
+ if(!c){
+ c=stream.eat(/[(\[{<\^'"!~\/]/);
+ if(c){
+ if(c=="[")
+ return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers);
+ if(c=="{")
+ return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers);
+ if(c=="<")
+ return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers);
+ if(c=="(")
+ return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers);
+ return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers);}}}
+ if(ch=="t"){
+ var c=/[\/>\]})\w]/.test(look(stream, -2));
+ if(!c){
+ c=stream.eat("r");if(c){
+ c=stream.eat(/[(\[{<\^'"!~\/]/);
+ if(c){
+ if(c=="[")
+ return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers);
+ if(c=="{")
+ return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers);
+ if(c=="<")
+ return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers);
+ if(c=="(")
+ return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers);
+ return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers);}}}}
+ if(ch=="`"){
+ return tokenChain(stream,state,[ch],"variable-2");}
+ if(ch=="/"){
+ if(!/~\s*$/.test(prefix(stream)))
+ return "operator";
+ else
+ return tokenChain(stream,state,[ch],RXstyle,RXmodifiers);}
+ if(ch=="$"){
+ var p=stream.pos;
+ if(stream.eatWhile(/\w/)&&PGvars.has(stream.current().substring(1)))
+ return PGstyle;
+ else
+ stream.pos=p;
+ if(stream.eatWhile(/\d/)||stream.eat("{")&&stream.eatWhile(/\d/)&&stream.eat("}"))
+ return "variable-2";
+ else
+ stream.pos=p;}
+ if(/[$@%]/.test(ch)){
+ var p=stream.pos;
+ if(stream.eat("^")&&stream.eat(/[A-Z]/)||!/[@$%&]/.test(look(stream, -2))&&stream.eat(/[=|\\\-#?@;:&`~\^!\[\]*'"$+.,\/<>()]/)){
+ var c=stream.current();
+ if(PERL[c])
+ return "variable-2";}
+ stream.pos=p;}
+ if(/[$@%&]/.test(ch)){
+ if(stream.eatWhile(/[\w$]/)||stream.eat("{")&&stream.eatWhile(/[\w$]/)&&stream.eat("}")){
+ var c=stream.current();
+ if(PERL[c])
+ return "variable-2";
+ else
+ return "variable";}}
+ if(ch=="#"){
+ if(look(stream, -2)!="$"){
+ stream.skipToEnd();
+ return "comment";}}
+ if(ch=="-"&&look(stream, -2)!=" "&&stream.match(/>\w+/))
+ return "variable";
+ if(/[:+\-\^*$&%@=<>!?|\/~\.]/.test(ch)){
+ var p=stream.pos;
+ stream.eatWhile(/[:+\-\^*$&%@=<>!?|\/~\.]/);
+ if(PERL[stream.current()])
+ return "operator";
+ else
+ stream.pos=p;}
+ if(ch=="_"){
+ if(stream.pos==1){
+ if(suffix(stream, 6)=="_END__"){
+ return tokenChain(stream,state,['\0'],"comment");}
+ else if(suffix(stream, 7)=="_DATA__"){
+ return tokenChain(stream,state,['\0'],"variable-2");}
+ else if(suffix(stream, 7)=="_C__"){
+ return tokenChain(stream,state,['\0'],"string");}}}
+ if(/\w/.test(ch)){
+ var p=stream.pos;
+ if(look(stream, -2)=="{"&&(look(stream, 0)=="}"||stream.eatWhile(/\w/)&&look(stream, 0)=="}"))
+ return "string";
+ else
+ stream.pos=p;
+ if(stream.match(/\w* *=>/))
+ return "string";}
+ if(/[A-Z]/.test(ch)){
+ var l=look(stream, -2);
+ var p=stream.pos;
+ stream.eatWhile(/[A-Z_]/);
+ if(/[\da-z]/.test(look(stream, 0))){
+ stream.pos=p;}
+ else{
+ var c=PERL[stream.current()];
+ var isPG = PGcmds.has(stream.current());
+ if(!c && !isPG)
+ return "meta";
+ if(isPG)
+ return PGkeyword;
+ if(c[1])
+ c=c[0];
+ if(l!=":"){
+ if(c==1)
+ return "keyword";
+ else if(c==2)
+ return "def";
+ else if(c==3)
+ return "atom";
+ else if(c==4)
+ return "operator";
+ else if(c==5)
+ return "variable-2";
+ else
+ return "meta";}
+ else
+ return "meta";}}
+ if(/[a-zA-Z_]/.test(ch)){
+ var l=look(stream, -2);
+ stream.eatWhile(/\w/);
+ var c=PERL[stream.current()];
+ var isPG = PGcmds.has(stream.current());
+ if(!c && !isPG)
+ return "meta";
+ if(isPG)
+ return PGkeyword;
+ if(c[1])
+ c=c[0];
+ if(l!=":"){
+ if(c==1)
+ return "keyword";
+ else if(c==2)
+ return "def";
+ else if(c==3)
+ return "atom";
+ else if(c==4)
+ return "operator";
+ else if(c==5)
+ return "variable-2";
+ else
+ return "meta";}
+ else
+ return "meta";}
+ return null;}
+
+ return {
+ startState: function() {
+ return {
+ tokenize: tokenPerl,
+ chain: null,
+ style: null,
+ tail: null
+ };
+ },
+ token: function(stream, state) {
+ return (state.tokenize || tokenPerl)(stream, state);
+ },
+ lineComment: '#'
+ };
+ });
+
+ CodeMirror.registerHelper("wordChars", "perl", /[\w$]/);
+
+ CodeMirror.defineMIME("text/x-perl", "perl");
+
+ // it's like "peek", but need for look-ahead or look-behind if index < 0
+ function look(stream, c){
+ return stream.string.charAt(stream.pos+(c||0));
+ }
+
+ // return a part of prefix of current stream from current position
+ function prefix(stream, c){
+ if(c){
+ var x=stream.pos-c;
+ return stream.string.substr((x>=0?x:0),c);}
+ else{
+ return stream.string.substr(0,stream.pos-1);
+ }
+ }
+
+ // return a part of suffix of current stream from current position
+ function suffix(stream, c){
+ var y=stream.string.length;
+ var x=y-stream.pos+1;
+ return stream.string.substr(stream.pos,(c&&c=(y=stream.string.length-1))
+ stream.pos=y;
+ else
+ stream.pos=x;
+ }
+
+})();
diff --git a/public/js/apps/PGCodeMirror/pgeditor.js b/public/js/apps/PGCodeMirror/pgeditor.js
new file mode 100644
index 000000000..10d8573c8
--- /dev/null
+++ b/public/js/apps/PGCodeMirror/pgeditor.js
@@ -0,0 +1,123 @@
+/* WeBWorK Online Homework Delivery System
+ * Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of either: (a) the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any later
+ * version, or (b) the "Artistic License" which comes with this package.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
+ * Artistic License for more details.
+ */
+
+(async function () {
+ if (!CodeMirror) return;
+
+ const loadResource = async (src) => {
+ return new Promise((resolve, reject) => {
+ let shouldAppend = false;
+ let el;
+ if (/\.js(?:\?[0-9a-zA-Z=^.]*)?$/.exec(src)) {
+ el = document.querySelector(`script[src="${src}"]`);
+ if (!el) {
+ el = document.createElement('script');
+ el.async = false;
+ el.src = src;
+ shouldAppend = true;
+ }
+ } else if (/\.css(?:\?[0-9a-zA-Z=^.]*)?$/.exec(src)) {
+ el = document.querySelector(`link[href="${src}"]`);
+ if (!el) {
+ el = document.createElement('link');
+ el.rel = 'stylesheet';
+ el.href = src;
+ shouldAppend = true;
+ }
+ } else {
+ reject();
+ return;
+ }
+
+ if (el.dataset.loaded) {
+ resolve();
+ return;
+ }
+
+ el.addEventListener('error', reject);
+ el.addEventListener('abort', reject);
+ el.addEventListener('load', () => {
+ if (el) el.dataset.loaded = 'true';
+ resolve();
+ });
+
+ if (shouldAppend) document.head.appendChild(el);
+ });
+ };
+
+ const loadConfig = async (file) => {
+ const configName = [...file.matchAll(/.*\/([^.]*?)(?:\.min)?\.(?:js|css)(?:\?[0-9a-zA-Z=^.]*)?$/g)][0]?.[1]
+ ?? 'default';
+ if (configName !== 'default') {
+ try {
+ await loadResource(file);
+ } catch {
+ return 'default';
+ }
+ }
+ return configName;
+ };
+
+ const cm = webworkConfig.pgCodeMirror = CodeMirror.fromTextArea(document.querySelector('.codeMirrorEditor'), {
+ mode: document.querySelector('.codeMirrorEditor')?.dataset.mode ?? 'PG',
+ indentUnit: 4,
+ tabMode: 'spaces',
+ lineNumbers: true,
+ lineWrapping: true,
+ extraKeys: { Tab: (cm) => cm.execCommand('insertSoftTab') },
+ highlightSelectionMatches: { annotateScrollbar: true },
+ matchBrackets: true,
+ inputStyle: 'contenteditable',
+ spellcheck: localStorage.getItem('WW_PGEditor_spellcheck') === 'true',
+ });
+ cm.setSize('100%', '550px');
+
+ const currentThemeFile = localStorage.getItem('WW_PGEditor_selected_theme') ?? 'default';
+ const currentThemeName = await loadConfig(currentThemeFile);
+ cm.setOption('theme', currentThemeName);
+
+ const currentKeymapFile = localStorage.getItem('WW_PGEditor_selected_keymap') ?? 'default';
+ const currentKeymapName = await loadConfig(currentKeymapFile);
+ cm.setOption('keyMap', currentKeymapName);
+
+ const selectTheme = document.getElementById('selectTheme');
+ selectTheme.value = currentThemeName === 'default' ? 'default' : currentThemeFile;
+ selectTheme.addEventListener('change', async () => {
+ const themeName = await loadConfig(selectTheme.value);
+ cm.setOption('theme', themeName);
+ localStorage.setItem('WW_PGEditor_selected_theme', themeName === 'default' ? 'default' : selectTheme.value);
+ });
+
+ const selectKeymap = document.getElementById('selectKeymap');
+ selectKeymap.value = currentKeymapName === 'default' ? 'default' : currentKeymapFile;
+ selectKeymap.addEventListener('change', async () => {
+ const keymapName = await loadConfig(selectKeymap.value);
+ cm.setOption('keyMap', keymapName);
+ localStorage.setItem('WW_PGEditor_selected_keymap',
+ keymapName === 'default' ? 'default' : selectKeymap.value);
+ });
+
+ const enableSpell = document.getElementById('enableSpell');
+ enableSpell.checked = localStorage.getItem('WW_PGEditor_spellcheck') === 'true';
+ enableSpell.addEventListener('change', () => {
+ cm.setOption('spellcheck', enableSpell.checked);
+ localStorage.setItem('WW_PGEditor_spellcheck', enableSpell.checked);
+ cm.focus();
+ });
+
+ const forceRTL = document.getElementById('forceRTL');
+ forceRTL.addEventListener('change', () => {
+ cm.setOption('direction', forceRTL.checked ? 'rtl' : 'ltr');
+ });
+})();
diff --git a/public/js/apps/PGCodeMirror/pgeditor.scss b/public/js/apps/PGCodeMirror/pgeditor.scss
new file mode 100644
index 000000000..ea12c09fa
--- /dev/null
+++ b/public/js/apps/PGCodeMirror/pgeditor.scss
@@ -0,0 +1,64 @@
+/* WeBWorK Online Homework Delivery System
+ * Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of either: (a) the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any later
+ * version, or (b) the "Artistic License" which comes with this package.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
+ * Artistic License for more details.
+ */
+
+.CodeMirror {
+ border: 1px solid #ddd;
+ min-height: 400px;
+ resize: vertical;
+}
+
+// This style is only used if the CodeMirror editor is disabled in localOverrides.conf.
+.codeMirrorEditor {
+ border: 1px solid #ddd;
+ padding: 2px;
+ height: 550px;
+ min-height: 400px;
+ width: 100%;
+ resize: vertical;
+}
+
+// Additional CSS for codemirror addons and overrides
+
+// CodeMirror overrides
+.CodeMirror-code {
+ outline: none;
+}
+
+pre.CodeMirror-line {
+ unicode-bidi: embed;
+}
+
+// Match Highligher CSS
+.CodeMirror-focused {
+ .cm-matchhighlight {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);
+ background-position: bottom;
+ background-repeat: repeat-x;
+ }
+}
+
+.cm-matchhighlight {
+ background-color: lightgreen;
+}
+
+.CodeMirror-selection-highlight-scrollbar {
+ background-color: green;
+}
+
+// CSS to highlight trailing whitespace in PGML blocks
+.cm-trailingspace {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==);
+ background-position: bottom left;
+ background-repeat: repeat-x;
+}
diff --git a/public/Problem/problem.js b/public/js/apps/Problem/problem.js
similarity index 100%
rename from public/Problem/problem.js
rename to public/js/apps/Problem/problem.js
diff --git a/public/Problem/submithelper.js b/public/js/apps/Problem/submithelper.js
similarity index 100%
rename from public/Problem/submithelper.js
rename to public/js/apps/Problem/submithelper.js
diff --git a/public/package.json b/public/package.json
new file mode 100644
index 000000000..aa0b34c35
--- /dev/null
+++ b/public/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "renderer.javascript_package_manager",
+ "description": "Third party javascript for Standalone Renderer",
+ "license": "GPL-2.0+",
+ "scripts": {
+ "generate-assets": "node generate-assets",
+ "prepare": "npm run generate-assets"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/openwebwork/renderer"
+ },
+ "dependencies": {
+ "@fortawesome/fontawesome-free": "^6.2.1",
+ "bootstrap": "~5.2.3",
+ "codemirror": "^5.65.11",
+ "iframe-resizer": "^4.3.2",
+ "jquery": "^3.6.3",
+ "jquery-ui-dist": "^1.13.2",
+ "mathjax": "^3.2.2"
+ },
+ "devDependencies": {
+ "autoprefixer": "^10.4.13",
+ "chokidar": "^3.5.3",
+ "cssnano": "^6.0.0",
+ "postcss": "^8.4.21",
+ "rtlcss": "^4.0.0",
+ "sass": "^1.57.1",
+ "terser": "^5.16.1",
+ "yargs": "^17.6.2"
+ },
+ "browserslist": [
+ "last 10 Chrome versions",
+ "last 10 Firefox versions",
+ "last 4 Edge versions",
+ "last 7 Safari versions",
+ "last 8 Android versions",
+ "last 8 ChromeAndroid versions",
+ "last 8 FirefoxAndroid versions",
+ "last 10 iOS versions",
+ "last 5 Opera versions"
+ ]
+}
diff --git a/templates/RPCRenderFormats/default.html.ep b/templates/RPCRenderFormats/default.html.ep
new file mode 100644
index 000000000..9a58ec1d6
--- /dev/null
+++ b/templates/RPCRenderFormats/default.html.ep
@@ -0,0 +1,144 @@
+% use WeBWorK::Utils qw(getAssetURL wwRound);
+%
+
+>
+
+
+
+
+ WeBWorK using host: <%= $SITE_URL %>,
+ format: <%= $formatName %>,
+
+ " rel="shortcut icon">
+ % # Add third party css and javascript as well as css and javascript requested by the problem.
+ % for (@$third_party_css) {
+ %= stylesheet $_
+ % }
+ % for (@$extra_css_files) {
+ %= stylesheet $_->{file}
+ % }
+ % for (@$third_party_js) {
+ %= javascript $_->[0], %{ $_->[1] // {} }
+ % }
+ % for (@$extra_js_files) {
+ %= javascript $_->{file}, %{ $_->{attributes} }
+ % }
+ %== $rh_result->{header_text} // ''
+ %== $rh_result->{post_header_text} // ''
+ %== $extra_header_text
+
+
+
+
+
+ %== $answerTemplate
+ <%= form_for $FORM_ACTION_URL, id => 'problemMainForm', class => 'problem-main-form',
+ name => 'problemMainForm', method => 'POST', begin %>
+
>
+ %== $problemText
+
+ % if ($showScoreSummary) {
+
<%= $lh->maketext('You received a score of [_1] for this attempt.',
+ wwRound(0, $rh_result->{problem_result}{score} * 100) . '%') %>
+ % if ($rh_result->{problem_result}{msg}) {
+
<%= $rh_result->{problem_result}{msg} %>
+ % }
+ <%= hidden_field 'problem-result-score' => $rh_result->{problem_result}{score},
+ id => 'problem-result-score' %>
+ % }
+ %= hidden_field sessionJWT => $rh_result->{sessionJWT}
+ % if ($rh_result->{JWTanswerURLstatus}) {
+ %= hidden_field JWTanswerURLstatus => $rh_result->{JWTanswerURLstatus}
+ % }
+ % if ($formatName eq 'debug' && $rh_result->{inputs_ref}{clientDebug}) {
+ %= hidden_field clientDebug => $rh_result->{inputs_ref}{clientDebug}
+ % }
+ % if ($formatName ne 'static') {
+
+ % # Submit buttons (all are shown by default)
+ % if ($showPreviewButton ne '0') {
+ <%= submit_button $lh->maketext('Preview My Answers'),
+ name => 'previewAnswers', id => 'previewAnswers_id', class => 'btn btn-primary mb-1' %>
+ % }
+ % if ($showCheckAnswersButton ne '0') {
+ <%= submit_button $lh->maketext('Submit Answers'),
+ name => 'submitAnswers', class => 'btn btn-primary mb-1' %>
+ % }
+ % if ($showCorrectAnswersButton ne '0') {
+ <%= submit_button $lh->maketext('Show Correct Answers'),
+ name => 'showCorrectAnswers', class => 'btn btn-primary mb-1' %>
+ % }
+
+ % }
+ % end
+
+
+ % # PG warning messages (this includes translator warnings but not translator errors).
+ % if ($rh_result->{pg_warnings}) {
+
+
<%= $lh->maketext('Warning messages') %>
+
+ % for (split("\n", $rh_result->{pg_warnings})) {
+ <%== $_ %>
+ % }
+
+
+ % }
+ % # PG warning messages generated with WARN_message.
+ % if (ref $rh_result->{warning_messages} eq 'ARRAY' && @{ $rh_result->{warning_messages} }) {
+
+
<%= $lh->maketext('PG warning messages') %>
+
+ % for (@{ $rh_result->{warning_messages} }) {
+ <%== $_ %>
+ % }
+
+
+ % }
+ % # Translator errors.
+ % if ($rh_result->{flags}{error_flag}) {
+
+
Translator errors
+ <%== $rh_result->{errors} %>
+
+ % }
+ % # Additional information output only for the debug format.
+ % if ($formatName eq 'debug') {
+ % # PG debug messages generated with DEBUG_message.
+ % if (@{ $rh_result->{debug_messages} }) {
+
+
PG debug messages
+
+ % for (@{ $rh_result->{debug_messages} }) {
+ <%== $_ %>
+ % }
+
+
+ % }
+ % # Internal debug messages generated within PGcore.
+ % if (ref $rh_result->{internal_debug_messages} eq 'ARRAY' && @{ $rh_result->{internal_debug_messages} }) {
+
+
Internal errors
+
+ % for (@{ $rh_result->{internal_debug_messages} }) {
+ <%== $_ %>
+ % }
+
+
+ % }
+ % if ($rh_result->{inputs_ref}{clientDebug}) {
+
Webwork client data
+ %== $pretty_print->($rh_result)
+ % }
+ % }
+
+ % # Show the footer unless it is explicity disabled.
+ % if ($showFooter ne '0') {
+
+ % }
+
+
diff --git a/templates/RPCRenderFormats/default.json.ep b/templates/RPCRenderFormats/default.json.ep
new file mode 100644
index 000000000..1c10ff170
--- /dev/null
+++ b/templates/RPCRenderFormats/default.json.ep
@@ -0,0 +1,88 @@
+% use Mojo::JSON qw(to_json);
+% use WeBWorK::Utils qw(wwRound);
+%
+% my $json_output = {
+ % head_part001 => "",
+ % head_part010 => q{ }
+ % . qq{ },
+ % head_part300 => join('',
+ % (map { stylesheet($_) } @$third_party_css),
+ % (map { stylesheet($_->{file}) } @$extra_css_files),
+ % (map { javascript($_->[0], %{ $_->[1] // {} }) } @$third_party_js),
+ % (map { javascript($_->{file}, %{ $_->{attributes} }) } @$extra_js_files),
+ % $rh_result->{header_text} // '',
+ % $rh_result->{post_header_text} // '',
+ % $extra_header_text
+ % ),
+ % head_part400 => 'WeBWorK problem ',
+ % head_part999 => '',
+ %
+ % body_part001 => '',
+ % body_part100 => '',
+ % body_part300 => $answerTemplate,
+ % body_part500 => '
'
+ % . ($showFooter eq '0' ? ''
+ % : qq{")
+ % . '}',
+ %
+ % hidden_input_field => {
+ % answersSubmitted => '1',
+ % sourceFilePath => $sourceFilePath,
+ % problemSource => $problemSource,
+ % problemSeed => $problemSeed,
+ % problemUUID => $problemUUID,
+ % psvn => $psvn,
+ % pathToProblemFile => $fileName,
+ % courseID => $courseID,
+ % user => $user,
+ % passwd => $passwd,
+ % displayMode => $displayMode,
+ % key => $key,
+ % outputformat => 'json',
+ % theme => $theme,
+ % language => $formLanguage,
+ % showSummary => $showSummary,
+ % showHints => $showHints,
+ % showSolutions => $showSolutions,
+ % showAnswerNumbers => $showAnswerNumbers,
+ % showPreviewButton => $showPreviewButton,
+ % showCheckAnswersButton => $showCheckAnswersButton,
+ % showCorrectAnswersButton => $showCorrectAnswersButton,
+ % showFooter => $showFooter,
+ % extraHeaderText => $extra_header_text
+ % },
+ %
+ % # Add the current score to the json output
+ % score => $ws->{inputs_ref}{WWsubmit} && $rh_result->{problem_result}
+ % ? wwRound(0, $rh_result->{problem_result}{score} * 100)
+ % : 0,
+ %
+ % # These are the real WeBWorK server URLs which the intermediate needs to use
+ % # to communicate with WW, while the distant client must use URLs of the
+ % # intermediate server (the man in the middle).
+ % real_webwork_SITE_URL => $SITE_URL,
+ % real_webwork_FORM_ACTION_URL => $FORM_ACTION_URL,
+ % internal_problem_lang_and_dir => $PROBLEM_LANG_AND_DIR
+% };
+%
+%== to_json($json_output)
diff --git a/templates/RPCRenderFormats/ptx.html.ep b/templates/RPCRenderFormats/ptx.html.ep
new file mode 100644
index 000000000..c950e4a70
--- /dev/null
+++ b/templates/RPCRenderFormats/ptx.html.ep
@@ -0,0 +1,7 @@
+
+
+
+%== $problemText
+%== $answerhashXML
+
+