diff --git a/.gitignore b/.gitignore
index 6b17b8dba..e127c93b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,8 +9,12 @@ private/
tmp/*
!tmp/.gitkeep
logs/*.log
+
node_modules
-node_modules/*
+public/**/*.min.js
+public/**/*.min.css
+public/static-assets.json
+
*.o
*.pm.tdy
*.bs
diff --git a/Dockerfile b/Dockerfile
index ae6221b36..011fc4114 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -55,7 +55,7 @@ RUN cp render_app.conf.dist render_app.conf
RUN cp conf/pg_config.yml lib/PG/conf/pg_config.yml
-RUN npm install
+RUN cd public/ && npm install && cd ..
RUN cd lib/PG/htdocs && npm install && cd ../../..
diff --git a/README.md b/README.md
index 2a5a5b456..d1ae2f953 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,9 @@ If using a local install instead of docker:
* copy `render_app.conf.dist` to `render_app.conf` and make any desired modifications
* copy `conf/pg_config.yml` to `lib/PG/pg_config.yml` and make any desired modifications
* install third party JavaScript dependencies
+ * `cd private/`
* `npm install`
+ * `cd ..`
* install PG JavaScript dependencies
* `cd lib/PG/htdocs`
* `npm install`
@@ -70,59 +72,115 @@ If using a local install instead of docker:
![image](https://user-images.githubusercontent.com/3385756/129100124-72270558-376d-4265-afe2-73b5c9a829af.png)
+## Server Configuration
+
+Modification of `baseURL` may be necessary to separate multiple services running on `SITE_HOST`, and will be used to extend `SITE_HOST`. The result of this extension will serve as the root URL for accessing the renderer (and any supplementary assets it may need to provide in support of a rendered problem). If `baseURL` is an absolute URL, it will be used verbatim -- userful if the renderer is running behind a load balancer.
+
+By default, `formURL` will further extend `baseURL`, and serve as the form-data target for user interactions with problems rendered by this service. If `formURL` is an absolute URL, it will be used verbatim -- useful if your implementation intends to sit in between the user and the renderer.
+
## Renderer API
-Can be interfaced through `/render-api`
-
-## Parameters
-
-| Key | Type | Default Value | Required | Description | Notes |
-| --- | ---- | ------------- | -------- | ----------- | ----- |
-| problemSourceURL | string | null | true if `sourceFilePath` and `problemSource` are null | The URL from which to fetch the problem source code | Takes precedence over `problemSource` and `sourceFilePath`. A request to this URL is expected to return valid pg source code in base64 encoding. |
-| problemSource | string (base64 encoded) | null | true if `problemSourceURL` and `sourceFilePath` are null | The source code of a problem to be rendered | Takes precedence over `sourceFilePath`. |
-| sourceFilePath | string | null | true if `problemSource` and `problemSourceURL` are null | The path to the file that contains the problem source code | Can begin with Library/ or Contrib/, in which case the renderer will automatically adjust the path relative to the webwork-open-problem-library root. Path may also begin with `private/` for local, non-OPL content. |
-| problemSeed | number | NA | true | The seed to determine the randomization of a problem | |
-| psvn | number | 123 | false | used for consistent randomization between problems | |
-| formURL | string | /render-api | false | the URL for form submission | |
-| baseURL | string | / | false | the URL for relative paths | |
-| format | string | '' | false | Determine how the response is formatted ('html' or 'json') ||
-| outputFormat | string (enum) | static | false | Determines how the problem should render, see below descriptions below | |
-| language | string | en | false | Language to render the problem in (if supported) | |
-| showHints | number (boolean) | 1 | false | Whether or not to show hints | |
-| showSolutions | number (boolean) | 0 | false | Whether or not to show the solutions | |
-| permissionLevel | number | 0 | false | Deprecated. See below. |
-| isInstructor | number (boolean) | 0 | false | Is the user viewing the problem an instructor or not. | Used by PG to determine if scaffolds can be allowed to be open among other things |
-| problemNumber | number | 1 | false | We don't use this | |
-| numCorrect | number | 0 | false | The number of correct attempts on a problem | |
-| numIncorrect | number | 1000 | false | The number of incorrect attempts on this problem | |
-| processAnswers | number (boolean) | 1 | false | Determines whether or not answer json is populated, and whether or not problem_result and problem_state are non-empty | |
-| answersSubmitted | number (boolean) | ? | false? | Determines whether to process form-data associated to the available input fields | |
-| showSummary | number (boolean) | ? | false? | Determines whether or not to show the summary result of processing the form-data associated with `answersSubmitted` above ||
-| showComments | number (boolean) | 0 | false | Renders author comment field at the end of the problem ||
-| includeTags | number (boolean) | 0 | false | Includes problem tags in the returned JSON | Only relevant when requesting `format: 'json'` |
-
-## Output Format
-
-| Key | Description |
-| ----- | ----- |
-| static | zero buttons, locked form fields (read-only) |
-| nosubmit | zero buttons, editable (for exams, save problem state and submit all together) |
-| single | one submit button (intended for graded content) |
-| classic | preview + submit buttons |
-| simple | preview + submit + show answers buttons |
-| practice | check answers + show answers buttons |
-
-## Permission level
-
-| Key | Value |
-| --- | ----- |
-| student | 0 |
-| prof | 10 |
-| admin | 20 |
-
-## Permission logic summary
-
-* `permissionLevel` is ignored if `isInstructor` is directly set.
-* If `permissionLevel >= 10`, then `isInstructor` will be set to true.
-* If `permissionLevel < 10`, then `isInstructor` will be set to false.
-* `permissionLevel` is not used to determine if hints or solutions are shown.
+Can be accessed by POST to `{SITE_HOST}{baseURL}{formURL}`.
+
+By default, `localhost:3000/render-api`.
+
+### **REQUIRED PARAMETERS**
+
+The bare minimum of parameters that must be included are:
+* the code for the problem, so, **ONE** of the following (in order of precedence):
+ * `problemSource` (raw pg source code, _can_ be base64 encoded)
+ * `sourceFilePath` (relative to OPL `Library/`, `Contrib/`; or in `private/`)
+ * `problemSourceURL` (fetch the pg source from remote server)
+* a "seed" value for consistent randomization
+ * `problemSeed` (integer)
+
+| Key | Type | Description | Notes |
+| --- | ---- | ----------- | ----- |
+| problemSource | string (possibly base64 encoded) | The source code of a problem to be rendered | Takes precedence over `sourceFilePath`. |
+| sourceFilePath | string | The path to the file that contains the problem source code | Renderer will automatically adjust `Library/` and `Contrib/` relative to the webwork-open-problem-library root. Path may also begin with `private/` for local, non-OPL content. |
+| problemSourceURL | string | The URL from which to fetch the problem source code | Takes precedence over `problemSource` and `sourceFilePath`. A request to this URL is expected to return valid pg source code in base64 encoding. |
+| problemSeed | number | The seed that determines the randomization of a problem | |
+
+**ALL** other request parameters are optional.
+
+### Infrastructure Parameters
+
+The defaults for these parameters are set in `render_app.conf`, but these can be overridden on a per-request basis.
+
+| Key | Type | Default Value | Description | Notes |
+| --- | ---- | ------------- | ----------- | ----- |
+| baseURL | string | '/' (as set in `render_app.conf`) | the URL for relative paths | |
+| formURL | string | '/render-api' (as set in `render_app.conf`) | the URL for form submission | |
+
+### Display Parameters
+
+#### Formatting
+
+Parameters that control the structure and templating of the response.
+
+| Key | Type | Default Value | Description | Notes |
+| --- | ---- | ------------- | ----------- | ----- |
+| language | string | en | Language to render the problem in (if supported) | affects the translation of template strings, _not_ actual problem content |
+| _format | string | 'html' | Determine how the response is _structured_ ('html' or 'json') | usually 'html' if the user is directly interacting with the renderer, 'json' if your CMS sits between user and renderer |
+| outputFormat | string | 'default' | Determines how the problem should be formatted | 'default', 'static', 'PTX', 'raw', or |
+| displayMode | string | 'MathJax' | How to prepare math content for display | 'MathJax' or 'ptx' |
+
+#### User Interactions
+
+Control how the user is allowed to interact with the rendered problem.
+
+Requesting `outputFormat: 'static'` will prevent any buttons from being included in the rendered output, regardless of the following options.
+
+| Key | Type | Default Value | Description | Notes |
+| --- | ---- | ------------- | ----------- | ----- |
+| hidePreviewButton | number (boolean) | false | "Preview My Answers" is enabled by default | |
+| hideCheckAnswersButton | number (boolean) | false | "Submit Answers" is enabled by default | |
+| showCorrectAnswersButton | number (boolean) | `isInstructor` | "Show Correct Answers" is disabled by default, enabled if `isInstructor` is true (see below) | |
+
+#### Content
+
+Control what is shown to the user: hints, solutions, attempt results, scores, etc.
+
+| Key | Type | Default Value | Description | Notes |
+| --- | ---- | ------------- | ----------- | ----- |
+| permissionLevel | number | 0 | **DEPRECATED.** Use `isInstructor` instead. |
+| isInstructor | number (boolean) | 0 | Is the user viewing the problem an instructor or not. | Used by PG to determine if scaffolds can be allowed to be open among other things |
+| showHints | number (boolean) | 1 | Whether or not to show hints | |
+| showSolutions | number (boolean) | `isInstructor` | Whether or not to show the solutions | |
+| hideAttemptsTable | number (boolean) | 0 | Hide the table of answer previews/results/messages | If you have a replacement for flagging the submitted entries as correct/incorrect |
+| showSummary | number (boolean) | 1 | Determines whether or not to show a summary of the attempt underneath the table | Only relevant if the Attempts Table is shown `hideAttemptsTable: false` (default) |
+| showComments | number (boolean) | 0 | Renders author comment field at the end of the problem | |
+| showFooter | number (boolean) | 0 | Show version information and WeBWorK copyright footer | |
+| includeTags | number (boolean) | 0 | Includes problem tags in the returned JSON | Only relevant when requesting `_format: 'json'` |
+
+## Using JWTs
+
+There are three JWT structures that the Renderer uses, each containing its predecessor:
+* problemJWT
+* sessionJWT
+* answerJWT
+
+### ProblemJWT
+
+This JWT encapsulates the request parameters described above, under the API heading. Any value set in the JWT cannot be overridden by form-data. For example, if the problemJWT includes `isInstructor: 0`, then any subsequent interaction with the problem rendered by this JWT cannot override this setting by including `isInstructor: 1` in the form-data.
+
+### SessionJWT
+
+This JWT encapsulates a user's attempt on a problem, including:
+* the text and LaTeX versions of each answer entry
+* count of incorrect attempts (stopping after a correct attempt, or after `showCorrectAnswers` is used)
+* the problemJWT
+
+If stored (see next), this JWT can be submitted as the sole request parameter, and the response will effectively restore the users current state of interaction with the problem (as of their last submission).
+
+### AnswerJWT
+
+If the initial problemJWT contains a value for `JWTanswerURL`, this JWT will be generated and sent to the specified URL. The answerJWT is the only content provided to the URL. The renderer is intended to to be user-agnostic. It is recommended that the JWTanswerURL specify the unique identifier for the user/problem combination. (e.g. `JWTanswerURL: 'https://db.yoursite.org/grades-api/:user_problem_id'`)
+
+For security purposes, this parameter is only accepted when included as part of a JWT.
+
+This JWT encapsulates the status of the user's interaction with the problem.
+* score
+* sessionJWT
+
+The goal here is to update the `JWTanswerURL` with the score and "state" for the user. If you have uses for additional information, please feel free to suggest as a GitHub Issue.
diff --git a/conf/pg_config.yml b/conf/pg_config.yml
index e1863774d..950ac9570 100644
--- a/conf/pg_config.yml
+++ b/conf/pg_config.yml
@@ -40,6 +40,7 @@ directories:
# (in this order) by loadMacros when it looks for a .pl macro file.
macrosPath:
- .
+ - $render_root/private/macros
- $pg_root/macros
- $pg_root/macros/answers
- $pg_root/macros/capa
@@ -182,9 +183,6 @@ options:
# This is the operations file to use for mathview, each contains a different locale.
mathViewLocale: mv_locale_us.js
- # Set to 1 to show the WirisEditor preview system.
- useWirisEditor: 0
-
# Catch translation warnings internally.
catchWarnings: 1
diff --git a/lib/RenderApp.pm b/lib/RenderApp.pm
index f63095074..565feed56 100644
--- a/lib/RenderApp.pm
+++ b/lib/RenderApp.pm
@@ -3,31 +3,32 @@ use Mojo::Base 'Mojolicious';
BEGIN {
use Mojo::File;
- $main::dirname = Mojo::File::curfile->dirname;
+ $main::libname = Mojo::File::curfile->dirname;
# RENDER_ROOT is required for initializing conf files.
- $ENV{RENDER_ROOT} = $main::dirname->dirname
+ $ENV{RENDER_ROOT} = $main::libname->dirname
unless ( defined( $ENV{RENDER_ROOT} ) );
# PG_ROOT is required for PG/lib/PGEnvironment.pm, FormatRenderedProblem.pm, and RenderProblem.pm.
# This is hardcoded to avoid conflict with the environment variable for webwork2.
# There is no need for this to be configurable.
- $ENV{PG_ROOT} = $main::dirname . '/PG';
+ $ENV{PG_ROOT} = $main::libname . '/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';
# $ENV{MOJO_LOG_LEVEL} = 'debug';
}
-use lib "$main::dirname";
-print "home directory " . $main::dirname . "\n";
+use lib "$main::libname";
+print "using root directory: $ENV{RENDER_ROOT}\n";
use RenderApp::Model::Problem;
-use RenderApp::Controller::RenderProblem;
use RenderApp::Controller::IO;
+use WeBWorK::RenderProblem;
+use WeBWorK::FormatRenderedProblem;
sub startup {
my $self = shift;
@@ -40,16 +41,12 @@ sub startup {
$ENV{$_} //= $self->config($_);
};
- $ENV{baseURL} = '' if ( $ENV{baseURL} eq '/' );
- $ENV{SITE_HOST} =~ s|/$||; # remove trailing slash
-
- # $r needs to be defined before the SITE_HOST is added to the baseURL
+ sanitizeHostURLs();
+ # baseURL sets the root at which the renderer is listening, and is used in Environment for pg_root_url
my $r = $self->routes->under($ENV{baseURL});
- # while iFrame embedded problems are likely to need the baseURL to include SITE_HOST
- # convert to absolute URLs
- $ENV{baseURL} = $ENV{SITE_HOST} . $ENV{baseURL} unless ( $ENV{baseURL} =~ m|^https?://| );
- $ENV{formURL} = $ENV{baseURL} . $ENV{formURL} unless ( $ENV{formURL} =~ m|^https?://| );
+ print "Renderer is based at $main::basehref\n";
+ print "Problem attempts will be sent to $main::formURL\n";
# Handle optional CORS settings
if (my $CORS_ORIGIN = $self->config('CORS_ORIGIN')) {
@@ -66,6 +63,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 +105,30 @@ 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('/*static')->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)});
+sub sanitizeHostURLs {
+ $ENV{baseURL} = "/$ENV{baseURL}";
+ warn "*** Configuration error: baseURL should not end in a slash\n" if $ENV{baseURL} =~ s!/$!!;
+ warn "*** Configuration error: baseURL should begin with a slash\n" unless $ENV{baseURL} =~ s!^//!/!;
+
+ # set an absolute base href for iframe embedding
+ my $basehref = $ENV{baseURL} =~ m!/$! ? $ENV{baseURL} : "$ENV{baseURL}/";
+ my $baseURL = Mojo::URL->new($basehref);
+ $main::basehref = $baseURL->is_abs
+ ? $baseURL
+ : Mojo::URL->new($ENV{SITE_HOST})->path($baseURL);
+
+ # respect absolute form URLs for man-in-the-middle implementations
+ warn "*** Configuration error: formURL should not begin with a slash\n" if $ENV{formURL} =~ s!^/!!;
+ my $renderEndpoint = $ENV{formURL} || 'render-api';
+ my $formURL = Mojo::URL->new($renderEndpoint);
+ warn "*** Possible configuration error: are you sure you want to use $main::basehref$renderEndpoint as the render endpoint?\n"
+ unless $formURL->is_abs || $renderEndpoint eq 'render-api';
+ $main::formURL = $formURL->is_abs
+ ? $formURL
+ : Mojo::URL->new($ENV{SITE_HOST})->path($basehref.$renderEndpoint);
}
1;
diff --git a/lib/RenderApp/Controller/FormatRenderedProblem.pm b/lib/RenderApp/Controller/FormatRenderedProblem.pm
deleted file mode 100755
index a73928381..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 => $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 => $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 6e4966a16..5686e55c6 100644
--- a/lib/RenderApp/Controller/Render.pm
+++ b/lib/RenderApp/Controller/Render.pm
@@ -37,7 +37,6 @@ sub parseRequest {
foreach my $key (keys %$claims) {
$params{$key} //= $claims->{$key};
}
- # @params{ keys %$claims } = values %$claims;
}
# problemJWT sets basic problem request configuration and rendering options
@@ -57,9 +56,19 @@ 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 {
+ # if no JWT is provided, create one
+ $params{aud} = $ENV{SITE_HOST};
+ my $req_jwt = encode_jwt(
+ payload => \%params,
+ key => $ENV{problemJWTsecret},
+ alg => 'PBES2-HS512+A256KW',
+ enc => 'A256GCM',
+ auto_iat => 1
+ );
+ $params{problemJWT} = $req_jwt;
}
return \%params;
}
@@ -81,7 +90,7 @@ sub fetchRemoteSource_p {
then(
sub {
my $tx = shift;
- return encode_base64($tx->result->body);
+ return $tx->result->body;
})->
catch(
sub {
@@ -97,12 +106,11 @@ 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};
my $random_seed = $inputs_ref->{problemSeed};
- $inputs_ref->{baseURL} ||= $ENV{baseURL};
- $inputs_ref->{formURL} ||= $ENV{formURL};
my $problem_contents;
if ( $inputs_ref->{problemSource} && $inputs_ref->{problemSource} =~ /Mojo::Promise/ ) {
@@ -120,71 +128,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 {
@@ -205,11 +209,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 {
@@ -237,11 +242,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 {
@@ -280,7 +286,6 @@ sub jweFromRequest {
my $inputs_ref = $c->parseRequest;
return unless $inputs_ref;
$inputs_ref->{aud} = $ENV{SITE_HOST};
- $inputs_ref->{key} = $ENV{problemJWTsecret};
my $req_jwt = encode_jwt(
payload => $inputs_ref,
key => $ENV{problemJWTsecret},
@@ -296,7 +301,6 @@ sub jwtFromRequest {
my $inputs_ref = $c->parseRequest;
return unless $inputs_ref;
$inputs_ref->{aud} = $ENV{SITE_HOST};
- $inputs_ref->{key} = $ENV{problemJWTsecret};
my $req_jwt = encode_jwt(
payload => $inputs_ref,
key => $ENV{problemJWTsecret},
diff --git a/lib/RenderApp/Controller/StaticFiles.pm b/lib/RenderApp/Controller/StaticFiles.pm
index faf8bea20..9b0dc9ad7 100644
--- a/lib/RenderApp/Controller/StaticFiles.pm
+++ b/lib/RenderApp/Controller/StaticFiles.pm
@@ -28,4 +28,8 @@ sub pg_file ($c) {
$c->reply_with_file_if_readable(path($ENV{PG_ROOT}, 'htdocs', $c->stash('static')));
}
+sub public_file($c) {
+ $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 b435a29ac..653c5d12e 100644
--- a/lib/RenderApp/Model/Problem.pm
+++ b/lib/RenderApp/Model/Problem.pm
@@ -8,7 +8,8 @@ use Mojo::IOLoop;
use Mojo::JSON qw( encode_json );
use Mojo::Base -async_await;
use Time::HiRes qw( time );
-use RenderApp::Controller::RenderProblem;
+use MIME::Base64 qw( decode_base64 );
+use WeBWorK::RenderProblem;
##### Problem params: #####
# = random_seed (set randomization for rendering)
@@ -67,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 {
@@ -88,9 +89,12 @@ sub source {
if ( scalar(@_) == 1 ) {
my $contents = shift;
+ # recognize and decode base64 if necessary
+ $contents = Encode::decode( "UTF-8", decode_base64($contents) )
+ if ( $contents =~ m!^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$!);
+
# UNIX style line-endings are required
- $contents =~ s/\r\n/\n/g;
- $contents =~ s/\r/\n/g;
+ $contents =~ s!\r\n?!\n!g;
$self->{problem_contents} = $contents;
}
return $self->{problem_contents};
@@ -131,7 +135,8 @@ sub path {
}
$self->{_error} = "404 I cannot find a problem with that file path."
unless ( -e $read_path || $force );
- $self->{read_path} = Mojo::File->new($read_path);
+ # if we objectify an empty string, it becomes truth-y -- AVOID!
+ $self->{read_path} = Mojo::File->new($read_path) if $read_path;
}
return $self->{read_path};
}
@@ -217,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..46edddb8f
--- /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::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::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 Mojolicious::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..b6904a8fb
--- /dev/null
+++ b/lib/WeBWorK/FormatRenderedProblem.pm
@@ -0,0 +1,313 @@
+################################################################################
+# 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 = $inputs_ref->{baseURL} || $main::basehref;
+ my $FORM_ACTION_URL = $inputs_ref->{formURL} || $main::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} && !$inputs_ref->{isInstructor}
+ ? 'static' : $inputs_ref->{outputFormat};
+
+ # 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 && !$previewMode,
+ showCorrectAnswers => $showCorrectMode,
+ showMessages => $showMessages,
+ showSummary => $showSummary && !$previewMode,
+ 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->to_string 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 && !$previewMode && $problemResult,
+ answerhashXML => $answerhashXML,
+ showPreviewButton => $inputs_ref->{hidePreviewButton} ? '0' : '',
+ showCheckAnswersButton => $inputs_ref->{hideCheckAnswersButton} ? '0' : '',
+ showCorrectAnswersButton => $inputs_ref->{showCorrectAnswersButton} // $inputs_ref->{isInstructor} ? '' : '0',
+ showFooter => $inputs_ref->{showFooter} // '0',
+ 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 52%
rename from lib/RenderApp/Controller/RenderProblem.pm
rename to lib/WeBWorK/RenderProblem.pm
index 04dc4001e..a15c846f6 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;
@@ -24,10 +24,10 @@ use Proc::ProcessTable; # use for log memory use
use WeBWorK::PG;
use WeBWorK::Utils::Tags;
use WeBWorK::Localize;
-use RenderApp::Controller::FormatRenderedProblem;
+use WeBWorK::FormatRenderedProblem;
-use 5.10.0;
-$Carp::Verbose = 1;
+# use 5.10.0;
+# $Carp::Verbose = 1;
### verbose output when UNIT_TESTS_ON =1;
our $UNIT_TESTS_ON = 0;
@@ -38,7 +38,7 @@ our $UNIT_TESTS_ON = 0;
# create log files :: expendable
##################################################
-my $path_to_log_file = 'logs/standalone_results.log';
+my $path_to_log_file = "$ENV{RENDER_ROOT}/logs/standalone_results.log";
eval { # attempt to create log file
local (*FH);
@@ -76,78 +76,77 @@ sub UNIVERSAL::TO_JSON {
sub process_pg_file {
my $problem = shift;
- my $inputHash = shift;
-
- my $file_path = $problem->path;
- my $problem_seed = $problem->seed || '666';
+ my $inputs_ref = shift;
# just make sure we have the fundamentals covered...
- $inputHash->{displayMode} //= 'MathJax';
- $inputHash->{sourceFilePath} ||= $file_path;
- $inputHash->{outputFormat} ||= 'static';
- $inputHash->{language} ||= 'en';
-
+ $inputs_ref->{displayMode} ||= 'MathJax';
+ $inputs_ref->{outputFormat} ||= $inputs_ref->{outputformat} || 'default';
+ $inputs_ref->{language} ||= 'en';
+ $inputs_ref->{isInstructor} //= ($inputs_ref->{permissionLevel} // 0) >= 10;
# HACK: required for problemRandomize.pl
- $inputHash->{effectiveUser} = 'red.ted';
- $inputHash->{user} = 'red.ted';
-
- # OTHER fundamentals - urls have been handled already...
- # form_action_url => $inputHash->{form_action_url}||'http://failure.org',
- # base_url => $inputHash->{base_url}||'http://failure.org'
- # #psvn => $psvn//'23456', # DEPRECATED
- # #forcePortNumber => $credentials{forcePortNumber}//'',
+ $inputs_ref->{effectiveUser} = 'red.ted';
+ $inputs_ref->{user} = 'red.ted';
- my $pg_start =
- time; # this is Time::HiRes's time, which gives floating point values
+ my $pg_start = time;
+ my $memory_use_start = get_current_process_memory();
- my ( $error_flag, $formatter, $error_string ) =
- process_problem( $file_path, $inputHash );
+ my ( $return_object, $error_flag, $error_string ) =
+ process_problem( $problem, $inputs_ref );
my $pg_stop = time;
my $pg_duration = $pg_stop - $pg_start;
+ my $log_file_path = $problem->path() || 'source provided without path';
+ my $memory_use_end = get_current_process_memory();
+ my $memory_use = $memory_use_end - $memory_use_start;
+ writeRenderLogEntry(
+ sprintf( "(duration: %.3f sec) ", $pg_duration )
+ . sprintf( "{memory: %6d bytes} ", $memory_use )
+ . "file: $log_file_path"
+ );
# 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->{resources},
- tags => $pg_obj->{pgResources},
- js => $pg_obj->{js},
- css => $pg_obj->{css},
- },
- form_data => $inputHash,
- raw_metadata_text => $pg_obj->{raw_metadata_text},
- JWT => {
- problem => $inputHash->{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($file_path, $inputHash->{problemSource}) if ( $inputHash->{includeTags} );
+ $return_object->{tags} = WeBWorK::Utils::Tags->new($inputs_ref->{sourceFilePath}, $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;
}
@@ -156,101 +155,36 @@ sub process_pg_file {
#######################################################################
sub process_problem {
- my $file_path = shift;
+ my $problem = shift;
my $inputs_ref = shift;
- my $adj_file_path;
- my $source;
-
- # obsolete if using JSON return format
- # These can FORCE display of AnsGroup AnsHash PGInfo and ResourceInfo
- # $inputs_ref->{showAnsGroupInfo} = 1; #$print_answer_group;
- # $inputs_ref->{showAnsHashInfo} = 1; #$print_answer_hash;
- # $inputs_ref->{showPGInfo} = 1; #$print_pg_hash;
- # $inputs_ref->{showResourceInfo} = 1; #$print_resource_hash;
-
- ### stash inputs that get wiped by PG
- my $problem_seed = $inputs_ref->{problemSeed};
- die "problem seed not defined in Controller::RenderProblem::process_problem"
- unless $problem_seed;
-
- # if base64 source is provided, use that over fetching problem path
- if ( $inputs_ref->{problemSource} && $inputs_ref->{problemSource} =~ m/\S/ )
- {
- # such hackery - but Mojo::Promises are so well-built that they are invisible
- # ... until you leave the Mojo space
- $inputs_ref->{problemSource} = $inputs_ref->{problemSource}{results}[0] if $inputs_ref->{problemSource} =~ /Mojo::Promise/;
- # sanitize the base64 encoded source
- $inputs_ref->{problemSource} =~ s/\s//gm;
- # while ($source =~ /([^A-Za-z0-9+])/gm) {
- # warn "invalid character found: ".sprintf( "\\u%04x", ord($1) )."\n";
- # }
- $source = Encode::decode("UTF-8", decode_base64( $inputs_ref->{problemSource} ) );
- }
- else {
- ( $adj_file_path, $source ) = get_source($file_path);
+ my $source = $problem->{problem_contents};
+ my $file_path = $inputs_ref->{sourceFilePath};
- # WHY are there so many fields in which to stash the file path?
- #$inputs_ref->{fileName} = $adj_file_path;
- #$inputs_ref->{probFileName} = $adj_file_path;
- #$inputs_ref->{sourceFilePath} = $adj_file_path;
- #$inputs_ref->{pathToProblemFile} = $adj_file_path;
- }
- my $raw_metadata_text = $1 if ($source =~ /(.*?)DOCUMENT\(\s*\)\s*;/s);
$inputs_ref->{problemUUID} = md5_hex(Encode::encode_utf8($source));
- # TODO verify line ending are LF instead of CRLF
-
- # included (external) pg content is not recorded by PGalias
+ # external dependencies on pg content is not recorded by PGalias
# record the dependency separately -- TODO: incorporate into PG.pl or PGcore?
- my $pgResources = [];
+ my @pgResources;
while ($source =~ m/includePG(?:problem|file)\(["'](.*)["']\);/g )
{
warn "PG asset reference found: $1\n" if $UNIT_TESTS_ON;
- push @$pgResources, $1;
+ push @pgResources, $1;
}
- # # this does not capture _all_ image asset references, unfortunately...
- # # asset filenames may be stored as variables before image() is called
- # while ($source =~ m/image\(\s*("[^\$]+?"|'[^\$]+?')\s*[,\)]/g) {
- # warn "Image asset reference found!\n" . $1 . "\n" if $UNIT_TESTS_ON;
- # my $image = $1;
- # $image =~ s/['"]//g;
- # $image = dirname($file_path) . '/' . $image if ($image =~ /^[^\/]*\.(?:gif|jpg|jpeg|png)$/i);
- # warn "Recording image asset as: $image\n" if $UNIT_TESTS_ON;
- # push @$assets, $image;
- # }
-
- # $inputs_ref->{pathToProblemFile} = $adj_file_path
- # if ( defined $adj_file_path );
-
##################################################
# Process the pg file
##################################################
- ### store the time before we invoke the content generator
- my $cg_start =
- time; # this is Time::HiRes's time, which gives floating point values
-
- ############################################
- # Call server via standaloneRenderer to render problem
- ############################################
-
our ( $return_object, $error_flag, $error_string );
$error_flag = 0;
$error_string = '';
- my $memory_use_start = get_current_process_memory();
-
# can include @args as third input below
$return_object = standaloneRenderer( \$source, $inputs_ref );
# stash assets list in $return_object
- $return_object->{pgResources} = $pgResources;
-
- # stash raw metadata text in $return_object
- $return_object->{raw_metadata_text} = $raw_metadata_text;
+ $return_object->{pgResources} = \@pgResources;
# generate sessionJWT to store session data and answerJWT to update grade store
- # only occurs if problemJWT exists!
my ($sessionJWT, $answerJWT) = generateJWTs($return_object, $inputs_ref);
$return_object->{sessionJWT} = $sessionJWT // '';
$return_object->{answerJWT} = $answerJWT // '';
@@ -262,7 +196,7 @@ sub process_problem {
print "\n\n Result of renderProblem \n\n" if $UNIT_TESTS_ON;
print pretty_print_rh($return_object) if $UNIT_TESTS_ON;
if ( not defined $return_object )
- { #FIXME make sure this is the right error message if site is unavailable
+ {
$error_string = "0\t Could not process $file_path problem file \n";
}
elsif ( defined( $return_object->{flags}->{error_flag} )
@@ -275,47 +209,11 @@ sub process_problem {
}
$error_flag = 1 if $return_object->{errors};
- ##################################################
- # Create FormatRenderedProblems object
- ##################################################
-
- # my $encoded_source = encode_base64($source); # create encoding of source_file;
- my $formatter = RenderApp::Controller::FormatRenderedProblem->new(
- return_object => $return_object,
- encoded_source => '', #encode_base64($source),
- sourceFilePath => $file_path,
- url => $inputs_ref->{baseURL},
- form_action_url => $inputs_ref->{formURL},
- maketext => sub {return @_},
- courseID => 'blackbox',
- userID => 'Motoko_Kusanagi',
- course_password => 'daemon',
- inputs_ref => $inputs_ref,
- problem_seed => $problem_seed
- );
-
- ##################################################
- # log elapsed time
- ##################################################
- my $scriptName = 'standalonePGproblemRenderer';
- my $log_file_path = $file_path // 'source provided without path';
- my $cg_end = time;
- my $cg_duration = $cg_end - $cg_start;
- my $memory_use_end = get_current_process_memory();
- my $memory_use = $memory_use_end - $memory_use_start;
- writeRenderLogEntry(
- "",
- "{script:$scriptName; file:$log_file_path; "
- . sprintf( "duration: %.3f sec;", $cg_duration )
- . sprintf( " memory: %6d bytes;", $memory_use ) . "}",
- ''
- );
-
#######################################################################
# End processing of the pg file
#######################################################################
- return $error_flag, $formatter, $error_string;
+ return $return_object, $error_flag, $error_string;
}
###########################################
@@ -331,32 +229,20 @@ sub standaloneRenderer {
my $processAnswers = $inputs_ref->{processAnswers} // 1;
print "NOT PROCESSING ANSWERS" unless $processAnswers == 1;
- unless (ref $problemFile) {
- # In this case the source file name is passed
- print "standaloneProblemRenderer: setting source_file = $problemFile";
- }
-
- # Attempt to match old parameters.
- my $isInstructor = $inputs_ref->{isInstructor} // ($inputs_ref->{permissionLevel} // 0) >= 10;
-
my $pg = WeBWorK::PG->new(
- ref $problemFile
- ? (
- sourceFilePath => $inputs_ref->{sourceFilePath} // '',
- r_source => $problemFile,
- )
- : (sourceFilePath => $problemFile),
+ sourceFilePath => $inputs_ref->{sourceFilePath} // '',
+ r_source => $problemFile,
problemSeed => $inputs_ref->{problemSeed},
processAnswers => $processAnswers,
showHints => $inputs_ref->{showHints}, # default is to showHint (set in PG.pm)
- showSolutions => $inputs_ref->{showSolutions},
+ showSolutions => $inputs_ref->{showSolutions} // $inputs_ref->{isInstructor} ? 1 : 0,
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},
- isInstructor => $isInstructor,
+ isInstructor => $inputs_ref->{isInstructor},
forceScaffoldsOpen => $inputs_ref->{forceScaffoldsOpen},
psvn => $inputs_ref->{psvn},
problemUUID => $inputs_ref->{problemUUID},
@@ -364,9 +250,11 @@ sub standaloneRenderer {
language_subroutine => WeBWorK::Localize::getLoc($inputs_ref->{language} // 'en'),
inputs_ref => {%$inputs_ref}, # Copy the inputs ref so the original can be relied on after rendering.
templateDirectory => "$ENV{RENDER_ROOT}/",
+ htmlURL => 'pg_files/',
+ tempURL => 'pg_files/tmp/',
debuggingOptions => {
show_resource_info => $inputs_ref->{show_resource_info},
- view_problem_debugging_info => $inputs_ref->{view_problem_debugging_info} // $isInstructor,
+ view_problem_debugging_info => $inputs_ref->{view_problem_debugging_info} // $inputs_ref->{isInstructor},
show_pg_info => $inputs_ref->{show_pg_info},
show_answer_hash_info => $inputs_ref->{show_answer_hash_info},
show_answer_group_info => $inputs_ref->{show_answer_group_info}
@@ -377,8 +265,8 @@ sub standaloneRenderer {
my ( $internal_debug_messages, $pgwarning_messages, $pgdebug_messages );
if ( ref( $pg->{pgcore} ) ) {
$internal_debug_messages = $pg->{pgcore}->get_internal_debug_messages;
- $pgwarning_messages = $pg->{pgcore}->get_warning_messages();
- $pgdebug_messages = $pg->{pgcore}->get_debug_messages();
+ $pgwarning_messages = $pg->{pgcore}->get_warning_messages;
+ $pgdebug_messages = $pg->{pgcore}->get_debug_messages;
}
else {
$internal_debug_messages =
@@ -391,8 +279,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},
@@ -405,19 +293,12 @@ sub standaloneRenderer {
$out2;
}
-sub display_html_output { #display the problem in a browser
- my $file_path = shift;
- my $formatter = shift;
- my $output_text = $formatter->formatRenderedProblem;
- return $output_text;
-}
-
##################################################
# utilities
##################################################
sub get_current_process_memory {
- state $pt = Proc::ProcessTable->new;
+ CORE::state $pt = Proc::ProcessTable->new;
my %info = map { $_->pid => $_ } @{ $pt->table };
return $info{$$}->rss;
}
@@ -427,11 +308,20 @@ sub get_current_process_memory {
sub generateJWTs {
my $pg = shift;
my $inputs_ref = shift;
- my $sessionHash = {'answersSubmitted' => 1, 'iss' =>$ENV{SITE_HOST}};
+ my $sessionHash = {'answersSubmitted' => 1, 'iss' =>$ENV{SITE_HOST}, problemJWT => $inputs_ref->{problemJWT}};
my $scoreHash = {};
-
- # if no problemJWT exists, then why bother?
- return unless $inputs_ref->{problemJWT};
+
+ # TODO: sometimes student_ans causes JWT corruption in PHP - why?
+ # proposed restructuring of the answerJWT -- prepare with LibreTexts
+ # my %studentKeys = qw(student_value value student_formula formula student_ans answer original_student_ans original);
+ # my %previewKeys = qw(preview_text_string text preview_latex_string latex);
+ # 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})};
+
+ # once the correct answers are shown, this setting is permanent
+ $sessionHash->{showCorrectAnswers} = 1 if $inputs_ref->{showCorrectAnswers} && !$inputs_ref->{isInstructor};
# store the current answer/response state for each entry
foreach my $ans (keys %{$pg->{answers}}) {
@@ -440,19 +330,20 @@ sub generateJWTs {
$sessionHash->{ 'previous_' . $ans } = $inputs_ref->{$ans};
$sessionHash->{ 'MaThQuIlL_' . $ans } = $inputs_ref->{ 'MaThQuIlL_' . $ans } if ($inputs_ref->{ 'MaThQuIlL_' . $ans});
- # $scoreHash->{ans_id} = $ans;
- # $scoreHash->{answer} = unbless($pg->{answers}{$ans}) // {},
- # $scoreHash->{score} = $pg->{answers}{$ans}{score} // 0,
-
- # TODO see why this key is causing JWT corruption in PHP
- delete( $pg->{answers}{$ans}{student_ans});
+ # More restructuring -- confirm with LibreTexts
+ # $scoreHash->{$ans}{student} = { map {exists $answers{$ans}{$_} ? ($studentKeys{$_} => $answers{$ans}{$_}) : ()} keys %studentKeys };
+ # $scoreHash->{$ans}{preview} = { map {exists $answers{$ans}{$_} ? ($previewKeys{$_} => $answers{$ans}{$_}) : ()} keys %previewKeys };
+ # $scoreHash->{$ans}{correct} = { map {exists $answers{$ans}{$_} ? ($correctKeys{$_} => $answers{$ans}{$_}) : ()} keys %correctKeys };
+ # $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'
- $sessionHash->{numCorrect} = (defined $inputs_ref->{submitAnswers}) ?
+ # but don't update either if the problem was already correct
+ $sessionHash->{numCorrect} = (defined $inputs_ref->{submitAnswers} && $inputs_ref->{numCorrect} == 0) ?
$pg->{problem_state}{num_of_correct_ans} : ($inputs_ref->{numCorrect} // 0);
- $sessionHash->{numIncorrect} = (defined $inputs_ref->{submitAnswers}) ?
+ $sessionHash->{numIncorrect} = (defined $inputs_ref->{submitAnswers} && $inputs_ref->{numCorrect} == 0) ?
$pg->{problem_state}{num_of_incorrect_ans} : ($inputs_ref->{numIncorrect} // 0);
# include the final result of the combined scores
@@ -467,47 +358,16 @@ sub generateJWTs {
iss => $ENV{SITE_HOST},
aud => $inputs_ref->{JWTanswerURL},
score => $scoreHash,
- problemJWT => $inputs_ref->{problemJWT},
+ # problemJWT => $inputs_ref->{problemJWT},
sessionJWT => $sessionJWT,
platform => 'standaloneRenderer'
};
# 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);
-
return ($sessionJWT, $answerJWT);
}
-# Get problem template source and adjust file_path name
-sub get_source {
- my $file_path = shift;
- my $source;
- die "Unable to read file $file_path \n"
- unless $file_path eq '-' or -r $file_path;
- eval { #File::Slurp would be faster (see perl monks)
- local $/ = undef;
- if ( $file_path eq '-' ) {
- $source = ;
- } else {
- # To support proper behavior with UTF-8 files, we need to open them with "<:encoding(UTF-8)"
- # as otherwise, the first HTML file will render properly, but when "Preview" "Submit answer"
- # or "Show correct answer" is used it will make problems, as in process_problem() the
- # encodeSource() method is called on a data which is still UTF-8 encoded, and leads to double
- # encoding and gibberish.
- # NEW:
- open( FH, "<:encoding(UTF-8)", $file_path )
- or die "Couldn't open file $file_path: $!";
-
- # OLD:
- #open(FH, "<" ,$file_path) or die "Couldn't open file $file_path: $!";
- $source = ; #slurp input
- close FH;
- }
- };
- die "Something is wrong with the contents of $file_path\n" if $@;
- return $file_path, $source;
-}
-
sub pretty_print_rh {
shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ );
my $rh = shift;
@@ -557,18 +417,15 @@ sub pretty_print_rh {
return $out . " ";
}
-sub writeRenderLogEntry($$$) {
- my ( $function, $details, $beginEnd ) = @_;
- $beginEnd =
- ( $beginEnd eq "begin" ) ? ">" : ( $beginEnd eq "end" ) ? "<" : "-";
+sub writeRenderLogEntry($) {
+ my $message = shift;
local *LOG;
if ( open LOG, ">>", $path_to_log_file ) {
print LOG "[", time2str( "%a %b %d %H:%M:%S %Y", time ),
- "] $$ " . time . " $beginEnd $function [$details]\n";
+ "] $message\n";
close LOG;
- }
- else {
+ } else {
warn "failed to open $path_to_log_file for writing: $!";
}
}
diff --git a/lib/WeBWorK/Utils.pm b/lib/WeBWorK/Utils.pm
index 095e34a5e..3665bc6a2 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 Mojo::URL->new("${baseURL}$file")->query(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};
+ 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/lib/WeBWorK/Utils/Tags.pm b/lib/WeBWorK/Utils/Tags.pm
index 9b1182a60..e19deeb98 100644
--- a/lib/WeBWorK/Utils/Tags.pm
+++ b/lib/WeBWorK/Utils/Tags.pm
@@ -249,7 +249,6 @@ sub new {
if ($source) {
@lines = split "\n", $source;
- $name = "";
} else {
if ( $name !~ /pg$/ && $name !~ /\.pg\.[-a-zA-Z0-9_.@]*\.tmp$/ ) {
warn "Not a pg file"; #print caused trouble with XMLRPC
@@ -263,7 +262,7 @@ sub new {
}
my $lineno = 0;
- $self->{file} = "$name";
+ $self->{file} = $name;
# Initialize some values
for my $tagname ( BASIC ) {
@@ -526,7 +525,7 @@ sub write {
next;
}
next if istagline($line);
- print $fh $line;
+ print $fh $line unless $lineno < $self->{lasttagline};
}
$fh->close();
diff --git a/lib/WebworkClient/classic_format.pl b/lib/WebworkClient/classic_format.pl
deleted file mode 100644
index fd8376ec4..000000000
--- a/lib/WebworkClient/classic_format.pl
+++ /dev/null
@@ -1,66 +0,0 @@
-$simple_format = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_css_files
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_js_files
-
-$problemHeadText
-$problemPostHeaderText
-
-WeBWorK using host: $SITE_URL, format: simple seed: $problemSeed
-
-
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-$simple_format;
diff --git a/lib/WebworkClient/json_format.pl b/lib/WebworkClient/json_format.pl
deleted file mode 100644
index 0daa9ef9e..000000000
--- a/lib/WebworkClient/json_format.pl
+++ /dev/null
@@ -1,145 +0,0 @@
-# The json output format needs to collect the data differently than
-# the other formats. It will return an array which alternates between
-# key-names and values, and each relevant value will later undergo
-# variable interpolation.
-
-# Most parts which need variable interpolation end in "_VI".
-# Other parts which need variable interpolation are:
-# hidden_input_field_*
-# real_webwork_*
-
-@pairs_for_json = (
- "head_part001_VI", "\n" . '' . "\n"
-);
-
-$nextBlock = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-push( @pairs_for_json, "head_part010", $nextBlock );
-
-$nextBlock = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-push( @pairs_for_json, "head_part100", $nextBlock );
-
-$nextBlock = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-
-
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-push( @pairs_for_json, "head_part200", $nextBlock );
-
-push( @pairs_for_json, "head_part300_VI", '$problemHeadText' . "\n" );
-
-$nextBlock = <<'ENDPROBLEMTEMPLATE';
-WeBWorK problem
-ENDPROBLEMTEMPLATE
-
-push( @pairs_for_json, "head_part400", $nextBlock );
-
-push( @pairs_for_json, "head_part999", "\n" );
-
-push( @pairs_for_json, "body_part001", "\n" );
-
-$nextBlock = <<'ENDPROBLEMTEMPLATE';
-
-
-
-ENDPROBLEMTEMPLATE
-
-push( @pairs_for_json, "body_part100", $nextBlock );
-
-push( @pairs_for_json, "body_part300_VI", '$answerTemplate' . "\n" );
-
-$nextBlock = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-push( @pairs_for_json, "body_part999", $nextBlock );
-
-push( @pairs_for_json, "hidden_input_field_answersSubmitted", '1' );
-push( @pairs_for_json, "hidden_input_field_sourceFilePath", '$sourceFilePath' );
-push( @pairs_for_json, "hidden_input_field_problemSource", '$encoded_source' );
-push( @pairs_for_json, "hidden_input_field_problemSeed", '$problemSeed' );
-push( @pairs_for_json, "hidden_input_field_problemUUID", '$problemUUID' );
-push( @pairs_for_json, "hidden_input_field_psvn", '$psvn' );
-push( @pairs_for_json, "hidden_input_field_pathToProblemFile", '$fileName' );
-push( @pairs_for_json, "hidden_input_field_courseName", '$courseID' );
-push( @pairs_for_json, "hidden_input_field_courseID", '$courseID' );
-push( @pairs_for_json, "hidden_input_field_userID", '$userID' );
-push( @pairs_for_json, "hidden_input_field_course_password", '$course_password' );
-push( @pairs_for_json, "hidden_input_field_displayMode", '$displayMode' );
-push( @pairs_for_json, "hidden_input_field_outputFormat", 'json' );
-push( @pairs_for_json, "hidden_input_field_language", '$formLanguage' );
-push( @pairs_for_json, "hidden_input_field_showSummary", '$showSummary' );
-push( @pairs_for_json, "hidden_input_field_forcePortNumber", '$forcePortNumber' );
-
-# 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).
-
-push( @pairs_for_json, "real_webwork_SITE_URL", '$SITE_URL' );
-push( @pairs_for_json, "real_webwork_FORM_ACTION_URL", '$FORM_ACTION_URL' );
-push( @pairs_for_json, "internal_problem_lang_and_dir", '$PROBLEM_LANG_AND_DIR');
-
-# Output back to WebworkClient.pm is the reference to the array:
-\@pairs_for_json;
diff --git a/lib/WebworkClient/jwe_secure_format.pl b/lib/WebworkClient/jwe_secure_format.pl
deleted file mode 100644
index 5511cbb4b..000000000
--- a/lib/WebworkClient/jwe_secure_format.pl
+++ /dev/null
@@ -1,106 +0,0 @@
-$jwe_format = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_css_files
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_js_files
-
-$problemHeadText
-$problemPostHeaderText
-
-WeBWorK using host: $SITE_URL
-
-
-
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-$jwe_format;
diff --git a/lib/WebworkClient/nosubmit_format.pl b/lib/WebworkClient/nosubmit_format.pl
deleted file mode 100644
index 1dd80d03a..000000000
--- a/lib/WebworkClient/nosubmit_format.pl
+++ /dev/null
@@ -1,57 +0,0 @@
-$nosubmit_format = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_css_files
-
-
-
-
-
-
-
-
-
-$extra_js_files
-
-$problemHeadText
-$problemPostHeaderText
-
-WeBWorK Standalone Renderer
-
-
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-$nosubmit_format;
diff --git a/lib/WebworkClient/practice_format.pl b/lib/WebworkClient/practice_format.pl
deleted file mode 100644
index 303dc099b..000000000
--- a/lib/WebworkClient/practice_format.pl
+++ /dev/null
@@ -1,65 +0,0 @@
-$simple_format = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_css_files
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_js_files
-
-$problemHeadText
-$problemPostHeaderText
-
-WeBWorK using host: $SITE_URL, format: simple seed: $problemSeed
-
-
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-$simple_format;
diff --git a/lib/WebworkClient/simple_format.pl b/lib/WebworkClient/simple_format.pl
deleted file mode 100644
index 630490c3a..000000000
--- a/lib/WebworkClient/simple_format.pl
+++ /dev/null
@@ -1,67 +0,0 @@
-$simple_format = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_css_files
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_js_files
-
-$problemHeadText
-$problemPostHeaderText
-
-WeBWorK using host: $SITE_URL, format: simple seed: $problemSeed
-
-
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-$simple_format;
diff --git a/lib/WebworkClient/single_format.pl b/lib/WebworkClient/single_format.pl
deleted file mode 100644
index 3aac0e498..000000000
--- a/lib/WebworkClient/single_format.pl
+++ /dev/null
@@ -1,64 +0,0 @@
-$single_format = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_css_files
-
-
-
-
-
-
-
-
-
-
-
-$extra_js_files
-
-$problemHeadText
-$problemPostHeaderText
-
-WeBWorK Standalone Renderer
-
-
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-$single_format;
diff --git a/lib/WebworkClient/standard_format.pl b/lib/WebworkClient/standard_format.pl
deleted file mode 100644
index bb9f2212c..000000000
--- a/lib/WebworkClient/standard_format.pl
+++ /dev/null
@@ -1,93 +0,0 @@
-$standard_format = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_css_files
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_js_files
-
-$problemHeadText
-$problemPostHeaderText
-
-WeBWorK using host: $SITE_URL, format: standard seed: $problemSeed course: $courseID
-
-
-
- WeBWorK using host: $SITE_URL, course: $courseID format: standard
-$answerTemplate
-
-
-
- Perl warning section
-$warnings
- PG Warning section
-$PG_warning_messages;
- Debug message section
-$debug_messages
- internal errors
-$internal_debug_messages
-
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-$standard_format;
diff --git a/lib/WebworkClient/static_format.pl b/lib/WebworkClient/static_format.pl
deleted file mode 100644
index 443a05a5f..000000000
--- a/lib/WebworkClient/static_format.pl
+++ /dev/null
@@ -1,59 +0,0 @@
-$static_format = <<'ENDPROBLEMTEMPLATE';
-
-
-
-
-
-
-
-
-
-
-
-
-$extra_css_files
-
-
-
-
-
-
-
-
-
-$extra_js_files
-
-$problemHeadText
-$problemPostHeaderText
-
-WeBWorK Standalone Renderer
-
-
-
-
-
-
-ENDPROBLEMTEMPLATE
-
-$static_format;
diff --git a/lib/WebworkClient/ww3_format.pl b/lib/WebworkClient/ww3_format.pl
deleted file mode 100644
index 7801b40c2..000000000
--- a/lib/WebworkClient/ww3_format.pl
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- answerTemplate => '$answerTemplate',
- scoreSummary => '$scoreSummary',
-
- problemText => <<'ENDPROBLEMTEMPLATE'
-$problemHeadText
-
-ENDPROBLEMTEMPLATE
-};
diff --git a/package-lock.json b/package-lock.json
index 45fb088be..07ea79ea4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2,23 +2,5 @@
"name": "renderer",
"lockfileVersion": 2,
"requires": true,
- "packages": {
- "": {
- "dependencies": {
- "codemirror": "^5.65.2"
- }
- },
- "node_modules/codemirror": {
- "version": "5.65.6",
- "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.6.tgz",
- "integrity": "sha512-zNihMSMoDxK9Gqv9oEyDT8oM51rcRrQ+IEo2zyS48gJByBq5Fj8XuNEguMra+MuIOuh6lkpnLUJeL70DoTt6yw=="
- }
- },
- "dependencies": {
- "codemirror": {
- "version": "5.65.6",
- "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.6.tgz",
- "integrity": "sha512-zNihMSMoDxK9Gqv9oEyDT8oM51rcRrQ+IEo2zyS48gJByBq5Fj8XuNEguMra+MuIOuh6lkpnLUJeL70DoTt6yw=="
- }
- }
+ "packages": {}
}
diff --git a/package.json b/package.json
deleted file mode 100644
index 3a353eb43..000000000
--- a/package.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "name": "pg.javascript_package_manager",
- "description": "Third party javascript for the standalon renderer",
- "license": "GPL-2.0+",
- "repository": {
- "type": "git",
- "url": "https://github.com/openwebwork/renderer"
- },
- "dependencies": {
- "codemirror": "^5.65.2"
- }
-}
diff --git a/public/PGCodeMirror/PG.js b/public/PGCodeMirror/PG.js
deleted file mode 100644
index b90e4d04d..000000000
--- a/public/PGCodeMirror/PG.js
+++ /dev/null
@@ -1,1025 +0,0 @@
-(function(mod) {
- if (typeof exports == "object" && typeof module == "object") // CommonJS
- mod(require("../../lib/codemirror"));
- else if (typeof define == "function" && define.amd) // AMD
- define(["../../lib/codemirror"], mod);
- else // Plain browser env
- mod(CodeMirror);
-})(function(CodeMirror) {
- "use strict";
-
- CodeMirror.defineMode("PG",function(){
- // http://perldoc.perl.org
- var 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,
- '$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],
- '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 filesytem
- 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 stirng
- 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
-
- var RXstyle="string-2";
- var 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;
- 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+1]!==undefined &&
- look(stream) === chain[i+1]){
- i++;
- state.chain=chain[i];
- state.style=style;
- state.tail=tail;}
- else if(chain[i+1]!==undefined) {
- state.chain=chain;
- 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);}
-
- function tokenEV3(stream,state,string) {
- state.tokenize=function(stream,state) {
-
- var thisEV3 = function(stream,state) {
- return tokenEV3(stream,state,string);
- }
-
- if(stream.eatSpace())
- return null;
-
- if(state.chain)
- return tokenChain(stream,state,state.chain,state.style,state.tail,thisEV3,string);
-
- if(stream.string==string) {
- state.tokenize=tokenPerl;
- }
-
- if (stream.match(/^\\\(/)) {
- return tokenChain(stream,state,["\\",")"],"comment",null,thisEV3);
- }
-
- if (stream.match(/^\\\[/)) {
- return tokenChain(stream,state,["\\","]"],"comment",null,thisEV3);
- }
-
- if (stream.match(/^\\{/)) {
- return tokenChain(stream,state,["\\","}"],"variable-2",null,thisEV3);
- }
-
- if (stream.match(/^`/)) {
- return tokenChain(stream,state,["`"],"variable-3",null,thisEV3);
- }
-
- var ch = stream.next();
-
- if (ch =="$"){
- var p=stream.pos;
- if(stream.eatWhile(/\w/)||stream.eat("{")&&stream.eatWhile(/\w/)&&stream.eat("}")) {
- return "variable";
- } else {
- stream.pos=p;
- }
- }
-
- stream.eatWhile(/[^\$\\`]/);
-
- return "block-quote";};
- return state.tokenize(stream,state);
-
- }
-
- 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;
- } else {
- stream.eatWhile('*');
- stream.match(/\{.*\}/);
- 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) {
- var strValue = JSON.stringify(prevState)
- newPrevState.prevState = JSON.parse(strValue);
- } else {
- newPrevState.prevState = null;
- }
-
- newPrevState.style = style;
- newPrevState.string = string;
-
- if (stream.match(/^\[:/)) {
- style = "variable-3";
- state.tokenize = function (stream,state) {
- return tokenPGML(stream,state,":\\]",
- style,
- newPrevState);
- }
- return style;
- } else if (stream.match(/^\[`/)) {
- style = "comment";
- state.tokenize = function (stream,state) {
- return tokenPGML(stream,state,"`\\]",
- style,
- newPrevState);
- }
- return style;
- } else if (stream.match(/^\[\|/)) {
- style = "tag";
- state.tokenize = function (stream,state) {
- return tokenPGML(stream,state,"\\|\\]",
- style,
- newPrevState);
- }
- return style;
- } else if (stream.match(/^\[%/)) {
- style = "bracket";
- state.tokenize = function (stream,state) {
- return tokenPGML(stream,state,"%\\]",
- style,
- newPrevState);
- }
- return style;
- } else if (stream.match(/^\[@/)) {
- style = "variable-2";
- state.tokenize = function (stream,state) {
- return tokenPGML(stream,state,"@\\]",
- style,
- newPrevState);
- }
- return style;
- } else if (stream.match(/^\[\$/)) {
- style = "variable";
- state.tokenize = function (stream,state) {
- return tokenPGML(stream,state,"]",
- style,
- newPrevState);
- }
- return style;
- } else if (stream.match(/^\[_+\]/)) {
- stream.eatWhile('*');
- stream.match(/\{.*\}/);
- return "builtin"
- } else if (stream.match(/^ +$/)) {
- return "trailingspace";
- } else if (stream.match(/^[\[\] :\|@%`]/)) {
- return style;
- }
-
- stream.eatWhile(/[^\[\] :\|@%`]/);
-
- 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,tokenPerl);
- if(stream.match(/^\-?[\d\.]/,false))
- if(stream.match(/^(\-?(\d*\.\d+(e[+-]?\d+)?|\d+\.\d*)|0x[\da-fA-F]+|0b[01]+|\d+(e[+-]?\d+)?)/))
- return 'number';
- if(stream.match(/^<<(?=\w)/)){ // 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(/\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(/[:+\-\^*$&%@=<>!?|\/~\.]/.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(/[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()];
- if(!c)
- return "meta";
- 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()];
- if(!c)
- return "meta";
- 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/PGCodeMirror/PGaddons.js b/public/PGCodeMirror/PGaddons.js
deleted file mode 100644
index 649ed898c..000000000
--- a/public/PGCodeMirror/PGaddons.js
+++ /dev/null
@@ -1,1085 +0,0 @@
-// CodeMirror, copyright (c) by Marijn Haverbeke and others
-// Distributed under an MIT license: http://codemirror.net/LICENSE
-
-// Open simple dialogs on top of an editor. Relies on dialog.css.
-
-(function(mod) {
- if (typeof exports == "object" && typeof module == "object") // CommonJS
- mod(require("../../lib/codemirror"));
- else if (typeof define == "function" && define.amd) // AMD
- define(["../../lib/codemirror"], mod);
- else // Plain browser env
- mod(CodeMirror);
-})(function(CodeMirror) {
- function dialogDiv(cm, template, bottom) {
- var wrap = cm.getWrapperElement();
- var dialog;
- dialog = wrap.appendChild(document.createElement("div"));
- if (bottom)
- dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
- else
- dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
-
- if (typeof template == "string") {
- dialog.innerHTML = template;
- } else { // Assuming it's a detached DOM element.
- dialog.appendChild(template);
- }
- return dialog;
- }
-
- function closeNotification(cm, newVal) {
- if (cm.state.currentNotificationClose)
- cm.state.currentNotificationClose();
- cm.state.currentNotificationClose = newVal;
- }
-
- CodeMirror.defineExtension("openDialog", function(template, callback, options) {
- if (!options) options = {};
-
- closeNotification(this, null);
-
- var dialog = dialogDiv(this, template, options.bottom);
- var closed = false, me = this;
- function close(newVal) {
- if (typeof newVal == 'string') {
- inp.value = newVal;
- } else {
- if (closed) return;
- closed = true;
- dialog.parentNode.removeChild(dialog);
- me.focus();
-
- if (options.onClose) options.onClose(dialog);
- }
- }
-
- var inp = dialog.getElementsByTagName("input")[0], button;
- if (inp) {
- inp.focus();
-
- if (options.value) {
- inp.value = options.value;
- if (options.selectValueOnOpen !== false) {
- inp.select();
- }
- }
-
- if (options.onInput)
- CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
- if (options.onKeyUp)
- CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
-
- CodeMirror.on(inp, "keydown", function(e) {
- if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
- if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
- inp.blur();
- CodeMirror.e_stop(e);
- close();
- }
- if (e.keyCode == 13) callback(inp.value, e);
- });
-
- if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
- } else if (button = dialog.getElementsByTagName("button")[0]) {
- CodeMirror.on(button, "click", function() {
- close();
- me.focus();
- });
-
- if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
-
- button.focus();
- }
- return close;
- });
-
- CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
- closeNotification(this, null);
- var dialog = dialogDiv(this, template, options && options.bottom);
- var buttons = dialog.getElementsByTagName("button");
- var closed = false, me = this, blurring = 1;
- function close() {
- if (closed) return;
- closed = true;
- dialog.parentNode.removeChild(dialog);
- me.focus();
- }
- buttons[0].focus();
- for (var i = 0; i < buttons.length; ++i) {
- var b = buttons[i];
- (function(callback) {
- CodeMirror.on(b, "click", function(e) {
- CodeMirror.e_preventDefault(e);
- close();
- if (callback) callback(me);
- });
- })(callbacks[i]);
- CodeMirror.on(b, "blur", function() {
- --blurring;
- setTimeout(function() { if (blurring <= 0) close(); }, 200);
- });
- CodeMirror.on(b, "focus", function() { ++blurring; });
- }
- });
-
- /*
- * openNotification
- * Opens a notification, that can be closed with an optional timer
- * (default 5000ms timer) and always closes on click.
- *
- * If a notification is opened while another is opened, it will close the
- * currently opened one and open the new one immediately.
- */
- CodeMirror.defineExtension("openNotification", function(template, options) {
- closeNotification(this, close);
- var dialog = dialogDiv(this, template, options && options.bottom);
- var closed = false, doneTimer;
- var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
-
- function close() {
- if (closed) return;
- closed = true;
- clearTimeout(doneTimer);
- dialog.parentNode.removeChild(dialog);
- }
-
- CodeMirror.on(dialog, 'click', function(e) {
- CodeMirror.e_preventDefault(e);
- close();
- });
-
- if (duration)
- doneTimer = setTimeout(close, duration);
-
- return close;
- });
-});
-
-// CodeMirror, copyright (c) by Marijn Haverbeke and others
-// Distributed under an MIT license: http://codemirror.net/LICENSE
-
-(function(mod) {
- if (typeof exports == "object" && typeof module == "object") // CommonJS
- mod(require("../../lib/codemirror"));
- else if (typeof define == "function" && define.amd) // AMD
- define(["../../lib/codemirror"], mod);
- else // Plain browser env
- mod(CodeMirror);
-})(function(CodeMirror) {
- "use strict";
- var Pos = CodeMirror.Pos;
-
- function SearchCursor(doc, query, pos, caseFold) {
- this.atOccurrence = false; this.doc = doc;
- if (caseFold == null && typeof query == "string") caseFold = false;
-
- pos = pos ? doc.clipPos(pos) : Pos(0, 0);
- this.pos = {from: pos, to: pos};
-
- // The matches method is filled in based on the type of query.
- // It takes a position and a direction, and returns an object
- // describing the next occurrence of the query, or null if no
- // more matches were found.
- if (typeof query != "string") { // Regexp match
- if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
- this.matches = function(reverse, pos) {
- if (reverse) {
- query.lastIndex = 0;
- var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
- for (;;) {
- query.lastIndex = cutOff;
- var newMatch = query.exec(line);
- if (!newMatch) break;
- match = newMatch;
- start = match.index;
- cutOff = match.index + (match[0].length || 1);
- if (cutOff == line.length) break;
- }
- var matchLen = (match && match[0].length) || 0;
- if (!matchLen) {
- if (start == 0 && line.length == 0) {match = undefined;}
- else if (start != doc.getLine(pos.line).length) {
- matchLen++;
- }
- }
- } else {
- query.lastIndex = pos.ch;
- var line = doc.getLine(pos.line), match = query.exec(line);
- var matchLen = (match && match[0].length) || 0;
- var start = match && match.index;
- if (start + matchLen != line.length && !matchLen) matchLen = 1;
- }
- if (match && matchLen)
- return {from: Pos(pos.line, start),
- to: Pos(pos.line, start + matchLen),
- match: match};
- };
- } else { // String query
- var origQuery = query;
- if (caseFold) query = query.toLowerCase();
- var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
- var target = query.split("\n");
- // Different methods for single-line and multi-line queries
- if (target.length == 1) {
- if (!query.length) {
- // Empty string would match anything and never progress, so
- // we define it to match nothing instead.
- this.matches = function() {};
- } else {
- this.matches = function(reverse, pos) {
- if (reverse) {
- var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
- var match = line.lastIndexOf(query);
- if (match > -1) {
- match = adjustPos(orig, line, match);
- return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
- }
- } else {
- var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
- var match = line.indexOf(query);
- if (match > -1) {
- match = adjustPos(orig, line, match) + pos.ch;
- return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
- }
- }
- };
- }
- } else {
- var origTarget = origQuery.split("\n");
- this.matches = function(reverse, pos) {
- var last = target.length - 1;
- if (reverse) {
- if (pos.line - (target.length - 1) < doc.firstLine()) return;
- if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
- var to = Pos(pos.line, origTarget[last].length);
- for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
- if (target[i] != fold(doc.getLine(ln))) return;
- var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
- if (fold(line.slice(cut)) != target[0]) return;
- return {from: Pos(ln, cut), to: to};
- } else {
- if (pos.line + (target.length - 1) > doc.lastLine()) return;
- var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
- if (fold(line.slice(cut)) != target[0]) return;
- var from = Pos(pos.line, cut);
- for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
- if (target[i] != fold(doc.getLine(ln))) return;
- if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return;
- return {from: from, to: Pos(ln, origTarget[last].length)};
- }
- };
- }
- }
- }
-
- SearchCursor.prototype = {
- findNext: function() {return this.find(false);},
- findPrevious: function() {return this.find(true);},
-
- find: function(reverse) {
- var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
- function savePosAndFail(line) {
- var pos = Pos(line, 0);
- self.pos = {from: pos, to: pos};
- self.atOccurrence = false;
- return false;
- }
-
- for (;;) {
- if (this.pos = this.matches(reverse, pos)) {
- this.atOccurrence = true;
- return this.pos.match || true;
- }
- if (reverse) {
- if (!pos.line) return savePosAndFail(0);
- pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
- }
- else {
- var maxLine = this.doc.lineCount();
- if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
- pos = Pos(pos.line + 1, 0);
- }
- }
- },
-
- from: function() {if (this.atOccurrence) return this.pos.from;},
- to: function() {if (this.atOccurrence) return this.pos.to;},
-
- replace: function(newText, origin) {
- if (!this.atOccurrence) return;
- var lines = CodeMirror.splitLines(newText);
- this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin);
- this.pos.to = Pos(this.pos.from.line + lines.length - 1,
- lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
- }
- };
-
- // Maps a position in a case-folded line back to a position in the original line
- // (compensating for codepoints increasing in number during folding)
- function adjustPos(orig, folded, pos) {
- if (orig.length == folded.length) return pos;
- for (var pos1 = Math.min(pos, orig.length);;) {
- var len1 = orig.slice(0, pos1).toLowerCase().length;
- if (len1 < pos) ++pos1;
- else if (len1 > pos) --pos1;
- else return pos1;
- }
- }
-
- CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
- return new SearchCursor(this.doc, query, pos, caseFold);
- });
- CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
- return new SearchCursor(this, query, pos, caseFold);
- });
-
- CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
- var ranges = [];
- var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold);
- while (cur.findNext()) {
- if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break;
- ranges.push({anchor: cur.from(), head: cur.to()});
- }
- if (ranges.length)
- this.setSelections(ranges, 0);
- });
-});
-
-// CodeMirror, copyright (c) by Marijn Haverbeke and others
-// Distributed under an MIT license: http://codemirror.net/LICENSE
-
-// Define search commands. Depends on dialog.js or another
-// implementation of the openDialog method.
-
-// Replace works a little oddly -- it will do the replace on the next
-// Ctrl-G (or whatever is bound to findNext) press. You prevent a
-// replace by making sure the match is no longer selected when hitting
-// Ctrl-G.
-
-(function(mod) {
- if (typeof exports == "object" && typeof module == "object") // CommonJS
- mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog"));
- else if (typeof define == "function" && define.amd) // AMD
- define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod);
- else // Plain browser env
- mod(CodeMirror);
-})(function(CodeMirror) {
- "use strict";
-
- function searchOverlay(query, caseInsensitive) {
- if (typeof query == "string")
- query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
- else if (!query.global)
- query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
-
- return {token: function(stream) {
- query.lastIndex = stream.pos;
- var match = query.exec(stream.string);
- if (match && match.index == stream.pos) {
- stream.pos += match[0].length || 1;
- return "searching";
- } else if (match) {
- stream.pos = match.index;
- } else {
- stream.skipToEnd();
- }
- }};
- }
-
- function SearchState() {
- this.posFrom = this.posTo = this.lastQuery = this.query = null;
- this.overlay = null;
- }
-
- function getSearchState(cm) {
- return cm.state.search || (cm.state.search = new SearchState());
- }
-
- function queryCaseInsensitive(query) {
- return typeof query == "string" && query == query.toLowerCase();
- }
-
- function getSearchCursor(cm, query, pos) {
- // Heuristic: if the query string is all lowercase, do a case insensitive search.
- return cm.getSearchCursor(query, pos, queryCaseInsensitive(query));
- }
-
- function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
- cm.openDialog(text, onEnter, {
- value: deflt,
- selectValueOnOpen: true,
- closeOnEnter: false,
- onClose: function() { clearSearch(cm); },
- onKeyDown: onKeyDown
- });
- }
-
- function dialog(cm, text, shortText, deflt, f) {
- if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
- else f(prompt(shortText, deflt));
- }
-
- function confirmDialog(cm, text, shortText, fs) {
- if (cm.openConfirm) cm.openConfirm(text, fs);
- else if (confirm(shortText)) fs[0]();
- }
-
- function parseString(string) {
- return string.replace(/\\(.)/g, function(_, ch) {
- if (ch == "n") return "\n"
- if (ch == "r") return "\r"
- return ch
- })
- }
-
- function parseQuery(query) {
- var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
- if (isRE) {
- try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); }
- catch(e) {} // Not a regular expression after all, do a string search
- } else {
- query = parseString(query)
- }
- if (typeof query == "string" ? query == "" : query.test(""))
- query = /x^/;
- return query;
- }
-
- var queryDialog =
- 'Search: (Use Ctl-Shift-F to Search/Replace) ';
-
- function startSearch(cm, state, query) {
- state.queryText = query;
- state.query = parseQuery(query);
- cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
- state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
- cm.addOverlay(state.overlay);
- if (cm.showMatchesOnScrollbar) {
- if (state.annotate) { state.annotate.clear(); state.annotate = null; }
- state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
- }
- }
-
- function doSearch(cm, rev, persistent, immediate) {
- var state = getSearchState(cm);
- if (state.query) return findNext(cm, rev);
- var q = cm.getSelection() || state.lastQuery;
- if (persistent && cm.openDialog) {
- var hiding = null
- var searchNext = function(query, event) {
- CodeMirror.e_stop(event);
- if (!query) return;
- if (query != state.queryText) {
- startSearch(cm, state, query);
- state.posFrom = state.posTo = cm.getCursor();
- }
- if (hiding) hiding.style.opacity = 1
- findNext(cm, event.shiftKey, function(_, to) {
- var dialog
- if (to.line < 3 && document.querySelector &&
- (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
- dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
- (hiding = dialog).style.opacity = .4
- })
- };
- persistentDialog(cm, queryDialog, q, searchNext, function(event, query) {
- var cmd = CodeMirror.keyMap[cm.getOption("keyMap")][CodeMirror.keyName(event)];
- if (cmd == "findNext" || cmd == "findPrev") {
- CodeMirror.e_stop(event);
- startSearch(cm, getSearchState(cm), query);
- cm.execCommand(cmd);
- } else if (cmd == "find" || cmd == "findPersistent") {
- CodeMirror.e_stop(event);
- searchNext(query, event);
- }
- });
- if (immediate) {
- startSearch(cm, state, q);
- findNext(cm, rev);
- }
- } else {
- dialog(cm, queryDialog, "Search for:", q, function(query) {
- if (query && !state.query) cm.operation(function() {
- startSearch(cm, state, query);
- state.posFrom = state.posTo = cm.getCursor();
- findNext(cm, rev);
- });
- });
- }
- }
-
- function findNext(cm, rev, callback) {cm.operation(function() {
- var state = getSearchState(cm);
- var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
- if (!cursor.find(rev)) {
- cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
- if (!cursor.find(rev)) return;
- }
- cm.setSelection(cursor.from(), cursor.to());
- cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
- state.posFrom = cursor.from(); state.posTo = cursor.to();
- if (callback) callback(cursor.from(), cursor.to())
- });}
-
- function clearSearch(cm) {cm.operation(function() {
- var state = getSearchState(cm);
- state.lastQuery = state.query;
- if (!state.query) return;
- state.query = state.queryText = null;
- cm.removeOverlay(state.overlay);
- if (state.annotate) { state.annotate.clear(); state.annotate = null; }
- });}
-
- var replaceQueryDialog =
- ' (Use /re/ syntax for regexp search/replace) ';
- var replacementQueryDialog = 'With: ';
- var doReplaceConfirm = "Replace? Yes No All Stop ";
-
- function replaceAll(cm, query, text) {
- cm.operation(function() {
- for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
- if (typeof query != "string") {
- var match = cm.getRange(cursor.from(), cursor.to()).match(query);
- cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
- } else cursor.replace(text);
- }
- });
- }
-
- function replace(cm, all) {
- if (cm.getOption("readOnly")) return;
- var query = cm.getSelection() || getSearchState(cm).lastQuery;
- var dialogText = all ? "Replace all:" : "Replace:"
- dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
- if (!query) return;
- query = parseQuery(query);
- dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
- text = parseString(text)
- if (all) {
- replaceAll(cm, query, text)
- } else {
- clearSearch(cm);
- var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
- var advance = function() {
- var start = cursor.from(), match;
- if (!(match = cursor.findNext())) {
- cursor = getSearchCursor(cm, query);
- if (!(match = cursor.findNext()) ||
- (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
- }
- cm.setSelection(cursor.from(), cursor.to());
- cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
- confirmDialog(cm, doReplaceConfirm, "Replace?",
- [function() {doReplace(match);}, advance,
- function() {replaceAll(cm, query, text)}]);
- };
- var doReplace = function(match) {
- cursor.replace(typeof query == "string" ? text :
- text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
- advance();
- };
- advance();
- }
- });
- });
- }
-
- CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
- CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
- CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);};
- CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);};
- CodeMirror.commands.findNext = doSearch;
- CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
- CodeMirror.commands.clearSearch = clearSearch;
- CodeMirror.commands.replace = replace;
- CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
-});
-
-// CodeMirror, copyright (c) by Marijn Haverbeke and others
-// Distributed under an MIT license: http://codemirror.net/LICENSE
-
-(function(mod) {
- if (typeof exports == "object" && typeof module == "object") // CommonJS
- mod(require("../../lib/codemirror"), require("./searchcursor"), require("../scroll/annotatescrollbar"));
- else if (typeof define == "function" && define.amd) // AMD
- define(["../../lib/codemirror", "./searchcursor", "../scroll/annotatescrollbar"], mod);
- else // Plain browser env
- mod(CodeMirror);
-})(function(CodeMirror) {
- "use strict";
-
- CodeMirror.defineExtension("showMatchesOnScrollbar", function(query, caseFold, options) {
- if (typeof options == "string") options = {className: options};
- if (!options) options = {};
- return new SearchAnnotation(this, query, caseFold, options);
- });
-
- function SearchAnnotation(cm, query, caseFold, options) {
- this.cm = cm;
- this.options = options;
- var annotateOptions = {listenForChanges: false};
- for (var prop in options) annotateOptions[prop] = options[prop];
- if (!annotateOptions.className) annotateOptions.className = "CodeMirror-search-match";
- this.annotation = cm.annotateScrollbar(annotateOptions);
- this.query = query;
- this.caseFold = caseFold;
- this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1};
- this.matches = [];
- this.update = null;
-
- this.findMatches();
- this.annotation.update(this.matches);
-
- var self = this;
- cm.on("change", this.changeHandler = function(_cm, change) { self.onChange(change); });
- }
-
- var MAX_MATCHES = 1000;
-
- SearchAnnotation.prototype.findMatches = function() {
- if (!this.gap) return;
- for (var i = 0; i < this.matches.length; i++) {
- var match = this.matches[i];
- if (match.from.line >= this.gap.to) break;
- if (match.to.line >= this.gap.from) this.matches.splice(i--, 1);
- }
- var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), this.caseFold);
- var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES;
- while (cursor.findNext()) {
- var match = {from: cursor.from(), to: cursor.to()};
- if (match.from.line >= this.gap.to) break;
- this.matches.splice(i++, 0, match);
- if (this.matches.length > maxMatches) break;
- }
- this.gap = null;
- };
-
- function offsetLine(line, changeStart, sizeChange) {
- if (line <= changeStart) return line;
- return Math.max(changeStart, line + sizeChange);
- }
-
- SearchAnnotation.prototype.onChange = function(change) {
- var startLine = change.from.line;
- var endLine = CodeMirror.changeEnd(change).line;
- var sizeChange = endLine - change.to.line;
- if (this.gap) {
- this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line);
- this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line);
- } else {
- this.gap = {from: change.from.line, to: endLine + 1};
- }
-
- if (sizeChange) for (var i = 0; i < this.matches.length; i++) {
- var match = this.matches[i];
- var newFrom = offsetLine(match.from.line, startLine, sizeChange);
- if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch);
- var newTo = offsetLine(match.to.line, startLine, sizeChange);
- if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch);
- }
- clearTimeout(this.update);
- var self = this;
- this.update = setTimeout(function() { self.updateAfterChange(); }, 250);
- };
-
- SearchAnnotation.prototype.updateAfterChange = function() {
- this.findMatches();
- this.annotation.update(this.matches);
- };
-
- SearchAnnotation.prototype.clear = function() {
- this.cm.off("change", this.changeHandler);
- this.annotation.clear();
- };
-});
-// CodeMirror, copyright (c) by Marijn Haverbeke and others
-// Distributed under an MIT license: http://codemirror.net/LICENSE
-
-(function(mod) {
- if (typeof exports == "object" && typeof module == "object") // CommonJS
- mod(require("../../lib/codemirror"));
- else if (typeof define == "function" && define.amd) // AMD
- define(["../../lib/codemirror"], mod);
- else // Plain browser env
- mod(CodeMirror);
-})(function(CodeMirror) {
- var ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
- (document.documentMode == null || document.documentMode < 8);
-
- var Pos = CodeMirror.Pos;
-
- var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
-
- function findMatchingBracket(cm, where, strict, config) {
- var line = cm.getLineHandle(where.line), pos = where.ch - 1;
- var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)];
- if (!match) return null;
- var dir = match.charAt(1) == ">" ? 1 : -1;
- if (strict && (dir > 0) != (pos == where.ch)) return null;
- var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
-
- var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
- if (found == null) return null;
- return {from: Pos(where.line, pos), to: found && found.pos,
- match: found && found.ch == match.charAt(0), forward: dir > 0};
- }
-
- // bracketRegex is used to specify which type of bracket to scan
- // should be a regexp, e.g. /[[\]]/
- //
- // Note: If "where" is on an open bracket, then this bracket is ignored.
- //
- // Returns false when no bracket was found, null when it reached
- // maxScanLines and gave up
- function scanForBracket(cm, where, dir, style, config) {
- var maxScanLen = (config && config.maxScanLineLength) || 10000;
- var maxScanLines = (config && config.maxScanLines) || 1000;
-
- var stack = [];
- var re = config && config.bracketRegex ? config.bracketRegex : /[(){}[\]]/;
- var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
- : Math.max(cm.firstLine() - 1, where.line - maxScanLines);
- for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
- var line = cm.getLine(lineNo);
- if (!line) continue;
- var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
- if (line.length > maxScanLen) continue;
- if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
- for (; pos != end; pos += dir) {
- var ch = line.charAt(pos);
- if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
- var match = matching[ch];
- if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
- else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
- else stack.pop();
- }
- }
- }
- return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
- }
-
- function matchBrackets(cm, autoclear, config) {
- // Disable brace matching in long lines, since it'll cause hugely slow updates
- var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
- var marks = [], ranges = cm.listSelections();
- for (var i = 0; i < ranges.length; i++) {
- var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, false, config);
- if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
- var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
- marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
- if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
- marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
- }
- }
-
- if (marks.length) {
- // Kludge to work around the IE bug from issue #1193, where text
- // input stops going to the textare whever this fires.
- if (ie_lt8 && cm.state.focused) cm.focus();
-
- var clear = function() {
- cm.operation(function() {
- for (var i = 0; i < marks.length; i++) marks[i].clear();
- });
- };
- if (autoclear) setTimeout(clear, 800);
- else return clear;
- }
- }
-
- var currentlyHighlighted = null;
- function doMatchBrackets(cm) {
- cm.operation(function() {
- if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
- currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
- });
- }
-
- CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
- if (old && old != CodeMirror.Init) {
- cm.off("cursorActivity", doMatchBrackets);
- if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
- }
- if (val) {
- cm.state.matchBrackets = typeof val == "object" ? val : {};
- cm.on("cursorActivity", doMatchBrackets);
- }
- });
-
- CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
- CodeMirror.defineExtension("findMatchingBracket", function(pos, strict, config){
- return findMatchingBracket(this, pos, strict, config);
- });
- CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
- return scanForBracket(this, pos, dir, style, config);
- });
-});
-
-
-// CodeMirror, copyright (c) by Marijn Haverbeke and others
-// Distributed under an MIT license: http://codemirror.net/LICENSE
-
-(function(mod) {
- if (typeof exports == "object" && typeof module == "object") // CommonJS
- mod(require("../../lib/codemirror"));
- else if (typeof define == "function" && define.amd) // AMD
- define(["../../lib/codemirror"], mod);
- else // Plain browser env
- mod(CodeMirror);
-})(function(CodeMirror) {
- "use strict";
-
- CodeMirror.defineExtension("annotateScrollbar", function(options) {
- if (typeof options == "string") options = {className: options};
- return new Annotation(this, options);
- });
-
- CodeMirror.defineOption("scrollButtonHeight", 0);
-
- function Annotation(cm, options) {
- this.cm = cm;
- this.options = options;
- this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight");
- this.annotations = [];
- this.doRedraw = this.doUpdate = null;
- this.div = cm.getWrapperElement().appendChild(document.createElement("div"));
- this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none";
- this.computeScale();
-
- function scheduleRedraw(delay) {
- clearTimeout(self.doRedraw);
- self.doRedraw = setTimeout(function() { self.redraw(); }, delay);
- }
-
- var self = this;
- cm.on("refresh", this.resizeHandler = function() {
- clearTimeout(self.doUpdate);
- self.doUpdate = setTimeout(function() {
- if (self.computeScale()) scheduleRedraw(20);
- }, 100);
- });
- cm.on("markerAdded", this.resizeHandler);
- cm.on("markerCleared", this.resizeHandler);
- if (options.listenForChanges !== false)
- cm.on("change", this.changeHandler = function() {
- scheduleRedraw(250);
- });
- }
-
- Annotation.prototype.computeScale = function() {
- var cm = this.cm;
- var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) /
- cm.getScrollerElement().scrollHeight
- if (hScale != this.hScale) {
- this.hScale = hScale;
- return true;
- }
- };
-
- Annotation.prototype.update = function(annotations) {
- this.annotations = annotations;
- this.redraw();
- };
-
- Annotation.prototype.redraw = function(compute) {
- if (compute !== false) this.computeScale();
- var cm = this.cm, hScale = this.hScale;
-
- var frag = document.createDocumentFragment(), anns = this.annotations;
-
- var wrapping = cm.getOption("lineWrapping");
- var singleLineH = wrapping && cm.defaultTextHeight() * 1.5;
- var curLine = null, curLineObj = null;
- function getY(pos, top) {
- if (curLine != pos.line) {
- curLine = pos.line;
- curLineObj = cm.getLineHandle(curLine);
- }
- if (wrapping && curLineObj.height > singleLineH)
- return cm.charCoords(pos, "local")[top ? "top" : "bottom"];
- var topY = cm.heightAtLine(curLineObj, "local");
- return topY + (top ? 0 : curLineObj.height);
- }
-
- if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) {
- var ann = anns[i];
- var top = nextTop || getY(ann.from, true) * hScale;
- var bottom = getY(ann.to, false) * hScale;
- while (i < anns.length - 1) {
- nextTop = getY(anns[i + 1].from, true) * hScale;
- if (nextTop > bottom + .9) break;
- ann = anns[++i];
- bottom = getY(ann.to, false) * hScale;
- }
- if (bottom == top) continue;
- var height = Math.max(bottom - top, 3);
-
- var elt = frag.appendChild(document.createElement("div"));
- elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: "
- + (top + this.buttonHeight) + "px; height: " + height + "px";
- elt.className = this.options.className;
- if (ann.id) {
- elt.setAttribute("annotation-id", ann.id);
- }
- }
- this.div.textContent = "";
- this.div.appendChild(frag);
- };
-
- Annotation.prototype.clear = function() {
- this.cm.off("refresh", this.resizeHandler);
- this.cm.off("markerAdded", this.resizeHandler);
- this.cm.off("markerCleared", this.resizeHandler);
- if (this.changeHandler) this.cm.off("change", this.changeHandler);
- this.div.parentNode.removeChild(this.div);
- };
-});
-
-// CodeMirror, copyright (c) by Marijn Haverbeke and others
-// Distributed under an MIT license: http://codemirror.net/LICENSE
-
-// Highlighting text that matches the selection
-//
-// Defines an option highlightSelectionMatches, which, when enabled,
-// will style strings that match the selection throughout the
-// document.
-//
-// The option can be set to true to simply enable it, or to a
-// {minChars, style, wordsOnly, showToken, delay} object to explicitly
-// configure it. minChars is the minimum amount of characters that should be
-// selected for the behavior to occur, and style is the token style to
-// apply to the matches. This will be prefixed by "cm-" to create an
-// actual CSS class name. If wordsOnly is enabled, the matches will be
-// highlighted only if the selected text is a word. showToken, when enabled,
-// will cause the current token to be highlighted when nothing is selected.
-// delay is used to specify how much time to wait, in milliseconds, before
-// highlighting the matches. If annotateScrollbar is enabled, the occurences
-// will be highlighted on the scrollbar via the matchesonscrollbar addon.
-
-(function(mod) {
- if (typeof exports == "object" && typeof module == "object") // CommonJS
- mod(require("../../lib/codemirror"), require("./matchesonscrollbar"));
- else if (typeof define == "function" && define.amd) // AMD
- define(["../../lib/codemirror", "./matchesonscrollbar"], mod);
- else // Plain browser env
- mod(CodeMirror);
-})(function(CodeMirror) {
- "use strict";
-
- var defaults = {
- style: "matchhighlight",
- minChars: 2,
- delay: 100,
- wordsOnly: false,
- annotateScrollbar: false,
- showToken: false,
- trim: true
- }
-
- function State(options) {
- this.options = {}
- for (var name in defaults)
- this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name]
- this.overlay = this.timeout = null;
- this.matchesonscroll = null;
- }
-
- CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
- if (old && old != CodeMirror.Init) {
- removeOverlay(cm);
- clearTimeout(cm.state.matchHighlighter.timeout);
- cm.state.matchHighlighter = null;
- cm.off("cursorActivity", cursorActivity);
- }
- if (val) {
- cm.state.matchHighlighter = new State(val);
- highlightMatches(cm);
- cm.on("cursorActivity", cursorActivity);
- }
- });
-
- function cursorActivity(cm) {
- var state = cm.state.matchHighlighter;
- clearTimeout(state.timeout);
- state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay);
- }
-
- function addOverlay(cm, query, hasBoundary, style) {
- var state = cm.state.matchHighlighter;
- cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
- if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
- var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query;
- state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
- {className: "CodeMirror-selection-highlight-scrollbar"});
- }
- }
-
- function removeOverlay(cm) {
- var state = cm.state.matchHighlighter;
- if (state.overlay) {
- cm.removeOverlay(state.overlay);
- state.overlay = null;
- if (state.matchesonscroll) {
- state.matchesonscroll.clear();
- state.matchesonscroll = null;
- }
- }
- }
-
- function highlightMatches(cm) {
- cm.operation(function() {
- var state = cm.state.matchHighlighter;
- removeOverlay(cm);
- if (!cm.somethingSelected() && state.options.showToken) {
- var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken;
- var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
- while (start && re.test(line.charAt(start - 1))) --start;
- while (end < line.length && re.test(line.charAt(end))) ++end;
- if (start < end)
- addOverlay(cm, line.slice(start, end), re, state.options.style);
- return;
- }
- var from = cm.getCursor("from"), to = cm.getCursor("to");
- if (from.line != to.line) return;
- if (state.options.wordsOnly && !isWord(cm, from, to)) return;
- var selection = cm.getRange(from, to)
- if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "")
- if (selection.length >= state.options.minChars)
- addOverlay(cm, selection, false, state.options.style);
- });
- }
-
- function isWord(cm, from, to) {
- var str = cm.getRange(from, to);
- if (str.match(/^\w+$/) !== null) {
- if (from.ch > 0) {
- var pos = {line: from.line, ch: from.ch - 1};
- var chr = cm.getRange(pos, from);
- if (chr.match(/\W/) === null) return false;
- }
- if (to.ch < cm.getLine(from.line).length) {
- var pos = {line: to.line, ch: to.ch + 1};
- var chr = cm.getRange(to, pos);
- if (chr.match(/\W/) === null) return false;
- }
- return true;
- } else return false;
- }
-
- function boundariesAround(stream, re) {
- return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) &&
- (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
- }
-
- function makeOverlay(query, hasBoundary, style) {
- return {token: function(stream) {
- if (stream.match(query) &&
- (!hasBoundary || boundariesAround(stream, hasBoundary)))
- return style;
- stream.next();
- stream.skipTo(query.charAt(0)) || stream.skipToEnd();
- }};
- }
-});
-
diff --git a/public/Problem/problem.js b/public/Problem/problem.js
deleted file mode 100644
index 02c52f936..000000000
--- a/public/Problem/problem.js
+++ /dev/null
@@ -1,8 +0,0 @@
-(() => {
- // Activate the popovers in the results table.
- document.querySelectorAll('.attemptResults .answer-preview[data-bs-toggle="popover"]')
- .forEach((preview) => {
- if (preview.dataset.bsContent)
- new bootstrap.Popover(preview);
- });
-})();
diff --git a/public/css/bootstrap.scss b/public/css/bootstrap.scss
new file mode 100644
index 000000000..5beb1d886
--- /dev/null
+++ b/public/css/bootstrap.scss
@@ -0,0 +1,89 @@
+/* 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;
+
+// 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";
+
+// Overrides
+a:not(.btn):focus {
+ color: $link-hover-color;
+ outline-style: solid;
+ outline-color: $link-hover-color;
+ outline-width: 1px;
+}
diff --git a/public/crt-display.css b/public/css/crt-display.css
similarity index 100%
rename from public/crt-display.css
rename to public/css/crt-display.css
diff --git a/public/filebrowser.css b/public/css/filebrowser.css
similarity index 100%
rename from public/filebrowser.css
rename to public/css/filebrowser.css
diff --git a/public/navbar.css b/public/css/navbar.css
similarity index 100%
rename from public/navbar.css
rename to public/css/navbar.css
diff --git a/public/opl-flex.css b/public/css/opl-flex.css
similarity index 100%
rename from public/opl-flex.css
rename to public/css/opl-flex.css
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/tags.css b/public/css/tags.css
similarity index 100%
rename from public/tags.css
rename to public/css/tags.css
diff --git a/public/twocolumn.css b/public/css/twocolumn.css
similarity index 100%
rename from public/twocolumn.css
rename to public/css/twocolumn.css
diff --git a/public/typing-sim.css b/public/css/typing-sim.css
similarity index 100%
rename from public/typing-sim.css
rename to public/css/typing-sim.css
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index 10893e0d6..000000000
Binary files a/public/favicon.ico and /dev/null differ
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/iframeResizer.contentWindow.map b/public/iframeResizer.contentWindow.map
deleted file mode 100644
index 6f732004f..000000000
--- a/public/iframeResizer.contentWindow.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["iframeResizer.contentWindow.js"],"names":["undefined","window","autoResize","base","bodyBackground","bodyMargin","bodyMarginStr","bodyObserver","bodyPadding","calculateWidth","doubleEventList","resize","click","eventCancelTimer","firstRun","height","heightCalcModeDefault","heightCalcMode","initLock","initMsg","inPageLinks","interval","intervalTimer","logging","msgID","msgIdLen","length","myID","resetRequiredMethods","max","min","bodyScroll","documentElementScroll","resizeFrom","sendPermit","target","parent","targetOriginDefault","tolerance","triggerLocked","triggerLockedTimer","throttledTimer","width","widthCalcModeDefault","widthCalcMode","win","onMessage","warn","onReady","onPageInfo","customCalcMethods","document","documentElement","offsetHeight","body","scrollWidth","eventHandlersByName","passiveSupported","options","Object","create","passive","get","addEventListener","noop","removeEventListener","error","func","context","args","result","timeout","previous","getNow","Date","now","getTime","getHeight","bodyOffset","getComputedStyle","offset","scrollHeight","custom","documentElementOffset","Math","apply","getAllMeasurements","grow","lowestElement","getMaxElement","getAllElements","taggedElement","getTaggedElements","getWidth","offsetWidth","scroll","rightMostElement","sizeIFrameThrottled","sizeIFrame","remaining","this","arguments","clearTimeout","setTimeout","later","event","processRequestFromParent","init","data","source","reset","log","triggerReset","sendSize","moveToAnchor","findTarget","getData","inPageLink","pageInfo","msgBody","JSON","parse","message","getMessageType","split","substr","indexOf","isInitMsg","true","false","callFromParent","messageType","module","exports","jQuery","prototype","chkLateLoaded","el","evt","capitalizeFirstLetter","string","charAt","toUpperCase","slice","formatLogMsg","msg","console","strBool","str","Number","enable","readDataFromParent","location","href","setupCustomCalcMethods","calcMode","calcFunc","iFrameResizer","constructor","stringify","keys","forEach","depricate","targetOrigin","heightCalculationMethod","widthCalculationMethod","readData","readDataFromPage","setBodyStyle","attr","value","chkCSS","setMargin","clearFix","createElement","style","clear","display","appendChild","injectClearFixIntoBodyElement","checkHeightMode","checkWidthMode","parentIFrame","startEventListeners","manageEventListeners","disconnect","clearInterval","sendMsg","close","getId","getPageInfo","callback","hash","resetIFrame","scrollTo","x","y","scrollToOffset","sendMessage","setHeightCalculationMethod","setWidthCalculationMethod","setTargetOrigin","size","customHeight","customWidth","getElementPosition","elPosition","getBoundingClientRect","pagePosition","pageXOffset","scrollLeft","pageYOffset","scrollTop","parseInt","left","top","jumpPosition","hashData","decodeURIComponent","getElementById","getElementsByName","checkLocationHash","bindAnchors","Array","call","querySelectorAll","getAttribute","e","preventDefault","setupInPageLinks","key","splitName","name","manageTriggerEvent","listener","add","eventName","handleEvent","eventType","remove","eventNames","map","method","checkCalcMode","calcModeDefault","modes","type","forceIntervalTimer","MutationObserver","WebKitMutationObserver","initInterval","addImageLoadListners","mutation","addImageLoadListener","element","complete","src","imageLoaded","imageError","elements","push","attributeName","removeImageLoadListener","splice","removeFromArray","imageEventTriggered","typeDesc","mutationObserved","mutations","observer","querySelector","observe","attributes","attributeOldValue","characterData","characterDataOldValue","childList","subtree","createMutationObserver","setupBodyMutationObserver","setupMutationObserver","setInterval","abs","prop","retVal","defaultView","side","elementsLength","elVal","maxVal","Side","timer","i","chkEventThottle","dimention","tag","triggerEvent","triggerEventDesc","currentHeight","currentWidth","checkTolarance","a","b","lockTrigger","resetPage","hcm","postMessage","readyState"],"mappings":"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","file":"iframeResizer.contentWindow.min.js"}
\ No newline at end of file
diff --git a/public/iframeResizer.contentWindow.min.js b/public/iframeResizer.contentWindow.min.js
deleted file mode 100644
index 5f94fc43d..000000000
--- a/public/iframeResizer.contentWindow.min.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/*! iFrame Resizer (iframeSizer.contentWindow.min.js) - v4.2.11 - 2020-06-02
- * Desc: Include this file in any page being loaded into an iframe
- * to force the iframe to resize to the content size.
- * Requires: iframeResizer.min.js on host page.
- * Copyright: (c) 2020 David J. Bradshaw - dave@bradshaw.net
- * License: MIT
- */
-
-!function(d){if("undefined"!=typeof window){var n=!0,o=10,i="",r=0,a="",t=null,u="",c=!1,s={resize:1,click:1},l=128,f=!0,m=1,h="bodyOffset",g=h,p=!0,v="",y={},b=32,w=null,T=!1,E="[iFrameSizer]",O=E.length,S="",M={max:1,min:1,bodyScroll:1,documentElementScroll:1},I="child",N=!0,A=window.parent,C="*",z=0,k=!1,e=null,R=16,x=1,L="scroll",F=L,P=window,D=function(){re("onMessage function not defined")},j=function(){},q=function(){},H={height:function(){return re("Custom height calculation function not defined"),document.documentElement.offsetHeight},width:function(){return re("Custom width calculation function not defined"),document.body.scrollWidth}},W={},B=!1;try{var J=Object.create({},{passive:{get:function(){B=!0}}});window.addEventListener("test",ee,J),window.removeEventListener("test",ee,J)}catch(e){}var U,V,K,Q,X,Y,G=Date.now||function(){return(new Date).getTime()},Z={bodyOffset:function(){return document.body.offsetHeight+pe("marginTop")+pe("marginBottom")},offset:function(){return Z.bodyOffset()},bodyScroll:function(){return document.body.scrollHeight},custom:function(){return H.height()},documentElementOffset:function(){return document.documentElement.offsetHeight},documentElementScroll:function(){return document.documentElement.scrollHeight},max:function(){return Math.max.apply(null,ye(Z))},min:function(){return Math.min.apply(null,ye(Z))},grow:function(){return Z.max()},lowestElement:function(){return Math.max(Z.bodyOffset()||Z.documentElementOffset(),ve("bottom",we()))},taggedElement:function(){return be("bottom","data-iframe-height")}},$={bodyScroll:function(){return document.body.scrollWidth},bodyOffset:function(){return document.body.offsetWidth},custom:function(){return H.width()},documentElementScroll:function(){return document.documentElement.scrollWidth},documentElementOffset:function(){return document.documentElement.offsetWidth},scroll:function(){return Math.max($.bodyScroll(),$.documentElementScroll())},max:function(){return Math.max.apply(null,ye($))},min:function(){return Math.min.apply(null,ye($))},rightMostElement:function(){return ve("right",we())},taggedElement:function(){return be("right","data-iframe-width")}},_=(U=Te,X=null,Y=0,function(){var e=G(),t=R-(e-(Y=Y||e));return V=this,K=arguments,t<=0||RM[c]["max"+e])throw new Error("Value for min"+e+" can not be greater than max"+e)}c in M&&"iFrameResizer"in i?E(c,"Ignored iFrame, already setup."):(d=(d=e)||{},M[c]={firstRun:!0,iframe:i,remoteHost:i.src&&i.src.split("/").slice(0,3).join("/")},function(e){if("object"!=typeof e)throw new TypeError("Options is not an object")}(d),Object.keys(d).forEach(n,d),function(e){for(var n in w)Object.prototype.hasOwnProperty.call(w,n)&&(M[c][n]=Object.prototype.hasOwnProperty.call(e,n)?e[n]:w[n])}(d),M[c]&&(M[c].targetOrigin=!0===M[c].checkOrigin?function(e){return""===e||null!==e.match(/^(about:blank|javascript:|file:\/\/)/)?"*":e}(M[c].remoteHost):"*"),function(){switch(R(c,"IFrame scrolling "+(M[c]&&M[c].scrolling?"enabled":"disabled")+" for "+c),i.style.overflow=!1===(M[c]&&M[c].scrolling)?"hidden":"auto",M[c]&&M[c].scrolling){case"omit":break;case!0:i.scrolling="yes";break;case!1:i.scrolling="no";break;default:i.scrolling=M[c]?M[c].scrolling:"no"}}(),f("Height"),f("Width"),u("maxHeight"),u("minHeight"),u("maxWidth"),u("minWidth"),"number"!=typeof(M[c]&&M[c].bodyMargin)&&"0"!==(M[c]&&M[c].bodyMargin)||(M[c].bodyMarginV1=M[c].bodyMargin,M[c].bodyMargin=M[c].bodyMargin+"px"),r=L(c),(s=p())&&(a=s,i.parentNode&&new a(function(e){e.forEach(function(e){Array.prototype.slice.call(e.removedNodes).forEach(function(e){e===i&&C(i)})})}).observe(i.parentNode,{childList:!0})),z(i,"load",function(){B("iFrame.onload",r,i,l,!0),function(){var e=M[c]&&M[c].firstRun,n=M[c]&&M[c].heightCalculationMethod in h;!e&&n&&j({iframe:i,height:0,width:0,type:"init"})}()}),B("init",r,i,l,!0),M[c]&&(M[c].iframe.iFrameResizer={close:C.bind(null,M[c].iframe),removeListeners:b.bind(null,M[c].iframe),resize:B.bind(null,"Window resize","resize",M[c].iframe),moveToAnchor:function(e){B("Move to anchor","moveToAnchor:"+e,M[c].iframe,c)},sendMessage:function(e){B("Send Message","message:"+(e=JSON.stringify(e)),M[c].iframe,c)}}))}function c(e,n){null===i&&(i=setTimeout(function(){i=null,e()},n))}function u(){"hidden"!==document.visibilityState&&(R("document","Trigger event: Visiblity change"),c(function(){f("Tab Visable","resize")},16))}function f(n,i){Object.keys(M).forEach(function(e){!function(e){return M[e]&&"parent"===M[e].resizeFrom&&M[e].autoResize&&!M[e].firstRun}(e)||B(n,i,M[e].iframe,e)})}function y(){z(window,"message",n),z(window,"resize",function(){!function(e){R("window","Trigger event: "+e),c(function(){f("Window "+e,"resize")},16)}("resize")}),z(document,"visibilitychange",u),z(document,"-webkit-visibilitychange",u)}function q(){function i(e,n){n&&(function(){if(!n.tagName)throw new TypeError("Object is not a valid DOM element");if("IFRAME"!==n.tagName.toUpperCase())throw new TypeError("Expected