diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..ee82116646 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +max_line_length = 120 +trim_trailing_whitespace = true +indent_style = tab +indent_size = 4 + +[*.yml] +indent_style = space +indent_size = 2 + +[*.pg] +trim_trailing_whitespace = false diff --git a/.github/workflows/check-formats.yml b/.github/workflows/check-formats.yml new file mode 100644 index 0000000000..ec47d6c741 --- /dev/null +++ b/.github/workflows/check-formats.yml @@ -0,0 +1,44 @@ +--- +name: Check Formatting of Code Base + +defaults: + run: + shell: bash + +on: + push: + branches-ignore: [main, develop] + pull_request: + +jobs: + perltidy: + name: Check Perl file formatting with perltidy + runs-on: ubuntu-22.04 + container: + image: perl:5.34 + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install dependencies + run: cpanm -n Perl::Tidy@20220613 + - name: Run perltidy + shell: bash + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + shopt -s extglob globstar nullglob + perltidy --pro=./.perltidyrc -b -bext='/' ./**/*.p[lm] ./**/*.t && git diff --exit-code + + prettier: + name: Check JavaScript, style, and HTML file formatting with prettier + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Install Dependencies + run: cd htdocs && npm ci --ignore-scripts + - name: Check formatting with prettier + run: cd htdocs && npm run prettier-check diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml deleted file mode 100644 index f4ff1e90d3..0000000000 --- a/.github/workflows/linter.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Lint Code Base - -defaults: - run: - shell: bash - -on: - push: - branches-ignore: [main, develop] - pull_request: - -jobs: - perltidy: - name: Run perltidy on Perl Files - runs-on: ubuntu-22.04 - container: - image: perl:5.34 - steps: - - uses: actions/checkout@v3 - - name: perl -V - run: perl -V - - name: Install dependencies - run: cpanm -n Perl::Tidy@20220613 - - name: perltidy --version - run: perltidy --version - - name: Run perltidy - shell: bash - run: | - git config --global --add safe.directory "$GITHUB_WORKSPACE" - shopt -s extglob globstar nullglob - perltidy --pro=./.perltidyrc -b -bext='/' ./**/*.p[lm] ./**/*.t && git diff --exit-code diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 02962164ea..620d179bdb 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout PG code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Ubuntu dependencies run: | @@ -28,6 +28,8 @@ jobs: libhtml-parser-perl \ libjson-perl \ libjson-xs-perl \ + liblocale-maketext-lexicon-perl \ + libmojolicious-perl \ libtest2-suite-perl \ libtie-ixhash-perl \ libuuid-tiny-perl \ diff --git a/.gitignore b/.gitignore index 0ee1d29ce5..b6da4a2dea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ -conf/pg_config.yml +*~ +*.swp *.bak + +conf/pg_config.yml cover_db/ htdocs/node_modules diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..b21dab0657 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "arrowParens": "always", + "bracketSpacing": true, + "printWidth": 120, + "semi": true, + "singleQuote": true, + "trailingComma": "none" +} diff --git a/LICENSE b/LICENSE index 8f723f9b5d..deff245e81 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ Online Homework Delivery System Version 2.* - Copyright 2000-2023, The WeBWorK Project + Copyright 2000-2024, The WeBWorK Project All rights reserved. This program is free software; you can redistribute it and/or modify diff --git a/README b/README index 3979df83b4..666e0280ac 100644 --- a/README +++ b/README @@ -6,6 +6,6 @@ http://webwork.maa.org/wiki/Category:Release_Notes - Copyright 2000-2023, The WeBWorK Project + Copyright 2000-2024, The WeBWorK Project http://webwork.maa.org All rights reserved. diff --git a/VERSION b/VERSION index 4863bcf17d..c62c4fcd26 100644 --- a/VERSION +++ b/VERSION @@ -1,4 +1,4 @@ -$PG_VERSION ='2.18'; -$PG_COPYRIGHT_YEARS = '1996-2023'; +$PG_VERSION ='2.19'; +$PG_COPYRIGHT_YEARS = '1996-2024'; 1; diff --git a/assets/tex/pg.sty b/assets/tex/pg.sty index 80ae201058..5cf975a592 100644 --- a/assets/tex/pg.sty +++ b/assets/tex/pg.sty @@ -36,3 +36,7 @@ % semantic macro definitions used by PG \newcommand{\answerRule}[2][]{\raisebox{-3pt}{\parbox[t]{#2ex}{\hrulefill}}} +% height of a strut, used for example to possition the top border of an inline image +% unit is initialized here, but value is set locally where needed +\newlength{\strutheight} + diff --git a/bin/convert-to-pgml.pl b/bin/convert-to-pgml.pl new file mode 100755 index 0000000000..62fb69fd02 --- /dev/null +++ b/bin/convert-to-pgml.pl @@ -0,0 +1,99 @@ +#!/usr/bin/env perl +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2024 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 + +convert-to-pgml.pl -- Convert pg problem with non-pgml structure to PGML structure. + +=head1 SYNOPSIS + + convert-to-pgml -b -s pgml file1.pg file2.pg ... + +=head1 DESCRIPTION + +This converts each pg file to PGML formatting. In particular, text blocks are +converted to their PGML forms. This includes BEGIN_TEXT/END_TEXT, BEGIN_HINT/END_HINT, +BEGIN_SOLUTION/END_SOLUTION. + +Within each block, the following are converted: math modes to their PGML version, +$BR and $PAR to line breaks or empty lines, C<$HR> to C<--->, bold and italics pairs, +any variables of the form C<$var> to C<[$var]>, scripts from \{ \} to [@ @], and C +to the form C<[_]{}> + +Many code features that are no longer needed are removed including +C, C<texStrings;>> and C<normalStrings;>>. +Any C commands are commented out. + +The C command is parsed, the C is included and C +is removed (because it is loaded by C) and C is added to the +end of the list. + +Note: many of the features are converted correctly, but often there will be errors +after the conversion. Generally after using this script, the PGML style answers +will need to have their corresponding variable added. + +=head2 OPTIONS + +The option C<-b> or C<--backup> will create a C<.bak> file with the original code and +replace the current file with the converted code. + +The option C<-s xyz> or C<--suffix=xyz> will convert the code and write the results in a file +with the given suffix C appended to the file name. If this is not given +C is used. If the C<-b> flag is used, this option will be ignored. + +=cut + +use strict; +use warnings; +use experimental 'signatures'; + +use Mojo::File qw(curfile); +use Getopt::Long; + +use lib curfile->dirname->dirname . '/lib'; + +use WeBWorK::PG::ConvertToPGML qw(convertToPGML); + +my $backup = 0; +my $verbose = 0; +my $suffix = 'pgml'; + +GetOptions( + "b|backup" => \$backup, + "s|suffix=s" => \$suffix, + "v|verbose" => \$verbose, +); + +die 'arguments must have a list of pg files' unless @ARGV > 0; +convertFile($_) for (grep { $_ =~ /\.pg$/ } @ARGV); + +sub convertFile ($filename) { + my $path = Mojo::File->new($filename); + die "The file: $filename does not exist or is not readable" unless -r $path; + + my $pg_source = $path->slurp; + my $converted_source = convertToPGML($pg_source); + + # copy the original file to a backup and then write the file + my $new_path = $backup ? $path : Mojo::File->new($filename =~ s/\.pg/.$suffix/r); + my $backup_file = $filename =~ s/\.pg$/.pg.bak/r; + $path->copy_to($backup_file) if $backup; + $new_path->spurt($converted_source); + print "Writing converted file to $new_path\n" if $verbose; + print "Backing up original file to $backup_file\n" if $verbose && $backup; +} + +1; diff --git a/bin/perltidy-pg.pl b/bin/perltidy-pg.pl index dfd475ebad..2fdfd661f0 100755 --- a/bin/perltidy-pg.pl +++ b/bin/perltidy-pg.pl @@ -1,7 +1,7 @@ #!/usr/bin/env perl ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/bin/run-perltidy.pl b/bin/run-perltidy.pl index 4b27686728..26f40ef10d 100755 --- a/bin/run-perltidy.pl +++ b/bin/run-perltidy.pl @@ -1,7 +1,7 @@ #!/usr/bin/env perl ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/bin/update-localization-files b/bin/update-localization-files new file mode 100755 index 0000000000..50da33991a --- /dev/null +++ b/bin/update-localization-files @@ -0,0 +1,65 @@ +#!/bin/bash + +function print_help_exit +{ + printf "Usage: %s [options]\n" $(basename $0) >&2 + printf " Update the pg.pot and language .po files with translation strings from the code.\n" >&2 + printf " options:\n" >&2 + printf " -p|--po-update Update po files as well. By default only the pg.pot file is updated.\n" >&2 + printf " -l|--langauge Update the only given language in addition to updating the pg.pot file.\n" >&2 + printf " -h|--help Show this help.\n" >&2 + exit 1 +} + +TEMP=$(getopt -a -o pl:h -l po-update,language:,help -n "$(basename $0)" -- "$@") + +eval set -- "$TEMP" + +UPDATE_PO=false +LANGUAGE="" + +while [ ! "$1" = "--" ] +do + case "$1" in + -p|--po-update) + UPDATE_PO=true + shift 1 + ;; + -l|--language) + LANGUAGE=$2 + shift 2 + ;; + -h|--help) + print_help_exit + ;; + *) + echo "Internal error!" + exit 1 + ;; + esac +done + +if [ -z "$PG_ROOT" ]; then + echo >&2 "You need to set the PG_ROOT environment variable. Aborting." + exit 1 +fi + +command -v xgettext.pl >/dev/null 2>&1 || { + echo >&2 "xgettext.pl needs to be installed. It is inlcuded in the perl package Locale::Maketext::Extract. Aborting."; + exit 1; +} + +LOCDIR=$PG_ROOT/lib/WeBWorK/PG/Localize + +cd $LOCDIR + +echo "Updating $LOCDIR/pg.pot" + +xgettext.pl -o pg.pot -D $PG_ROOT/lib -D $PG_ROOT/macros + +if $UPDATE_PO; then + find $LOCDIR -name '*.po' -exec bash -c "echo \"Updating {}\"; msgmerge -qUN {} pg.pot" \; +elif [[ $LANGUAGE != "" && -e "$LANGUAGE.po" ]]; then + echo "Updating $LOCDIR/$LANGUAGE.po" + msgmerge -qUN $LANGUAGE.po pg.pot +fi diff --git a/conf/pg_config.dist.yml b/conf/pg_config.dist.yml index 144587cdc5..9fedd6e308 100644 --- a/conf/pg_config.dist.yml +++ b/conf/pg_config.dist.yml @@ -52,7 +52,6 @@ directories: - $pg_root/macros/ui - $pg_root/macros/deprecated - URLs: # The public URL of the html directory above. html: /pg_files @@ -85,12 +84,9 @@ equationCacheDB: '' externalPrograms: curl: /usr/bin/curl - cp: /bin/cp - mv: /bin/mv - rm: /bin/rm tar: /bin/tar latex: /usr/bin/latex --no-shell-escape - pdflatex: /usr/bin/pdflatex --no-shell-escape + latex2pdf: /usr/bin/xelatex --no-shell-escape dvisvgm: /usr/bin/dvisvgm pdf2svg: /usr/bin/pdf2svg convert: /usr/bin/convert @@ -114,7 +110,7 @@ specialPGEnvironmentVars: # Binary that the PGtikz.pl and PGlateximage.pl macros will use to create svg images. # This should be either 'pdf2svg' or 'dvisvgm'. - latexImageSVGMethod: pdf2svg + latexImageSVGMethod: dvisvgm # When ImageMagick is used for image conversions, this sets the default options. # See https://imagemagick.org/script/convert.php for a full list of options. @@ -201,9 +197,10 @@ displayModeOptions: passwd: '' # PG modules to load -# The first item of each list is the module to load. The remaining items are additional packages to import. -# That is: If you wish to include a module MyModule.pm which depends on additional modules Dependency1.pm and -# Dependency2.pm, these should appear as [Mymodule, Dependency1, Dependency2] +# The first item of each list is the module file to load. The remaining items are additional packages to import that are +# also contained in that file. +# That is, if you wish to include a file MyModule.pm which containes the package MyModule and the additional packages +# Dependency1 and Dependency2, then these should appear as [Mymodule, Dependency1, Dependency2]. modules: - [Encode] - ['Encode::Encoding'] @@ -212,7 +209,8 @@ modules: - [DynaLoader] - [Exporter] - [GD] - - [AlgParser, AlgParserWithImplicitExpand, Expr, ExprWithImplicitExpand, utf8] + - [utf8] + - [AlgParser, AlgParserWithImplicitExpand, Expr, ExprWithImplicitExpand] - [AnswerHash, AnswerEvaluator] - [LaTeXImage] - [WWPlot] # required by Circle (and others) @@ -235,14 +233,25 @@ modules: - [Select] - [Units] - [VectorField] - - [Parser, Value] + - [Parser] + - [Value] - ['Parser::Legacy'] - [Statistics] - [Chromatic] # for Northern Arizona graph problems - [Applet] - - [PGcore, PGalias, PGresource, PGloadfiles, PGanswergroup, PGresponsegroup, 'Tie::IxHash'] + - [PGcore] + - [PGalias] + - [PGresource] + - [PGloadfiles] + - [PGanswergroup] + - [PGresponsegroup] + - ['Tie::IxHash'] - ['Locale::Maketext'] + - ['WeBWorK::PG::Localize'] - [JSON] - - [Rserve, 'Class::Tiny', 'IO::Handle'] + - ['Class::Tiny'] + - ['IO::Handle'] + - ['Rserve'] - [DragNDrop] - ['Types::Serialiser'] + - [strict] diff --git a/cpanfile b/cpanfile index cf158276ba..ca6cbe1f03 100644 --- a/cpanfile +++ b/cpanfile @@ -17,6 +17,8 @@ on runtime => sub { requires 'JSON'; requires 'JSON::XS'; requires 'Locale::Maketext'; + requires 'Locale::Maketext::Lexicon'; + requires 'Mojolicious'; requires 'Tie::IxHash'; requires 'Types::Serialiser'; requires 'UUID::Tiny'; diff --git a/docker/pg.Dockerfile b/docker/pg.Dockerfile index ae2fcbc7e5..36741fc5ff 100644 --- a/docker/pg.Dockerfile +++ b/docker/pg.Dockerfile @@ -20,6 +20,8 @@ RUN apt-get update \ libhtml-parser-perl \ libjson-perl \ libjson-xs-perl \ + liblocale-maketext-lexicon-perl \ + libmojolicious-perl \ libtest2-suite-perl \ libtie-ixhash-perl \ libuuid-tiny-perl \ diff --git a/htdocs/generate-assets.js b/htdocs/generate-assets.js index 0feadbf350..91647b2c6f 100755 --- a/htdocs/generate-assets.js +++ b/htdocs/generate-assets.js @@ -15,7 +15,10 @@ const rtlcss = require('rtlcss'); const cssMinify = require('cssnano'); const argv = yargs - .usage('$0 Options').version(false).alias('help', 'h').wrap(100) + .usage('$0 Options') + .version(false) + .alias('help', 'h') + .wrap(100) .option('enable-sourcemaps', { alias: 's', description: 'Generate source maps. (Not for use in production!)', @@ -30,8 +33,7 @@ const argv = yargs alias: 'd', description: 'Delete all generated files.', type: 'boolean' - }) - .argv; + }).argv; const assetFile = path.resolve(__dirname, 'static-assets.json'); const assets = {}; @@ -48,7 +50,7 @@ const cleanDir = (dir) => { } } } -} +}; // The is set to true after all files are processed for the first time. let ready = false; @@ -75,12 +77,13 @@ const processFile = async (file, _details) => { return; } - const minJS = result.code + ( - argv.enableSourcemaps && result.map - ? `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${ - Buffer.from(result.map).toString('base64')}` - : '' - ); + const minJS = + result.code + + (argv.enableSourcemaps && result.map + ? `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from( + result.map + ).toString('base64')}` + : ''); const contentHash = crypto.createHash('sha256'); contentHash.update(minJS); @@ -114,18 +117,19 @@ const processFile = async (file, _details) => { return; } - if (result.sourceMap) result.sourceMap.sources = [ baseName ]; + if (result.sourceMap) result.sourceMap.sources = [baseName]; // Pass the compiled css through the autoprefixer. // This is really only needed for the bootstrap.css files, but doesn't hurt for the rest. let prefixedResult = await postcss([autoprefixer, cssMinify]).process(result.css, { from: baseName }); - const minCSS = prefixedResult.css + ( - argv.enableSourcemaps && result.sourceMap - ? `/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${ - Buffer.from(JSON.stringify(result.sourceMap)).toString('base64')}*/` - : '' - ); + const minCSS = + prefixedResult.css + + (argv.enableSourcemaps && result.sourceMap + ? `/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from( + JSON.stringify(result.sourceMap) + ).toString('base64')}*/` + : ''); const contentHash = crypto.createHash('sha256'); contentHash.update(minCSS); @@ -149,18 +153,21 @@ const processFile = async (file, _details) => { // Pass the compiled css through rtlcss and autoprefixer to generate css for right-to-left languages. let rtlResult = await postcss([rtlcss, autoprefixer, cssMinify]).process(result.css, { from: baseName }); - const rtlCSS = rtlResult.css + ( - argv.enableSourcemaps && result.sourceMap - ? `/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${ - Buffer.from(JSON.stringify(result.sourceMap)).toString('base64')}*/` - : '' - ); + const rtlCSS = + rtlResult.css + + (argv.enableSourcemaps && result.sourceMap + ? `/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from( + JSON.stringify(result.sourceMap) + ).toString('base64')}*/` + : ''); const rtlContentHash = crypto.createHash('sha256'); rtlContentHash.update(rtlCSS); - const newRTLVersion = file.replace(/\.s?css$/, - `.rtl.${rtlContentHash.digest('hex').substring(0, 8)}.min.css`); + const newRTLVersion = file.replace( + /\.s?css$/, + `.rtl.${rtlContentHash.digest('hex').substring(0, 8)}.min.css` + ); fs.writeFileSync(path.resolve(__dirname, newRTLVersion), rtlCSS); const rtlAssetName = file.replace(/\.s?css$/, '.rtl.css'); @@ -180,8 +187,9 @@ const processFile = async (file, _details) => { } } else { if (argv.watchFiles) - console.log('\x1b[33mWatches established, and initial build complete.\n' - + 'Press Control-C to stop.\x1b[0m'); + console.log( + '\x1b[33mWatches established, and initial build complete.\n' + 'Press Control-C to stop.\x1b[0m' + ); ready = true; } @@ -195,15 +203,16 @@ if (argv.clean) process.exit(); // Set up the watcher. if (argv.watchFiles) console.log('\x1b[32mEstablishing watches and performing initial build.\x1b[0m'); -chokidar.watch(['js'], { - ignored: /\.min\.(js|css)$/, - cwd: __dirname, // Make sure all paths are given relative to the htdocs directory. - usePolling: true, // Needed to get changes to symlinks. - interval: 500, - awaitWriteFinish: { stabilityThreshold: 500 }, - persistent: argv.watchFiles ? true : false -}) - .on('add', processFile).on('change', processFile).on('ready', processFile) +chokidar + .watch(['js'], { + ignored: /\.min\.(js|css)$/, + cwd: __dirname, // Make sure all paths are given relative to the htdocs directory. + awaitWriteFinish: { stabilityThreshold: 500 }, + persistent: argv.watchFiles ? true : false + }) + .on('add', processFile) + .on('change', processFile) + .on('ready', processFile) .on('unlink', (file) => { // If a file is deleted, then also delete the corresponding generated file. if (assets[file]) { diff --git a/htdocs/helpFiles/Entering-Angles.html b/htdocs/helpFiles/Entering-Angles.html index 611d879e0c..bfa15541e0 100644 --- a/htdocs/helpFiles/Entering-Angles.html +++ b/htdocs/helpFiles/Entering-Angles.html @@ -6,21 +6,22 @@

Entering Angles

For an angle of 60 degrees, enter it in radians as pi/3 or - 1.04719..., but not 60. + 1.04719..., but not 60.

By default, trig functions are evaluated in radians, so cos(pi/3) = 1/2, but - cos(60) = -0.9524 since it is radians. - You must convert degrees to radians before applying a trig function to an angle. + cos(60) = -0.9524 since it is radians. You must convert degrees to + radians before applying a trig function to an angle.

  • Occasionally, units are required on angles:
    - If asked for units on an angle, enter, for example, pi/6 rad - (including rad) or 30 deg (including deg). + If asked for units on an angle, enter, for example, pi/6 rad (including + rad) or 30 deg (including deg).
  • diff --git a/htdocs/helpFiles/Entering-Equations.html b/htdocs/helpFiles/Entering-Equations.html index e0041bfdc8..bd758fdda8 100644 --- a/htdocs/helpFiles/Entering-Equations.html +++ b/htdocs/helpFiles/Entering-Equations.html @@ -12,8 +12,8 @@

    Entering Equations

    Examples of valid equations that are equivalent:

    - 32 = 5*x + 2 is the same as 30 = 5x - or x = 6 + 32 = 5*x + 2 is the same as + 30 = 5x or x = 6

    diff --git a/htdocs/helpFiles/Entering-Formulas.html b/htdocs/helpFiles/Entering-Formulas.html index 6868d6fcbe..60e8dea8b6 100644 --- a/htdocs/helpFiles/Entering-Formulas.html +++ b/htdocs/helpFiles/Entering-Formulas.html @@ -12,7 +12,7 @@

    Entering Formulas

  • Examples of valid formulas:
    -
      +
      • 5*sin((pi*x)/2) or 5 sin(pi x/2)
      • @@ -20,9 +20,7 @@

        Entering Formulas

        e^(-x) or e**(-x) or 1/(e^x) -
      • - abs(5y) or |5y| -
      • +
      • abs(5y) or |5y|
      • sqrt(9 - z^2) or (9 - z^2)^(1/2)
      • @@ -40,8 +38,8 @@

        Entering Formulas

      • Entering logarithms:
        - In this question, use ln(x) or log(x) - for natural log, and logten(x) or + In this question, use ln(x) or + log(x) for natural log, and logten(x) or log10(x) for the base 10 logarithm. Enter log base b as ln(x)/ln(b).
        @@ -55,14 +53,14 @@

        Entering Formulas

        Addition +, subtraction -, multiplication *, division /, - exponentiation ^ (or **), - factorial ! + exponentiation ^ (or **), factorial + !
      • Examples of functions used in formulas:
        -
          +
          • sqrt(x) = x^(1/2)
          • abs(x) = |x|
          • 2^x
          • diff --git a/htdocs/helpFiles/Entering-Formulas10.html b/htdocs/helpFiles/Entering-Formulas10.html index 27c5048153..7f60e91f56 100644 --- a/htdocs/helpFiles/Entering-Formulas10.html +++ b/htdocs/helpFiles/Entering-Formulas10.html @@ -12,7 +12,7 @@

            Entering Formulas

          • Examples of valid formulas:
            -
              +
              • 5*sin((pi*x)/2) or 5 sin(pi x/2)
              • @@ -20,9 +20,7 @@

                Entering Formulas

                e^(-x) or e**(-x) or 1/(e^x) -
              • - abs(5y) or |5y| -
              • +
              • abs(5y) or |5y|
              • sqrt(9 - z^2) or (9 - z^2)^(1/2)
              • @@ -55,14 +53,14 @@

                Entering Formulas

                Addition +, subtraction -, multiplication *, division /, - exponentiation ^ (or **), - factorial ! + exponentiation ^ (or **), factorial + !
              • Examples of functions used in formulas:
                -
                  +
                  • sqrt(x) = x^(1/2)
                  • abs(x) = |x|
                  • 2^x
                  • diff --git a/htdocs/helpFiles/Entering-Fractions.html b/htdocs/helpFiles/Entering-Fractions.html index 6a6935b93b..8dbd9ddf19 100644 --- a/htdocs/helpFiles/Entering-Fractions.html +++ b/htdocs/helpFiles/Entering-Fractions.html @@ -19,15 +19,15 @@

                    Entering Fractions

                  • Sometimes decimals are not allowed:
                    -

                    Allowed: 5/2, -1/3, pi/3, 4, sqrt(2)/2, 2^(1/2)

                    -

                    Not allowed: 2.5, -0.33333, 3.14159/3, 0.707106/2, 2^(0.5)

                    +

                    Allowed: 5/2, -1/3, pi/3, 4, sqrt(2)/2, 2^(1/2)

                    +

                    Not allowed: 2.5, -0.33333, 3.14159/3, 0.707106/2, 2^(0.5)

                  • Sometimes a mixed fraction is required:
                    - Enter 1 2/3 (for 1 and 2/3) with a space between the 1 and the 2 instead of - 5/3. + Enter 1 2/3 (for 1 and 2/3) with a space between the 1 and the 2 instead + of 5/3.
                  • @@ -54,8 +54,8 @@

                    Entering Fractions

                    Sometimes, certain operations are not allowed.
                    Usually, the operations that are not allowed include addition +, - subtraction -, multiplication *, - and exponentiation ^ (or **). When these + subtraction -, multiplication *, and + exponentiation ^ (or **). When these operations are not allowed, it is usually because you are expected to be able to simplify your answer, often without using a calculator.
                    diff --git a/htdocs/helpFiles/Entering-Inequalities.html b/htdocs/helpFiles/Entering-Inequalities.html index a553485e21..904e0f453b 100644 --- a/htdocs/helpFiles/Entering-Inequalities.html +++ b/htdocs/helpFiles/Entering-Inequalities.html @@ -7,15 +7,19 @@

                    Entering Inequalities

                    • < less than
                    • - <= less than or equal to - (=< might also work) + <= less than or equal to (=< + might also work)
                    • = equals
                    • != not equal to (uses exclamation point)
                    • > greater than
                    • - >= greater than or equal to - (=> might also work) + >= greater than or equal to (=> + might also work)
                @@ -125,8 +129,8 @@

                Entering Inequalities

                (-inf,0)U(2,inf) - or is special, - U denotes union + or is special, U denotes + union diff --git a/htdocs/helpFiles/Entering-Intervals.html b/htdocs/helpFiles/Entering-Intervals.html index 46d3a2edb6..69fdf337b7 100644 --- a/htdocs/helpFiles/Entering-Intervals.html +++ b/htdocs/helpFiles/Entering-Intervals.html @@ -1,35 +1,35 @@

                Using Interval Notation

                  -
                • +
                • If an endpoint is included, then use [ or ]. If not, then use ( or ). For example, the interval from -3 to 7 that includes 7 but not -3 is expressed (-3,7].
                • -
                • +
                • For infinite intervals, use Inf for - (infinity) and - -Inf for -∞ + (infinity) and + -Inf for -∞ (-Infinity). For example, the infinite interval containing all points greater than or equal to 6 is expressed [6,Inf).
                • -
                • +
                • If the set includes more than one interval, they are joined using the union symbol U. For example, the set consisting of all points in (-3,7] together with all points in [-8,-5) is expressed [-8,-5)U(-3,7].
                • -
                • +
                • If the answer is the empty set, you can specify that by using braces with nothing inside: { }
                • -
                • +
                • You can use R as a shorthand for all real numbers. So, it is equivalent to entering (-Inf, Inf).
                • -
                • +
                • You can use set difference notation. So, for all real numbers except 3, you can use R-{3} or (-Inf, 3)U(3,Inf) (they are the - same). Similarly, [1,10)-{3,4} is the same as + same). Similarly, [1,10)-{3,4} is the same as [1,3)U(3,4)U(4,10).
                • diff --git a/htdocs/helpFiles/Entering-Matrices.html b/htdocs/helpFiles/Entering-Matrices.html index 6d22202d31..681a62ca2c 100644 --- a/htdocs/helpFiles/Entering-Matrices.html +++ b/htdocs/helpFiles/Entering-Matrices.html @@ -4,15 +4,18 @@

                  When there is one big answer box

                  • If the matrix has only one row, enter a list. For example, enter a 1 x 3 matrix as a comma separated list - enclosed by square brackets:
                    [1, 2, 3]
                    + enclosed by square brackets: +
                    [1, 2, 3]
                  • If the matrix has more than one row, enter a list of lists. For example, enter a 2 x 3 matrix with 1, 2, 3 in - the top row and 4, 5, 6 in the bottom row as:
                    [ [1, 2, 3], [4, 5, 6] ]
                    + the top row and 4, 5, 6 in the bottom row as: +
                    [ [1, 2, 3], [4, 5, 6] ]
                  • If the matrix has only one column, enter a list of lists. For example, enter a 2 x 1 matrix with 1 in the top - row and 2 in the bottom row as:
                    [ [1], [2] ]
                    + row and 2 in the bottom row as: +
                    [ [1], [2] ]
                  • Enter DNE (short for Does Not Exist) if the answer is that no matrix with the desired property exists. @@ -25,6 +28,3 @@

                    When there are multiple small answer boxes

                    (often a number or a formula) into each answer box.
                  - - - diff --git a/htdocs/helpFiles/Entering-Numbers.html b/htdocs/helpFiles/Entering-Numbers.html index 318d4c4922..45d57f1d70 100644 --- a/htdocs/helpFiles/Entering-Numbers.html +++ b/htdocs/helpFiles/Entering-Numbers.html @@ -1,4 +1,4 @@ -

                  Entering Angles

                  +

                  Entering Numbers

                  • diff --git a/htdocs/helpFiles/Entering-Syntax.html b/htdocs/helpFiles/Entering-Syntax.html index a95067272b..c8793fd408 100644 --- a/htdocs/helpFiles/Entering-Syntax.html +++ b/htdocs/helpFiles/Entering-Syntax.html @@ -32,10 +32,10 @@

                    Syntax for entering exp

                  • Sometimes using the * symbol to indicate mutiplication makes things easier to - read. For example (1+2)*(3+4) and (1+2)(3+4) - are both valid. So are 3*4 and 3 4 - (3 space 4, not 34) but using a - * makes things clearer. + read. For example (1+2)*(3+4) and + (1+2)(3+4) are both valid. So are 3*4 and + 3 4 (3 space 4, not + 34) but using a * makes things clearer.
                  • Use ('s and )'s to make your meaning clear. @@ -58,7 +58,7 @@

                    Syntax for entering exp Be careful when entering functions. It's always good practice to use parentheses when entering functions. Write sin(t) instead of sint or sin t even though WeBWorK is smart enough to usually accept - sin t or even sint. For example, + sin t or even sint. For example, sin 2t is interpreted as sin(2)t, i.e. (sin(2))*t so be careful.

                  • @@ -75,12 +75,12 @@

                    Syntax for entering exp
                  • For example 2+3sin^2(4x) will work and is equivalent to 2+3(sin(4x))^2 or 2+3sin(4x)^2. Why does the - last expression work? Because things in parentheses are always done first - [ i.e. (4x)], next all functions, such as sin, are evaluated - [giving sin(4x)], next all exponents are taken - [giving sin(4x)^2], next all multiplications and divisions are performed in - order from left to right [giving 3sin(4x)^2], and finally all additions and - subtractions are performed [giving 2+3sin(4x)^2]. + last expression work? Because things in parentheses are always done first [ i.e. + (4x)], next all functions, such as sin, are evaluated [giving + sin(4x)], next all exponents are taken [giving + sin(4x)^2], next all multiplications and divisions are performed in order from + left to right [giving 3sin(4x)^2], and finally all additions and subtractions + are performed [giving 2+3sin(4x)^2].
                  • Is -5^2 positive or negative? It's negative. This is because the square @@ -147,23 +147,23 @@

                    Mathematical Functions
                  • logten( ) The log to the base 10.
                  • arcsin( )
                  • - asin( ) or sin^-1( ) - Another name for arcsin. + asin( ) or sin^-1( ) Another name for + arcsin.
                  • arccos( )
                  • - acos( ) or cos^-1( ) - Another name for arccos. + acos( ) or cos^-1( ) Another name for + arccos.
                  • arctan( )
                  • - atan( ) or tan^-1( ) - Another name for arctan. + atan( ) or tan^-1( ) Another name for + arctan.
                  • arccot( )
                  • - acot( ) or cot^-1( ) - Another name for arccot. + acot( ) or cot^-1( ) Another name for + arccot.
                  • arcsec( )
                  • @@ -172,8 +172,8 @@

                    Mathematical Functions

                  • arccsc( )
                  • - acsc( ) or csc^-1( ) - Another name for arccsc. + acsc( ) or csc^-1( ) Another name for + arccsc.
                  • sinh( )
                  • cosh( )
                  • @@ -183,33 +183,33 @@

                    Mathematical Functions
                  • coth( )
                  • arcsinh( )
                  • - asinh( ) or sinh^-1( ) - Another name for arcsinh. + asinh( ) or sinh^-1( ) Another name for + arcsinh.
                  • arccosh( )
                  • - acosh( ) or cosh^-1( ) - Another name for arccosh. + acosh( ) or cosh^-1( ) Another name for + arccosh.
                  • arctanh( )
                  • - atanh( ) or tanh^-1( ) - Another name for arctanh. + atanh( ) or tanh^-1( ) Another name for + arctanh.
                  • arcsech( )
                  • - asech( ) or sech^-1( ) - Another name for arcsech. + asech( ) or sech^-1( ) Another name for + arcsech.
                  • arccsch( )
                  • - acsch( ) or csch^-1( ) - Another name for arccsch. + acsch( ) or csch^-1( ) Another name for + arccsch.
                  • arccoth( )
                  • - acoth( ) or coth^-1( ) - Another name for arccoth. + acoth( ) or coth^-1( ) Another name for + arccoth.
                  • sqrt( )
                  • @@ -222,14 +222,14 @@

                    Other Mathematical Func

                    These functions may not always be available for every problem.

                    • - sgn( ) The sign function. Its value is one of + sgn( ) The sign function. Its value is one of -1, 0, or 1.
                    • - step( ) The step function. Its value is - 0 if x ≤ 0 and - 1 if x > 0. + step( ) The step function. Its value is 0 if + x ≤ 0 and 1 if + x > 0.
                    • fact(n) diff --git a/htdocs/helpFiles/Entering-Units.html b/htdocs/helpFiles/Entering-Units.html index 271aa54eae..9ac3fc9616 100644 --- a/htdocs/helpFiles/Entering-Units.html +++ b/htdocs/helpFiles/Entering-Units.html @@ -2,7 +2,7 @@

                      Units Available in WeBWorK

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

                      @@ -55,7 +55,7 @@

                      Units Available in WeBWorK

                      - + @@ -130,7 +130,6 @@

                      Units Available in WeBWorK

                      - diff --git a/htdocs/helpFiles/IntervalNotation.html b/htdocs/helpFiles/IntervalNotation.html index 46d3a2edb6..69fdf337b7 100644 --- a/htdocs/helpFiles/IntervalNotation.html +++ b/htdocs/helpFiles/IntervalNotation.html @@ -1,35 +1,35 @@

                      Using Interval Notation

                        -
                      • +
                      • If an endpoint is included, then use [ or ]. If not, then use ( or ). For example, the interval from -3 to 7 that includes 7 but not -3 is expressed (-3,7].
                      • -
                      • +
                      • For infinite intervals, use Inf for - (infinity) and - -Inf for -∞ + (infinity) and + -Inf for -∞ (-Infinity). For example, the infinite interval containing all points greater than or equal to 6 is expressed [6,Inf).
                      • -
                      • +
                      • If the set includes more than one interval, they are joined using the union symbol U. For example, the set consisting of all points in (-3,7] together with all points in [-8,-5) is expressed [-8,-5)U(-3,7].
                      • -
                      • +
                      • If the answer is the empty set, you can specify that by using braces with nothing inside: { }
                      • -
                      • +
                      • You can use R as a shorthand for all real numbers. So, it is equivalent to entering (-Inf, Inf).
                      • -
                      • +
                      • You can use set difference notation. So, for all real numbers except 3, you can use R-{3} or (-Inf, 3)U(3,Inf) (they are the - same). Similarly, [1,10)-{3,4} is the same as + same). Similarly, [1,10)-{3,4} is the same as [1,3)U(3,4)U(4,10).
                      • diff --git a/htdocs/helpFiles/PDE-notation.html b/htdocs/helpFiles/PDE-notation.html index 3ec368c2e8..0834a2e798 100644 --- a/htdocs/helpFiles/PDE-notation.html +++ b/htdocs/helpFiles/PDE-notation.html @@ -67,8 +67,8 @@

                        Models

                      @@ -102,6 +102,5 @@

                      Entering Boundary and Initial Conditions

                      Suppose that we want to enter the boundary condition \(\frac{\partial u}{\partial x}\big\vert_{x=0}=0\). You will be given two answer blanks: the first is to input the partial derivative and the point, \(ux(0,t)\), and the second - will be for the right hand side. In WeBWorK notation the boundary condition would be given as - \(ux(0,t) = 0\). + will be for the right hand side. In WeBWorK notation the boundary condition would be given as \(ux(0,t) = 0\).

                      diff --git a/htdocs/helpFiles/Syntax.html b/htdocs/helpFiles/Syntax.html index a95067272b..c8793fd408 100644 --- a/htdocs/helpFiles/Syntax.html +++ b/htdocs/helpFiles/Syntax.html @@ -32,10 +32,10 @@

                      Syntax for entering exp
                    • Sometimes using the * symbol to indicate mutiplication makes things easier to - read. For example (1+2)*(3+4) and (1+2)(3+4) - are both valid. So are 3*4 and 3 4 - (3 space 4, not 34) but using a - * makes things clearer. + read. For example (1+2)*(3+4) and + (1+2)(3+4) are both valid. So are 3*4 and + 3 4 (3 space 4, not + 34) but using a * makes things clearer.
                    • Use ('s and )'s to make your meaning clear. @@ -58,7 +58,7 @@

                      Syntax for entering exp Be careful when entering functions. It's always good practice to use parentheses when entering functions. Write sin(t) instead of sint or sin t even though WeBWorK is smart enough to usually accept - sin t or even sint. For example, + sin t or even sint. For example, sin 2t is interpreted as sin(2)t, i.e. (sin(2))*t so be careful.

                    • @@ -75,12 +75,12 @@

                      Syntax for entering exp
                    • For example 2+3sin^2(4x) will work and is equivalent to 2+3(sin(4x))^2 or 2+3sin(4x)^2. Why does the - last expression work? Because things in parentheses are always done first - [ i.e. (4x)], next all functions, such as sin, are evaluated - [giving sin(4x)], next all exponents are taken - [giving sin(4x)^2], next all multiplications and divisions are performed in - order from left to right [giving 3sin(4x)^2], and finally all additions and - subtractions are performed [giving 2+3sin(4x)^2]. + last expression work? Because things in parentheses are always done first [ i.e. + (4x)], next all functions, such as sin, are evaluated [giving + sin(4x)], next all exponents are taken [giving + sin(4x)^2], next all multiplications and divisions are performed in order from + left to right [giving 3sin(4x)^2], and finally all additions and subtractions + are performed [giving 2+3sin(4x)^2].
                    • Is -5^2 positive or negative? It's negative. This is because the square @@ -147,23 +147,23 @@

                      Mathematical Functions
                    • logten( ) The log to the base 10.
                    • arcsin( )
                    • - asin( ) or sin^-1( ) - Another name for arcsin. + asin( ) or sin^-1( ) Another name for + arcsin.
                    • arccos( )
                    • - acos( ) or cos^-1( ) - Another name for arccos. + acos( ) or cos^-1( ) Another name for + arccos.
                    • arctan( )
                    • - atan( ) or tan^-1( ) - Another name for arctan. + atan( ) or tan^-1( ) Another name for + arctan.
                    • arccot( )
                    • - acot( ) or cot^-1( ) - Another name for arccot. + acot( ) or cot^-1( ) Another name for + arccot.
                    • arcsec( )
                    • @@ -172,8 +172,8 @@

                      Mathematical Functions

                    • arccsc( )
                    • - acsc( ) or csc^-1( ) - Another name for arccsc. + acsc( ) or csc^-1( ) Another name for + arccsc.
                    • sinh( )
                    • cosh( )
                    • @@ -183,33 +183,33 @@

                      Mathematical Functions
                    • coth( )
                    • arcsinh( )
                    • - asinh( ) or sinh^-1( ) - Another name for arcsinh. + asinh( ) or sinh^-1( ) Another name for + arcsinh.
                    • arccosh( )
                    • - acosh( ) or cosh^-1( ) - Another name for arccosh. + acosh( ) or cosh^-1( ) Another name for + arccosh.
                    • arctanh( )
                    • - atanh( ) or tanh^-1( ) - Another name for arctanh. + atanh( ) or tanh^-1( ) Another name for + arctanh.
                    • arcsech( )
                    • - asech( ) or sech^-1( ) - Another name for arcsech. + asech( ) or sech^-1( ) Another name for + arcsech.
                    • arccsch( )
                    • - acsch( ) or csch^-1( ) - Another name for arccsch. + acsch( ) or csch^-1( ) Another name for + arccsch.
                    • arccoth( )
                    • - acoth( ) or coth^-1( ) - Another name for arccoth. + acoth( ) or coth^-1( ) Another name for + arccoth.
                    • sqrt( )
                    • @@ -222,14 +222,14 @@

                      Other Mathematical Func

                      These functions may not always be available for every problem.

                      • - sgn( ) The sign function. Its value is one of + sgn( ) The sign function. Its value is one of -1, 0, or 1.
                      • - step( ) The step function. Its value is - 0 if x ≤ 0 and - 1 if x > 0. + step( ) The step function. Its value is 0 if + x ≤ 0 and 1 if + x > 0.
                      • fact(n) diff --git a/htdocs/helpFiles/Units.html b/htdocs/helpFiles/Units.html index da55b4ad05..456bc1ada4 100644 --- a/htdocs/helpFiles/Units.html +++ b/htdocs/helpFiles/Units.html @@ -2,7 +2,7 @@

                        Units Available in WeBWorK

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

                    • Metersm, meter, metre, meters or metresm, meter, metre, meters or metres
                      Centimeters gal, gallon or gallons
                      Force
                      wave equation with damping - \(a^2 \frac{\partial^2 u}{\partial x^2} = - \frac{\partial^2 u}{\partial t^2}+c \frac{\partial u}{\partial t}\) + \(a^2 \frac{\partial^2 u}{\partial x^2} = \frac{\partial^2 u}{\partial t^2}+c \frac{\partial u}{\partial + t}\)
                      diff --git a/htdocs/js/AppletSupport/ww_applet_support.js b/htdocs/js/AppletSupport/ww_applet_support.js index e6d74cd859..fe743aa3b6 100644 --- a/htdocs/js/AppletSupport/ww_applet_support.js +++ b/htdocs/js/AppletSupport/ww_applet_support.js @@ -1,6 +1,6 @@ // ################################################################################ // # WeBWorK Online Homework Delivery System -// # Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +// # Copyright © 2000-2024 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 @@ -31,36 +31,38 @@ const getQE = (name1) => { } else { return obj; } -} +}; const getQuestionElement = getQE; // WW_Applet class definition class ww_applet { constructor(appletName) { - this.appletName = appletName; - this.type = ''; - this.initialState = ''; - this.configuration = ''; - this.getStateAlias = ''; - this.setStateAlias = ''; - this.setConfigAlias = ''; - this.getConfigAlias = ''; + this.appletName = appletName; + this.type = ''; + this.initialState = ''; + this.configuration = ''; + this.getStateAlias = ''; + this.setStateAlias = ''; + this.setConfigAlias = ''; + this.getConfigAlias = ''; this.submitActionScript = ''; - this.onInit = 0; - this.debug = 0; + this.onInit = 0; + this.debug = 0; } // Determine whether an XML string has been base64 encoded. // This returns false if the string is empty, or if it contains a < or > character. // The empty string is not a base64 string, and // base64 can't contain < or > and xml strings contain lots of them. - base64Q(str) { return str && !/[<>]+/.exec(str); } + base64Q(str) { + return str && !/[<>]+/.exec(str); + } // Make sure that the applet has this function available methodDefined(methodName) { const applet = getApplet(this.appletName); - if (methodName && typeof(applet[methodName]) == 'function') return true; + if (methodName && typeof applet[methodName] == 'function') return true; if (this.debug) console.log(`${this.appletName}: Method name ${methodName} is not defined`); return false; } @@ -72,11 +74,15 @@ class ww_applet { const applet = getApplet(this.appletName); try { if (this.methodDefined(this.setConfigAlias)) { - if (this.debug) console.log(`${this.appletName}: calling ${this.setConfigAlias}${ - this.debug > 1 ? `with configuration:\n${this.configuration}` : ''}`); + if (this.debug) + console.log( + `${this.appletName}: calling ${this.setConfigAlias}${ + this.debug > 1 ? `with configuration:\n${this.configuration}` : '' + }` + ); applet[this.setConfigAlias](this.configuration); } - } catch(e) { + } catch (e) { console.log(`Error configuring ${this.appletName} using command ${this.setConfigAlias}: ${e}`); } } @@ -90,15 +96,18 @@ class ww_applet { if (this.debug) console.log(`${this.appletName}: calling ${this.getConfigAlias}`); console.log(applet[this.getConfigAlias]()); } - } catch(e) { - console.log(`Error getting configuration for ${this.appletName} using command ${this.getConfigAlias}: ${e}`); + } catch (e) { + console.log( + `Error getting configuration for ${this.appletName} using command ${this.getConfigAlias}: ${e}` + ); } } // Set the state stored on the HTML page setHTMLAppletState(newState) { - if (this.debug) console.log(`${this.appletName}: setHTMLAppletState${this.debug > 1 ? ` to:\n${newState}` : ''}`); - if (typeof(newState) === 'undefined') newState = 'restart_applet'; + if (this.debug) + console.log(`${this.appletName}: setHTMLAppletState${this.debug > 1 ? ` to:\n${newState}` : ''}`); + if (typeof newState === 'undefined') newState = 'restart_applet'; const stateInput = ww_applet_list[this.appletName].stateInput; getQE(stateInput).value = newState; getQE(`previous_${stateInput}`).value = newState; @@ -133,11 +142,10 @@ class ww_applet { // initialState variable. // Exceptional cases - if (state.match(/^restart_applet<\/xml>/) || - state.match(/^\s*$/) || - state.match(/^\s*<\/xml>/)) { - - if (typeof(this.initialState) == 'undefined') { this.initialState = ''; } + if (state.match(/^restart_applet<\/xml>/) || state.match(/^\s*$/) || state.match(/^\s*<\/xml>/)) { + if (typeof this.initialState == 'undefined') { + this.initialState = ''; + } if (this.debug > 1) console.log(`${this.appletName}: Restarting with initial state:\n${this.initialState}`); if (this.initialState.match(/^\s*<\/xml>/) || this.initialState.match(/^\s*$/)) { // Set the saved state to the empty state, so that the submit action will not be overridden by @@ -163,13 +171,18 @@ class ww_applet { if (state.match(/\ 1 ? ` with state ${state}` : ''}`); + if (this.debug) + console.log( + `${this.appletName}: calling ${this.setStateAlias}${ + this.debug > 1 ? ` with state ${state}` : '' + }` + ); applet[this.setStateAlias](state); } - } catch(err) { - console.log(`Error setting state for ${this.appletName} using command ${ - this.setStateAlias}: ${err} ${err.number} ${err.description}`); + } catch (err) { + console.log( + `Error setting state for ${this.appletName} using command ${this.setStateAlias}: ${err} ${err.number} ${err.description}` + ); } } } @@ -232,14 +245,14 @@ class ww_applet { // Configure the applet. try { this.setConfig(); - } catch(e) { + } catch (e) { console.log(`Unable to configure ${this.appletName}:\n${e}`); } // Set the applet state. try { this.setState(); - } catch(e) { + } catch (e) { console.log(`Unable to set the state for ${this.appletName}:\n${e}`); } } @@ -256,10 +269,15 @@ class ww_applet { if (typeof ggbOnInitFromProblem == 'function') { ggbOnInitFromProblem(appletName); } - if (appletName in ww_applet_list && ww_applet_list[appletName].onInit && - ww_applet_list[appletName].onInit != 'ggbOnInit') { - if (window[ww_applet_list[appletName].onInit] && - typeof(window[ww_applet_list[appletName].onInit]) == 'function') { + if ( + appletName in ww_applet_list && + ww_applet_list[appletName].onInit && + ww_applet_list[appletName].onInit != 'ggbOnInit' + ) { + if ( + window[ww_applet_list[appletName].onInit] && + typeof window[ww_applet_list[appletName].onInit] == 'function' + ) { window[ww_applet_list[appletName].onInit](appletName); } else { eval(ww_applet_list[appletName].onInit); @@ -350,7 +368,7 @@ class ww_applet { ww_applet_list[appletName].safe_applet_initialize(); } } - } + }; window.addEventListener('PGContentLoaded', initializeAppletSupport); window.addEventListener('DOMContentLoaded', initializeAppletSupport); diff --git a/htdocs/js/Base64/Base64.js b/htdocs/js/Base64/Base64.js index 7d9536a4f0..e802db0781 100644 --- a/htdocs/js/Base64/Base64.js +++ b/htdocs/js/Base64/Base64.js @@ -1,26 +1,23 @@ - /** -* -* Base64 encode / decode -* http://www.webtoolkit.info/ -* -**/ + * + * Base64 encode / decode + * http://www.webtoolkit.info/ + * + **/ var Base64 = { - // private property - _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", + _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', // public method for encoding - encode : function (input) { - var output = ""; + encode: function (input) { + var output = ''; var chr1, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0; input = Base64._utf8_encode(input); while (i < input.length) { - chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); @@ -36,26 +33,27 @@ var Base64 = { enc4 = 64; } - output = output + - this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + - this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); - + output = + output + + this._keyStr.charAt(enc1) + + this._keyStr.charAt(enc2) + + this._keyStr.charAt(enc3) + + this._keyStr.charAt(enc4); } return output; }, // public method for decoding - decode : function (input) { - var output = ""; + decode: function (input) { + var output = ''; var chr1, chr2, chr3; var enc1, enc2, enc3, enc4; var i = 0; - input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); while (i < input.length) { - enc1 = this._keyStr.indexOf(input.charAt(i++)); enc2 = this._keyStr.indexOf(input.charAt(i++)); enc3 = this._keyStr.indexOf(input.charAt(i++)); @@ -73,71 +71,60 @@ var Base64 = { if (enc4 != 64) { output = output + String.fromCharCode(chr3); } - } output = Base64._utf8_decode(output); return output; - }, // private method for UTF-8 encoding - _utf8_encode : function (string) { - string = string.replace(/\r\n/g,"\n"); - var utftext = ""; + _utf8_encode: function (string) { + string = string.replace(/\r\n/g, '\n'); + var utftext = ''; for (var n = 0; n < string.length; n++) { - var c = string.charCodeAt(n); if (c < 128) { utftext += String.fromCharCode(c); - } - else if((c > 127) && (c < 2048)) { + } else if (c > 127 && c < 2048) { utftext += String.fromCharCode((c >> 6) | 192); utftext += String.fromCharCode((c & 63) | 128); - } - else { + } else { utftext += String.fromCharCode((c >> 12) | 224); utftext += String.fromCharCode(((c >> 6) & 63) | 128); utftext += String.fromCharCode((c & 63) | 128); } - } return utftext; }, // private method for UTF-8 decoding - _utf8_decode : function (utftext) { - var string = ""; + _utf8_decode: function (utftext) { + var string = ''; var i = 0; - var c = c1 = c2 = 0; - - while ( i < utftext.length ) { + var c = (c1 = c2 = 0); + while (i < utftext.length) { c = utftext.charCodeAt(i); if (c < 128) { string += String.fromCharCode(c); i++; - } - else if((c > 191) && (c < 224)) { - c2 = utftext.charCodeAt(i+1); + } else if (c > 191 && c < 224) { + c2 = utftext.charCodeAt(i + 1); string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); i += 2; - } - else { - c2 = utftext.charCodeAt(i+1); - c3 = utftext.charCodeAt(i+2); + } else { + c2 = utftext.charCodeAt(i + 1); + c3 = utftext.charCodeAt(i + 2); string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); i += 3; } - } return string; } - -} +}; diff --git a/htdocs/js/DragNDrop/dragndrop.js b/htdocs/js/DragNDrop/dragndrop.js index d92e91c97b..d7e78b693f 100644 --- a/htdocs/js/DragNDrop/dragndrop.js +++ b/htdocs/js/DragNDrop/dragndrop.js @@ -112,7 +112,8 @@ const bucketLabel = document.createElement('div'); bucketLabel.classList.add('dd-bucket-label'); bucketLabel.innerHTML = - label || (this.bucketPool.labelFormat ? `${this.bucketPool.labelFormat.replace(/%s/, this.id + 1)}` : ''); + label || + (this.bucketPool.labelFormat ? `${this.bucketPool.labelFormat.replace(/%s/, this.id + 1)}` : ''); this.ddList = document.createElement('div'); this.ddList.classList.add('dd-list'); diff --git a/htdocs/js/Essay/essay.js b/htdocs/js/Essay/essay.js index 2d0fab6b38..7f6f11df0c 100644 --- a/htdocs/js/Essay/essay.js +++ b/htdocs/js/Essay/essay.js @@ -1,17 +1,28 @@ 'use strict'; (() => { - const addPreviewButton = (latexEntry) => { - if (latexEntry.dataset.previewBtnAdded) return; - latexEntry.dataset.previewBtnAdded = 'true'; + const initializePreviewButton = (latexEntry) => { + if (latexEntry.dataset.previewBtnInitialized) return; + latexEntry.dataset.previewBtnInitialized = 'true'; - const buttonContainer = document.createElement('div'); - buttonContainer.classList.add('latexentry-button-container', 'mt-1'); + const buttonContainer = + document.getElementById(`${latexEntry.id}-latexentry-button-container`) || document.createElement('div'); - const button = document.createElement('button'); - button.type = 'button'; - button.classList.add('latexentry-preview', 'btn', 'btn-secondary', 'btn-sm'); - button.textContent = 'Preview'; + if (!buttonContainer.classList.contains('latexentry-button-container')) { + buttonContainer.classList.add('latexentry-button-container', 'mt-1'); + buttonContainer.id = `${latexEntry.id}-latexentry-button-container`; + latexEntry.after(buttonContainer); + } + + const button = buttonContainer.querySelector('.latexentry-preview') || document.createElement('button'); + + if (!button.classList.contains('latexentry-preview')) { + button.type = 'button'; + button.classList.add('latexentry-preview', 'btn', 'btn-secondary', 'btn-sm'); + button.textContent = 'Preview'; + + buttonContainer.append(button); + } button.addEventListener('click', () => { button.dataset.bsContent = latexEntry.value @@ -40,8 +51,9 @@ button.addEventListener( 'show.bs.popover', () => { - MathJax.startup.promise = - MathJax.startup.promise.then(() => MathJax.typesetPromise(['.popover-body'])); + MathJax.startup.promise = MathJax.startup.promise.then(() => + MathJax.typesetPromise(['.popover-body']) + ); }, { once: true } ); @@ -49,19 +61,16 @@ popover.show(); } }); - - buttonContainer.append(button); - latexEntry.after(buttonContainer); }; - document.querySelectorAll('.latexentryfield').forEach(addPreviewButton); + document.querySelectorAll('.latexentryfield').forEach(initializePreviewButton); const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { for (const node of mutation.addedNodes) { if (node instanceof Element) { - if (node.classList.contains('latexentryfield')) addPreviewButton(node); - else node.querySelectorAll('.latexentryfield').forEach(addPreviewButton); + if (node.classList.contains('latexentryfield')) initializePreviewButton(node); + else node.querySelectorAll('.latexentryfield').forEach(initializePreviewButton); } } } diff --git a/htdocs/js/Feedback/feedback.js b/htdocs/js/Feedback/feedback.js new file mode 100644 index 0000000000..4562ac8199 --- /dev/null +++ b/htdocs/js/Feedback/feedback.js @@ -0,0 +1,96 @@ +(() => { + const feedbackPopovers = []; + + const initializeFeedback = (feedbackBtn) => { + if (feedbackBtn.dataset.popoverInitialized) return; + feedbackBtn.dataset.popoverInitialized = 'true'; + + const feedbackPopover = new bootstrap.Popover(feedbackBtn, { + sanitize: false, + container: feedbackBtn.parentElement + }); + feedbackPopovers.push(feedbackPopover); + + // Render MathJax previews. + if (window.MathJax) { + feedbackBtn.addEventListener('show.bs.popover', () => { + MathJax.startup.promise = MathJax.startup.promise.then(() => MathJax.typesetPromise(['.popover-body'])); + }); + } + + feedbackBtn.addEventListener('shown.bs.popover', () => { + // Execute javascript in the answer preview. + feedbackPopover.tip?.querySelectorAll('script').forEach((origScript) => { + const newScript = document.createElement('script'); + Array.from(origScript.attributes).forEach((attr) => newScript.setAttribute(attr.name, attr.value)); + newScript.appendChild(document.createTextNode(origScript.innerHTML)); + origScript.parentNode.replaceChild(newScript, origScript); + setTimeout(() => feedbackPopover.update()); + }); + + const moveToFront = () => { + if (feedbackPopover.tip) feedbackPopover.tip.style.zIndex = 18; + for (const popover of feedbackPopovers) { + if (popover === feedbackPopover) continue; + popover.tip?.style.setProperty('z-index', null); + } + }; + feedbackPopover.tip?.addEventListener('click', moveToFront); + feedbackPopover.tip?.addEventListener('focusin', moveToFront); + moveToFront(); + + // Make a click on the popover header close the popover. + feedbackPopover.tip?.querySelector('.btn-close')?.addEventListener('click', () => feedbackPopover.hide()); + + if (feedbackPopover.tip) feedbackPopover.tip.dataset.iframeHeight = '1'; + + const revealCorrectBtn = feedbackPopover.tip?.querySelector('.reveal-correct-btn'); + if (revealCorrectBtn && feedbackPopover.correctRevealed) { + revealCorrectBtn.nextElementSibling?.classList.remove('d-none'); + revealCorrectBtn.remove(); + } else { + revealCorrectBtn?.addEventListener('click', () => { + feedbackPopover.correctRevealed = true; + revealCorrectBtn.classList.add('fade-out'); + revealCorrectBtn.parentElement.classList.add('resize-transition'); + revealCorrectBtn.parentElement.style.maxWidth = `${revealCorrectBtn.parentElement.offsetWidth}px`; + revealCorrectBtn.parentElement.style.maxHeight = `${revealCorrectBtn.parentElement.offsetHeight}px`; + revealCorrectBtn.addEventListener('animationend', () => { + revealCorrectBtn.nextElementSibling?.classList.remove('d-none'); + revealCorrectBtn.nextElementSibling?.classList.add('fade-in'); + revealCorrectBtn.parentElement.style.maxWidth = '1000px'; + revealCorrectBtn.parentElement.style.maxHeight = '1000px'; + revealCorrectBtn.remove(); + feedbackPopover.update(); + }); + }); + } + }); + + if (feedbackBtn.dataset.showCorrectOnly) { + setTimeout(() => { + feedbackBtn.click(); + setTimeout(() => feedbackPopover.update(), 100); + }, 0); + } + }; + + // Setup feedback popovers already on the page. + document.querySelectorAll('.ww-feedback-btn').forEach(initializeFeedback); + + // Deal with feedback popovers that are added to the page later. + const observer = new MutationObserver((mutationsList) => { + mutationsList.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node instanceof Element) { + if (node.classList.contains('ww-feedback-btn')) initializeFeedback(node.firstElementChild); + else node.querySelectorAll('.ww-feedback-btn').forEach(initializeFeedback); + } + }); + }); + }); + observer.observe(document.body, { childList: true, subtree: true }); + + // Stop the mutation observer when the window is closed. + window.addEventListener('unload', () => observer.disconnect()); +})(); diff --git a/htdocs/js/GraphTool/cubictool.js b/htdocs/js/GraphTool/cubictool.js index 2ee32a333e..54be369ab8 100644 --- a/htdocs/js/GraphTool/cubictool.js +++ b/htdocs/js/GraphTool/cubictool.js @@ -8,8 +8,8 @@ preInit(gt, point1, point2, point3, point4, solid) { [point1, point2, point3, point4].forEach((point) => { point.setAttribute(gt.definingPointAttributes); - point.on('down', () => gt.board.containerObj.style.cursor = 'none'); - point.on('up', () => gt.board.containerObj.style.cursor = 'auto'); + point.on('down', () => (gt.board.containerObj.style.cursor = 'none')); + point.on('up', () => (gt.board.containerObj.style.cursor = 'auto')); }); return gt.graphObjectTypes.cubic.createCubic(point1, point2, point3, point4, solid, gt.color.curve); }, @@ -40,55 +40,94 @@ pointData = gt.pointRegexp.exec(string); } if (points.length < 4) return false; - var point1 = gt.graphObjectTypes.cubic.createPoint( - parseFloat(points[0][0]), parseFloat(points[0][1])); - var point2 = gt.graphObjectTypes.cubic.createPoint( - parseFloat(points[1][0]), parseFloat(points[1][1]), [point1]); - var point3 = gt.graphObjectTypes.cubic.createPoint( - parseFloat(points[2][0]), parseFloat(points[2][1]), [point1, point2]); - var point4 = gt.graphObjectTypes.cubic.createPoint( - parseFloat(points[3][0]), parseFloat(points[3][1]), [point1, point2, point3]); + const point1 = gt.graphObjectTypes.cubic.createPoint( + parseFloat(points[0][0]), + parseFloat(points[0][1]) + ); + const point2 = gt.graphObjectTypes.cubic.createPoint( + parseFloat(points[1][0]), + parseFloat(points[1][1]), + [point1] + ); + const point3 = gt.graphObjectTypes.cubic.createPoint( + parseFloat(points[2][0]), + parseFloat(points[2][1]), + [point1, point2] + ); + const point4 = gt.graphObjectTypes.cubic.createPoint( + parseFloat(points[3][0]), + parseFloat(points[3][1]), + [point1, point2, point3] + ); return new gt.graphObjectTypes.cubic(point1, point2, point3, point4, /solid/.test(string)); }, helperMethods: { createParabola(gt, point1, point2, point3, solid, color) { - return gt.board.create('curve', [ - // x and y coordinates of point on curve - (x) => x, - (x) => { - const x1 = point1.X(), x2 = point2.X(), x3 = point3.X(), - y1 = point1.Y(), y2 = point2.Y(), y3 = point3.Y(); - return (x - x2) * (x - x3) * y1 / ((x1 - x2) * (x1 - x3)) - + (x - x1) * (x - x3) * y2 / ((x2 - x1) * (x2 - x3)) - + (x - x1) * (x - x2) * y3 / ((x3 - x1) * (x3 - x2)); - }, - // domain minimum and maximum - () => gt.board.getBoundingBox()[0], () => gt.board.getBoundingBox()[2] - ], { - strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.color.underConstruction, - dash: solid ? 0 : 2 - }); + return gt.board.create( + 'curve', + [ + // x and y coordinates of point on curve + (x) => x, + (x) => { + const x1 = point1.X(), + x2 = point2.X(), + x3 = point3.X(), + y1 = point1.Y(), + y2 = point2.Y(), + y3 = point3.Y(); + return ( + ((x - x2) * (x - x3) * y1) / ((x1 - x2) * (x1 - x3)) + + ((x - x1) * (x - x3) * y2) / ((x2 - x1) * (x2 - x3)) + + ((x - x1) * (x - x2) * y3) / ((x3 - x1) * (x3 - x2)) + ); + }, + // domain minimum and maximum + () => gt.board.getBoundingBox()[0], + () => gt.board.getBoundingBox()[2] + ], + { + strokeWidth: 2, + highlight: false, + strokeColor: color ? color : gt.color.underConstruction, + dash: solid ? 0 : 2 + } + ); }, createCubic(gt, point1, point2, point3, point4, solid, color) { - return gt.board.create('curve', [ - // x and y coordinate of point on curve - (x) => x, - (x) => { - const x1 = point1.X(), x2 = point2.X(), x3 = point3.X(), x4 = point4.X(), - y1 = point1.Y(), y2 = point2.Y(), y3 = point3.Y(), y4 = point4.Y(); - return (x - x2) * (x - x3) * (x - x4) * y1 / ((x1 - x2) * (x1 - x3) * (x1 - x4)) - + (x - x1) * (x - x3) * (x - x4) * y2 / ((x2 - x1) * (x2 - x3) * (x2 - x4)) - + (x - x1) * (x - x2) * (x - x4) * y3 / ((x3 - x1) * (x3 - x2) * (x3 - x4)) - + (x - x1) * (x - x2) * (x - x3) * y4 / ((x4 - x1) * (x4 - x2) * (x4 - x3)); - }, - // domain minimum and maximum - () => gt.board.getBoundingBox()[0], () => gt.board.getBoundingBox()[2] - ], { - strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.color.underConstruction, - dash: solid ? 0 : 2 - }); + return gt.board.create( + 'curve', + [ + // x and y coordinate of point on curve + (x) => x, + (x) => { + const x1 = point1.X(), + x2 = point2.X(), + x3 = point3.X(), + x4 = point4.X(), + y1 = point1.Y(), + y2 = point2.Y(), + y3 = point3.Y(), + y4 = point4.Y(); + return ( + ((x - x2) * (x - x3) * (x - x4) * y1) / ((x1 - x2) * (x1 - x3) * (x1 - x4)) + + ((x - x1) * (x - x3) * (x - x4) * y2) / ((x2 - x1) * (x2 - x3) * (x2 - x4)) + + ((x - x1) * (x - x2) * (x - x4) * y3) / ((x3 - x1) * (x3 - x2) * (x3 - x4)) + + ((x - x1) * (x - x2) * (x - x3) * y4) / ((x4 - x1) * (x4 - x2) * (x4 - x3)) + ); + }, + // domain minimum and maximum + () => gt.board.getBoundingBox()[0], + () => gt.board.getBoundingBox()[2] + ], + { + strokeWidth: 2, + highlight: false, + strokeColor: color ? color : gt.color.underConstruction, + dash: solid ? 0 : 2 + } + ); }, // Prevent a point from being moved off the board by a drag. If a group of other points is provided, @@ -116,7 +155,7 @@ } point.setPosition(JXG.COORDS_BY_USER, [ - left_x < bbox[0] ? right_x : (preferLeft || right_x > bbox[2]) ? left_x : right_x, + left_x < bbox[0] ? right_x : preferLeft || right_x > bbox[2] ? left_x : right_x, y ]); } @@ -132,7 +171,12 @@ const point = gt.board.create( 'point', [gt.snapRound(x, gt.snapSizeX), gt.snapRound(y, gt.snapSizeY)], - { size: 2, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false } + { + size: 2, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, + withLabel: false + } ); point.setAttribute({ snapToGrid: true }); if (typeof grouped_points !== 'undefined' && grouped_points.length) { @@ -144,9 +188,11 @@ paired_point.on('drag', gt.graphObjectTypes.cubic.groupedPointDrag); } paired_point.grouped_points.push(point); - if (!paired_point.eventHandlers.drag || - paired_point.eventHandlers.drag.every((dragHandler) => - dragHandler.handler !== gt.graphObjectTypes.cubic.groupedPointDrag) + if ( + !paired_point.eventHandlers.drag || + paired_point.eventHandlers.drag.every( + (dragHandler) => dragHandler.handler !== gt.graphObjectTypes.cubic.groupedPointDrag + ) ) paired_point.on('drag', gt.graphObjectTypes.cubic.groupedPointDrag); }); @@ -191,8 +237,10 @@ this.phase2 = (coords) => { // Don't allow the second point to be created on the same // vertical line as the first point or off the board. - if (this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) || - !gt.boardHasPoint(coords[1], coords[2])) + if ( + this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) || + !gt.boardHasPoint(coords[1], coords[2]) + ) return; gt.board.off('up'); @@ -221,14 +269,18 @@ this.phase3 = (coords) => { // Don't allow the third point to be created on the same vertical line as the // first point, on the same vertical line as the second point, or off the board. - if (this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) || + if ( + this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) || this.point2.X() == gt.snapRound(coords[1], gt.snapSizeX) || - !gt.boardHasPoint(coords[1], coords[2])) + !gt.boardHasPoint(coords[1], coords[2]) + ) return; gt.board.off('up'); - this.point3 = gt.graphObjectTypes.cubic.createPoint(coords[1], coords[2], - [this.point1, this.point2]); + this.point3 = gt.graphObjectTypes.cubic.createPoint(coords[1], coords[2], [ + this.point1, + this.point2 + ]); this.point3.setAttribute({ fixed: true, highlight: false }); // Get a new x coordinate that is to the right, unless that is off the board. @@ -257,18 +309,28 @@ // Don't allow the fourth point to be created on the same vertical line as the first // point, on the same vertical line as the second point, on the same vertical line as // the third point, or off the board. - if (this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) || + if ( + this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) || this.point2.X() == gt.snapRound(coords[1], gt.snapSizeX) || this.point3.X() == gt.snapRound(coords[1], gt.snapSizeX) || - !gt.boardHasPoint(coords[1], coords[2])) + !gt.boardHasPoint(coords[1], coords[2]) + ) return; gt.board.off('up'); - const point4 = gt.graphObjectTypes.cubic.createPoint(coords[1], coords[2], - [this.point1, this.point2, this.point3]); - gt.selectedObj = new gt.graphObjectTypes.cubic(this.point1, this.point2, this.point3, point4, - gt.drawSolid); + const point4 = gt.graphObjectTypes.cubic.createPoint(coords[1], coords[2], [ + this.point1, + this.point2, + this.point3 + ]); + gt.selectedObj = new gt.graphObjectTypes.cubic( + this.point1, + this.point2, + this.point3, + point4, + gt.drawSolid + ); gt.selectedObj.focusPoint = point4; gt.graphedObjs.push(gt.selectedObj); delete this.point1; @@ -308,14 +370,17 @@ } else if (e instanceof JXG.Coords) { coords = e; this.hlObjs.hl_point?.setPosition(JXG.COORDS_BY_USER, [coords.usrCoords[1], coords.usrCoords[2]]); - } else - return false; + } else return false; if (!this.hlObjs.hl_point) { this.hlObjs.hl_point = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], { - size: 2, color: gt.color.underConstruction, snapToGrid: true, - snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - highlight: false, withLabel: false + size: 2, + color: gt.color.underConstruction, + snapToGrid: true, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, + highlight: false, + withLabel: false }); this.hlObjs.hl_point.rendNode.focus(); } @@ -327,7 +392,7 @@ if (this.point1) groupedPoints.push(this.point1); if (this.point2) groupedPoints.push(this.point2); if (this.point3) groupedPoints.push(this.point3); - gt.graphObjectTypes.cubic.adjustDragPosition(e, this.hlObjs.hl_point, groupedPoints) + gt.graphObjectTypes.cubic.adjustDragPosition(e, this.hlObjs.hl_point, groupedPoints); } if (this.point3 && !this.hlObjs.hl_cubic) { @@ -338,7 +403,12 @@ } this.hlObjs.hl_cubic = gt.graphObjectTypes.cubic.createCubic( - this.point1, this.point2, this.point3, this.hlObjs.hl_point, gt.drawSolid); + this.point1, + this.point2, + this.point3, + this.hlObjs.hl_point, + gt.drawSolid + ); } else if (this.point2 && !this.point3 && !this.hlObjs.hl_parabola) { // Delete the temporary highlight line if it exists. if (this.hlObjs.hl_line) { @@ -347,10 +417,16 @@ } this.hlObjs.hl_parabola = gt.graphObjectTypes.cubic.createParabola( - this.point1, this.point2, this.hlObjs.hl_point, gt.drawSolid); + this.point1, + this.point2, + this.hlObjs.hl_point, + gt.drawSolid + ); } else if (this.point1 && !this.point2 && !this.hlObjs.hl_line) { this.hlObjs.hl_line = gt.board.create('line', [this.point1, this.hlObjs.hl_point], { - fixed: true, strokeColor: gt.color.underConstruction, highlight: false, + fixed: true, + strokeColor: gt.color.underConstruction, + highlight: false, dash: gt.drawSolid ? 0 : 2 }); } @@ -362,7 +438,7 @@ deactivate(gt) { delete this.helpText; gt.board.off('up'); - ['point1', 'point2', 'point3'].forEach(function(point) { + ['point1', 'point2', 'point3'].forEach(function (point) { if (this[point]) gt.board.removeObject(this[point]); delete this[point]; }, this); diff --git a/htdocs/js/GraphTool/graphtool.js b/htdocs/js/GraphTool/graphtool.js index f8a5a5618c..b348721cad 100644 --- a/htdocs/js/GraphTool/graphtool.js +++ b/htdocs/js/GraphTool/graphtool.js @@ -9,6 +9,7 @@ window.graphTool = (containerId, options) => { const gt = {}; gt.graphContainer = document.getElementById(containerId); + if (!gt.graphContainer) return; if (gt.graphContainer.offsetWidth === 0) { setTimeout(() => window.graphTool(containerId, options), 100); return; @@ -56,25 +57,32 @@ window.graphTool = (containerId, options) => { gt.snapSizeY = options.snapSizeY ? options.snapSizeY : 1; gt.isStatic = options.isStatic ? true : false; if (!(options.availableTools instanceof Array)) - options.availableTools = - ['LineTool', 'CircleTool', 'VerticalParabolaTool', 'HorizontalParabolaTool', 'FillTool', 'SolidDashTool']; + options.availableTools = [ + 'LineTool', + 'CircleTool', + 'VerticalParabolaTool', + 'HorizontalParabolaTool', + 'FillTool', + 'SolidDashTool' + ]; // This is the icon used for the fill tool and fill graph object. - gt.fillIcon = (color) => "data:image/svg+xml," + + gt.fillIcon = (color) => + 'data:image/svg+xml,' + encodeURIComponent( "" + - "" + - "" + - "" + - "" + "viewBox='0 0 32 32' height='32px' width='32px'>" + + "" + + "" + + "" + + '' ); if ('htmlInputId' in options) gt.html_input = document.getElementById(options.htmlInputId); @@ -104,7 +112,7 @@ window.graphTool = (containerId, options) => { straightLast: false, fixed: true }, - grid: { gridX: gt.snapSizeX, gridY: gt.snapSizeY }, + grid: { majorStep: [gt.snapSizeX, gt.snapSizeY] }, keyboard: { enabled: true, dx: gt.snapSizeX, @@ -160,23 +168,23 @@ window.graphTool = (containerId, options) => { // Add an empty text that will hold the cursor position. gt.current_pos_text = options.numberLine ? gt.board.create( - 'text', - [ - () => gt.board.getBoundingBox()[0] + 10 / gt.board.unitX, - () => gt.board.getBoundingBox()[1] - 2 / gt.board.unitY, - () => '' - ], - { anchorX: 'left', anchorY: 'top', fixed: true, useMathJax: true } - ) + 'text', + [ + () => gt.board.getBoundingBox()[0] + 10 / gt.board.unitX, + () => gt.board.getBoundingBox()[1] - 2 / gt.board.unitY, + () => '' + ], + { anchorX: 'left', anchorY: 'top', fixed: true, useMathJax: true } + ) : gt.board.create( - 'text', - [ - () => gt.board.getBoundingBox()[2] - 5 / gt.board.unitX, - () => gt.board.getBoundingBox()[3] + 5 / gt.board.unitY, - () => '' - ], - { anchorX: 'right', anchorY: 'bottom', fixed: true, useMathJax: true } - ); + 'text', + [ + () => gt.board.getBoundingBox()[2] - 5 / gt.board.unitX, + () => gt.board.getBoundingBox()[3] + 5 / gt.board.unitY, + () => '' + ], + { anchorX: 'right', anchorY: 'bottom', fixed: true, useMathJax: true } + ); // Overwrite the popup infobox for points. gt.board.highlightInfobox = (_x, _y, el) => gt.board.highlightCustomInfobox('', el); @@ -220,7 +228,7 @@ window.graphTool = (containerId, options) => { gt.hasFocus = false; gt.objectFocusSet = false; - gt.board.containerObj.addEventListener('focus', () => gt.hasFocus = true); + gt.board.containerObj.addEventListener('focus', () => (gt.hasFocus = true)); gt.graphContainer.addEventListener('focusin', (e) => { e.preventDefault(); @@ -276,11 +284,13 @@ window.graphTool = (containerId, options) => { }) ); - if (e.relatedTarget !== gt.board.containerObj && + if ( + e.relatedTarget !== gt.board.containerObj && (gt.buttonBox.contains(e.relatedTarget) || (gt.board.containerObj.contains(e.relatedTarget) && - gt.graphedObjs.every( - (obj) => obj.definingPts.every((point) => point.rendNode !== e.relatedTarget)))) && + gt.graphedObjs.every((obj) => + obj.definingPts.every((point) => point.rendNode !== e.relatedTarget) + ))) && gt.graphedObjs.some((obj) => obj.definingPts.some((point) => point.rendNode === e.target)) ) { if (!gt.objectFocusSet) { @@ -304,8 +314,7 @@ window.graphTool = (containerId, options) => { gt.selectedObj.focusPoint = gt.selectedObj?.definingPts[0]; } gt.objectFocusSet = true; - } else - gt.objectFocusSet = false; + } else gt.objectFocusSet = false; gt.hasFocus = true; if (!gt.activeTool) gt.selectTool.activate(); @@ -396,11 +405,15 @@ window.graphTool = (containerId, options) => { setTimeout(resize, 1000); return; } - if (gt.board.canvasWidth != gt.board.containerObj.offsetWidth - 2 || - gt.board.canvasHeight != gt.board.containerObj.offsetHeight - 2) - { + if ( + gt.board.canvasWidth != gt.board.containerObj.offsetWidth - 2 || + gt.board.canvasHeight != gt.board.containerObj.offsetHeight - 2 + ) { gt.board.resizeContainer( - gt.board.containerObj.offsetWidth - 2, gt.board.containerObj.offsetHeight - 2, true); + gt.board.containerObj.offsetWidth - 2, + gt.board.containerObj.offsetHeight - 2, + true + ); gt.graphedObjs.forEach((object) => object.onResize()); gt.staticObjs.forEach((object) => object.onResize()); } @@ -419,16 +432,22 @@ window.graphTool = (containerId, options) => { gt.snapRound = (x, snap, precision = 10 ** 5) => Math.round(Math.round(x / snap) * snap * precision) / precision; // Convert a decimal number into a fraction or mixed number with denominator at most 10 ** 8. - gt.toLatexFrac = (x, mixed = false, snapSize = gt.snapSizeX) => { - const sign = x ? Math.abs(x) / x : 1, int = mixed ? Math.trunc(sign * x) : 0; - let a = 0, step = sign * x - int, h0 = 1, h1 = a, k0 = 0, k1 = 1; + gt.toLatexFrac = (x, mixed = false, _snapSize = gt.snapSizeX) => { + const sign = x ? Math.abs(x) / x : 1, + int = mixed ? Math.trunc(sign * x) : 0; + let a = 0, + step = sign * x - int, + h0 = 1, + h1 = a, + k0 = 0, + k1 = 1; while (Math.abs(step - a) >= JXG.Math.eps) { step = 1 / (step - a); a = Math.trunc(step); - const [ newh, newk ] = [ a * h1 + h0, a * k1 + k0 ]; + const [newh, newk] = [a * h1 + h0, a * k1 + k0]; if (newk > 10 ** 8) break; - [ h0, h1, k0, k1 ] = [ h1, newh, k1, newk ]; + [h0, h1, k0, k1] = [h1, newh, k1, newk]; } if (k1 === 1) return mixed ? `${sign * int}` : `${sign * h1}`; @@ -437,9 +456,8 @@ window.graphTool = (containerId, options) => { }; gt.setTextCoords = options.showCoordinateHints - ? ( - options.numberLine - ? (x) => { + ? options.numberLine + ? (x) => { const bbox = gt.board.getBoundingBox(); const xSnap = gt.snapRound(x, gt.snapSizeX); if (xSnap <= bbox[0]) gt.current_pos_text.setText(() => '\\(-\\infty\\)'); @@ -451,67 +469,68 @@ window.graphTool = (containerId, options) => { options.coordinateHintsTypeX === 'mixed' ); gt.current_pos_text.setText(() => `\\(${text}\\)`); - } else - gt.current_pos_text.setText(() => `\\(${xSnap}\\)`); + } else gt.current_pos_text.setText(() => `\\(${xSnap}\\)`); } } - : (x, y) => { + : (x, y) => { const xText = options.coordinateHintsTypeX === 'mixed' || options.coordinateHintsTypeX === 'fraction' - ? gt.toLatexFrac( - gt.snapRound(x, gt.snapSizeX, 10 ** 13), - options.coordinateHintsTypeX === 'mixed' - ) - : gt.snapRound(x, gt.snapSizeX); + ? gt.toLatexFrac( + gt.snapRound(x, gt.snapSizeX, 10 ** 13), + options.coordinateHintsTypeX === 'mixed' + ) + : gt.snapRound(x, gt.snapSizeX); const yText = options.coordinateHintsTypeY === 'mixed' || options.coordinateHintsTypeY === 'fraction' - ? gt.toLatexFrac( - gt.snapRound(y, gt.snapSizeY, 10 ** 13), - options.coordinateHintsTypeY === 'mixed', - gt.snapSizeY - ) - : gt.snapRound(y, gt.snapSizeY); + ? gt.toLatexFrac( + gt.snapRound(y, gt.snapSizeY, 10 ** 13), + options.coordinateHintsTypeY === 'mixed', + gt.snapSizeY + ) + : gt.snapRound(y, gt.snapSizeY); gt.current_pos_text.setText(() => `\\(\\left(${xText}, ${yText}\\right)\\)`); } - ) : () => {}; gt.updateText = () => { gt.html_input.value = gt.graphedObjs.reduce( - (val, obj) => `${val}${val.length ? ',' : ''}{${obj.stringify()}}`, '' + (val, obj) => `${val}${val.length ? ',' : ''}{${obj.stringify()}}`, + '' ); }; - gt.setMessageContent = (newContent, confirmation = false) => new Promise((resolve, reject) => { - if (gt.confirmationActive) return resolve(); - gt.confirmationActive = confirmation - - clearInterval(gt.setMessageContent.IntervalId); - for (const message of gt.messageBox.querySelectorAll('.gt-message-content')) - message.classList.remove('gt-message-fade'); - gt.setMessageContent.IntervalId = setTimeout(() => { - requestAnimationFrame(() => { - while (gt.messageBox.firstChild) gt.messageBox.firstChild.remove(); - if (newContent) { - gt.messageBox.append(newContent); - newContent.classList.add('gt-message-content'); - setTimeout(() => newContent.classList.add('gt-message-content', 'gt-message-fade')); - - if (window.MathJax) { - MathJax.startup.promise = - MathJax.startup.promise.then(() => MathJax.typesetPromise([ newContent ])); + gt.setMessageContent = (newContent, confirmation = false) => + new Promise((resolve, _reject) => { + if (gt.confirmationActive) return resolve(); + gt.confirmationActive = confirmation; + + clearInterval(gt.setMessageContent.IntervalId); + for (const message of gt.messageBox.querySelectorAll('.gt-message-content')) + message.classList.remove('gt-message-fade'); + gt.setMessageContent.IntervalId = setTimeout(() => { + requestAnimationFrame(() => { + while (gt.messageBox.firstChild) gt.messageBox.firstChild.remove(); + if (newContent) { + gt.messageBox.append(newContent); + newContent.classList.add('gt-message-content'); + setTimeout(() => newContent.classList.add('gt-message-content', 'gt-message-fade')); + + if (window.MathJax) { + MathJax.startup.promise = MathJax.startup.promise.then(() => + MathJax.typesetPromise([newContent]) + ); + } } - } - resolve(); - }); - }, 100); - }); + resolve(); + }); + }, 100); + }); gt.setMessageText = (content) => { if (gt.confirmationActive || !gt.helpEnabled) return; - const newMessage = (content instanceof Array ? content.join(' ') : content); + const newMessage = content instanceof Array ? content.join(' ') : content; if (newMessage) { const par = document.createElement('p'); par.textContent = newMessage; @@ -519,18 +538,17 @@ window.graphTool = (containerId, options) => { } else { gt.setMessageContent(); } - } + }; gt.updateHelp = () => { if (gt.confirmationActive || !gt.helpEnabled) return; gt.setMessageText( - gt.tools.map((tool) => typeof tool.helpText === 'function' - ? tool.helpText() - : (tool.helpText || '')) - .filter((helpText) => !!helpText) + gt.tools + .map((tool) => (typeof tool.helpText === 'function' ? tool.helpText() : tool.helpText || '')) + .filter((helpText) => !!helpText) ); - } + }; gt.updateUI = () => { gt.deleteButton.disabled = !gt.selectedObj; @@ -559,7 +577,8 @@ window.graphTool = (containerId, options) => { // Use this instead of gt.board.hasPoint. That method uses strict inequality. // Using inequality with equality allows points on the edge of the board. gt.boardHasPoint = (x, y) => { - let px = x, py = y; + let px = x, + py = y; const bbox = gt.board.getBoundingBox(); if (JXG.exists(x) && JXG.isArray(x.usrCoords)) { @@ -567,12 +586,7 @@ window.graphTool = (containerId, options) => { py = x.usrCoords[2]; } - return JXG.isNumber(px) && - JXG.isNumber(py) && - bbox[0] <= px && - px <= bbox[2] && - bbox[1] >= py && - py >= bbox[3]; + return JXG.isNumber(px) && JXG.isNumber(py) && bbox[0] <= px && px <= bbox[2] && bbox[1] >= py && py >= bbox[3]; }; gt.pointRegexp = /\( *(-?[0-9]*(?:\.[0-9]*)?), *(-?[0-9]*(?:\.[0-9]*)?) *\)/g; @@ -582,9 +596,10 @@ window.graphTool = (containerId, options) => { // called, the point has already been moved by JSXGraph. This prevents lines and circles from being made // degenerate. gt.adjustDragPosition = (e, point, pairedPoint) => { - if ((point.X() == pairedPoint?.X() && point.Y() == pairedPoint?.Y()) || - !gt.boardHasPoint(point.X(), point.Y())) - { + if ( + (point.X() == pairedPoint?.X() && point.Y() == pairedPoint?.Y()) || + !gt.boardHasPoint(point.X(), point.Y()) + ) { const bbox = gt.board.getBoundingBox(); // Clamp the coordinates to the board. @@ -599,9 +614,8 @@ window.graphTool = (containerId, options) => { const coords = gt.getMouseCoords(e); const x_trans = coords.usrCoords[1] - pairedPoint.X(), y_trans = coords.usrCoords[2] - pairedPoint.Y(); - [ xDir, yDir ] = Math.abs(x_trans) < Math.abs(y_trans) - ? [ 0, y_trans < 0 ? -1 : 1 ] - : [ x_trans < 0 ? -1 : 1, 0 ]; + [xDir, yDir] = + Math.abs(x_trans) < Math.abs(y_trans) ? [0, y_trans < 0 ? -1 : 1] : [x_trans < 0 ? -1 : 1, 0]; } else if (e.type === 'keydown') { xDir = e.key === 'ArrowLeft' ? -1 : e.key === 'ArrowRight' ? 1 : 0; yDir = e.key === 'ArrowUp' ? 1 : e.key === 'ArrowDown' ? -1 : 0; @@ -632,10 +646,7 @@ window.graphTool = (containerId, options) => { // horizontal or vertical line as its paired point by a drag. Note that when this method is called, the point has // already been moved by JSXGraph. This prevents parabolas from being made degenerate. gt.adjustDragPositionRestricted = (e, point, pairedPoint) => { - if (point.X() == pairedPoint?.X() || - point.Y() == pairedPoint?.Y() || - !gt.boardHasPoint(point.X(), point.Y())) - { + if (point.X() == pairedPoint?.X() || point.Y() == pairedPoint?.Y() || !gt.boardHasPoint(point.X(), point.Y())) { const bbox = gt.board.getBoundingBox(); // Clamp the coordinates to the board. @@ -678,23 +689,26 @@ window.graphTool = (containerId, options) => { }; gt.createPoint = (x, y, paired_point, restrict) => { - const point = gt.board.create('point', [gt.snapRound(x, gt.snapSizeX), gt.snapRound(y, gt.snapSizeY)], - { snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, ...gt.definingPointAttributes }); + const point = gt.board.create('point', [gt.snapRound(x, gt.snapSizeX), gt.snapRound(y, gt.snapSizeY)], { + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, + ...gt.definingPointAttributes + }); point.setAttribute({ snapToGrid: true }); point.on('down', () => (gt.board.containerObj.style.cursor = 'none')); point.on('up', () => (gt.board.containerObj.style.cursor = 'auto')); if (typeof paired_point !== 'undefined') { point.paired_point = paired_point; paired_point.paired_point = point; - paired_point.on('drag', + paired_point.on( + 'drag', restrict ? (e) => gt.pairedPointDragRestricted(e, paired_point) : (e) => gt.pairedPointDrag(e, paired_point) ); - point.on('drag', - restrict - ? (e) => gt.pairedPointDragRestricted(e, point) - : (e) => gt.pairedPointDrag(e, point) + point.on( + 'drag', + restrict ? (e) => gt.pairedPointDragRestricted(e, point) : (e) => gt.pairedPointDrag(e, point) ); } return point; @@ -749,19 +763,31 @@ window.graphTool = (containerId, options) => { update() {} - fillCmp(/* point */) { return 1; } + fillCmp(/* point */) { + return 1; + } remove() { this.definingPts.forEach((point) => gt.board.removeObject(point)); gt.board.removeObject(this.baseObj); } - setSolid(solid) { this.baseObj.setAttribute({ dash: solid ? 0 : 2 }); } + setSolid(solid) { + this.baseObj.setAttribute({ dash: solid ? 0 : 2 }); + } - stringify() { return ''; } - id() { return this.baseObj.id; } - on(e, handler, context) { this.baseObj.on(e, handler, context); } - off(e, handler) { this.baseObj.off(e, handler); } + stringify() { + return ''; + } + id() { + return this.baseObj.id; + } + on(e, handler, context) { + this.baseObj.on(e, handler, context); + } + off(e, handler) { + this.baseObj.off(e, handler); + } onResize() {} updateTextCoords(coords) { @@ -795,9 +821,14 @@ window.graphTool = (containerId, options) => { static strId = 'line'; constructor(point1, point2, solid) { - super(gt.board.create('line', [point1, point2], - { fixed: true, highlight: false, strokeColor: gt.color.curve, dash: solid ? 0 : 2 } - )); + super( + gt.board.create('line', [point1, point2], { + fixed: true, + highlight: false, + strokeColor: gt.color.curve, + dash: solid ? 0 : 2 + }) + ); this.definingPts.push(point1, point2); this.focusPoint = point1; } @@ -835,9 +866,14 @@ window.graphTool = (containerId, options) => { static strId = 'circle'; constructor(center, point, solid) { - super(gt.board.create('circle', [center, point], - { fixed: true, highlight: false, strokeColor: gt.color.curve, dash: solid ? 0 : 2 } - )); + super( + gt.board.create('circle', [center, point], { + fixed: true, + highlight: false, + strokeColor: gt.color.curve, + dash: solid ? 0 : 2 + }) + ); this.definingPts.push(center, point); this.focusPoint = center; @@ -858,9 +894,10 @@ window.graphTool = (containerId, options) => { } fillCmp(point) { - return gt.sign(this.baseObj.stdform[3] * - (point[1] * point[1] + point[2] * point[2]) - + JXG.Math.innerProduct(point, this.baseObj.stdform)); + return gt.sign( + this.baseObj.stdform[3] * (point[1] * point[1] + point[2] * point[2]) + + JXG.Math.innerProduct(point, this.baseObj.stdform) + ); } static restore(string) { @@ -887,24 +924,42 @@ window.graphTool = (containerId, options) => { : (point.X() - vertex.X()) / Math.pow(point.Y() - vertex.Y(), 2); const createParabola = (vertex, point, vertical, solid, color) => { - if (vertical) return gt.board.create('curve', [ - // x and y coordinates of point on curve - (x) => x, (x) => aVal(vertex, point, vertical) * Math.pow(x - vertex.X(), 2) + vertex.Y(), - // domain minimum and maximum - () => gt.board.getBoundingBox()[0], () => gt.board.getBoundingBox()[2] - ], { - strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.color.underConstruction, - dash: solid ? 0 : 2 - }); - else return gt.board.create('curve', [ - // x and y coordinate of point on curve - (x) => aVal(vertex, point, vertical) * Math.pow(x - vertex.Y(), 2) + vertex.X(), (x) => x, - // domain minimum and maximum - () => gt.board.getBoundingBox()[3], () => gt.board.getBoundingBox()[1] - ], { - strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.color.underConstruction, - dash: solid ? 0 : 2 - }); + if (vertical) + return gt.board.create( + 'curve', + [ + // x and y coordinates of point on curve + (x) => x, + (x) => aVal(vertex, point, vertical) * Math.pow(x - vertex.X(), 2) + vertex.Y(), + // domain minimum and maximum + () => gt.board.getBoundingBox()[0], + () => gt.board.getBoundingBox()[2] + ], + { + strokeWidth: 2, + highlight: false, + strokeColor: color ? color : gt.color.underConstruction, + dash: solid ? 0 : 2 + } + ); + else + return gt.board.create( + 'curve', + [ + // x and y coordinate of point on curve + (x) => aVal(vertex, point, vertical) * Math.pow(x - vertex.Y(), 2) + vertex.X(), + (x) => x, + // domain minimum and maximum + () => gt.board.getBoundingBox()[3], + () => gt.board.getBoundingBox()[1] + ], + { + strokeWidth: 2, + highlight: false, + strokeColor: color ? color : gt.color.underConstruction, + dash: solid ? 0 : 2 + } + ); }; class Parabola extends GraphObject { @@ -957,7 +1012,11 @@ window.graphTool = (containerId, options) => { // Make the point invisible, but not with the jsxgraph visible attribute. The icon will be shown instead. point.setAttribute({ - strokeOpacity: 0, highlightStrokeOpacity: 0, fillOpacity: 0, highlightFillOpacity: 0, fixed: gt.isStatic + strokeOpacity: 0, + highlightStrokeOpacity: 0, + fillOpacity: 0, + highlightFillOpacity: 0, + fixed: gt.isStatic }); this.definingPts.push(point); this.focusPoint = point; @@ -1044,8 +1103,11 @@ window.graphTool = (containerId, options) => { }; const isFillPixel = (x, y) => { - const curPixel = [1.0, (x - gt.board.origin.scrCoords[1]) / gt.board.unitX, - (gt.board.origin.scrCoords[2] - y) / gt.board.unitY]; + const curPixel = [ + 1.0, + (x - gt.board.origin.scrCoords[1]) / gt.board.unitX, + (gt.board.origin.scrCoords[2] - y) / gt.board.unitY + ]; for (let i = 0; i < allObjects.length; ++i) { if (allObjects[i].fillCmp(curPixel) != a_vals[i]) return false; } @@ -1063,11 +1125,15 @@ window.graphTool = (containerId, options) => { canvas.remove(); const boundingBox = gt.board.getBoundingBox(); - this.fillObj = gt.board.create('image', [ - dataURL, - [boundingBox[0], boundingBox[3]], - [boundingBox[2] - boundingBox[0], boundingBox[1] - boundingBox[3]] - ], { withLabel: false, highlight: false, fixed: true, layer: 0 }); + this.fillObj = gt.board.create( + 'image', + [ + dataURL, + [boundingBox[0], boundingBox[3]], + [boundingBox[2] - boundingBox[0], boundingBox[1] - boundingBox[3]] + ], + { withLabel: false, highlight: false, fixed: true, layer: 0 } + ); }; if (!('isStatic' in this) || (gt.isStatic && !gt.graphingAnswers) || this.isAnswer) { @@ -1109,8 +1175,12 @@ window.graphTool = (containerId, options) => { if ('customGraphObjects' in options) { Object.keys(options.customGraphObjects).forEach((name) => { const graphObject = options.customGraphObjects[name]; - const parentObject = 'parent' in graphObject ? - (graphObject.parent ? gt.graphObjectTypes[graphObject.parent] : null) : GraphObject; + const parentObject = + 'parent' in graphObject + ? graphObject.parent + ? gt.graphObjectTypes[graphObject.parent] + : null + : GraphObject; const customGraphObject = class extends parentObject { static strId = name; @@ -1209,7 +1279,7 @@ window.graphTool = (containerId, options) => { // These are methods that must be called with a class instance (as in this.method(...args)). if ('classMethods' in graphObject) { for (const method of Object.keys(graphObject.classMethods)) { - customGraphObject.prototype[method] = function(...args) { + customGraphObject.prototype[method] = function (...args) { return graphObject.classMethods[method].call(this, gt, ...args); }; } @@ -1218,7 +1288,7 @@ window.graphTool = (containerId, options) => { // These are static class methods. if ('helperMethods' in graphObject) { Object.keys(graphObject.helperMethods).forEach((method) => { - customGraphObject[method] = function(...args) { + customGraphObject[method] = function (...args) { return graphObject.helperMethods[method].apply(this, [gt, ...args]); }; }); @@ -1266,7 +1336,9 @@ window.graphTool = (containerId, options) => { handleKeyEvent(/* e: KeyboardEvent */) {} - updateHighlights(/* e: MouseEvent | KeyboardEvent | JXG.Coords | undefined */) { return false; } + updateHighlights(/* e: MouseEvent | KeyboardEvent | JXG.Coords | undefined */) { + return false; + } removeHighlights() { for (const obj in this.hlObjs) { @@ -1291,7 +1363,7 @@ window.graphTool = (containerId, options) => { if (gt.activeTool === this) { return gt.graphedObjs.length ? 'Make changes to the selected object, or select a new tool (Shift-N) to graph another object.' - : 'Select a new tool (Shift-N) to graph an object.' + : 'Select a new tool (Shift-N) to graph an object.'; } return ''; } @@ -1380,7 +1452,8 @@ window.graphTool = (containerId, options) => { // focus the previous or next object when shift-tab or tab is pressed. if (pIndex === 0 || pIndex === a.length - 1) { point.focusOutHandler = (e) => { - if (e.key !== 'Tab' || + if ( + e.key !== 'Tab' || (index === 0 && e.shiftKey) || (index === gt.graphedObjs.length - 1 && !e.shiftKey) || (a.length > 1 && @@ -1421,11 +1494,13 @@ window.graphTool = (containerId, options) => { obj.off('down', obj.selectionChangedHandler); delete obj.selectionChangedHandler; - obj.definingPts.filter((_p, i, a) => i === 0 || i === a.length - 1).forEach((point) => { - point.rendNode.removeEventListener('keydown', point.focusOutHandler); - point.rendNode.removeEventListener('focusin', point.focusInHandler); - delete point.focusOutHandler; - }); + obj.definingPts + .filter((_p, i, a) => i === 0 || i === a.length - 1) + .forEach((point) => { + point.rendNode.removeEventListener('keydown', point.focusOutHandler); + point.rendNode.removeEventListener('focusin', point.focusInHandler); + delete point.focusOutHandler; + }); } gt.selectedObj?.blur(); @@ -1468,13 +1543,17 @@ window.graphTool = (containerId, options) => { } else if (e instanceof JXG.Coords) { coords = e; this.hlObjs.hl_point?.setPosition(JXG.COORDS_BY_USER, [coords.usrCoords[1], coords.usrCoords[2]]); - } else - return false; + } else return false; if (!this.hlObjs.hl_point) { this.hlObjs.hl_point = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], { - size: 2, color: gt.color.underConstruction, snapToGrid: true, highlight: false, - snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false + size: 2, + color: gt.color.underConstruction, + snapToGrid: true, + highlight: false, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, + withLabel: false }); this.hlObjs.hl_point.rendNode.focus(); } @@ -1484,7 +1563,9 @@ window.graphTool = (containerId, options) => { if (this.point1 && !this.hlObjs.hl_line) { this.hlObjs.hl_line = gt.board.create('line', [this.point1, this.hlObjs.hl_point], { - fixed: true, strokeColor: gt.color.underConstruction, highlight: false, + fixed: true, + strokeColor: gt.color.underConstruction, + highlight: false, dash: gt.drawSolid ? 0 : 2 }); } @@ -1528,8 +1609,12 @@ window.graphTool = (containerId, options) => { gt.board.off('up'); this.point1 = gt.board.create('point', [coords[1], coords[2]], { - size: 2, withLabel: false, highlight: false, - snapToGrid: true, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY + size: 2, + withLabel: false, + highlight: false, + snapToGrid: true, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY }); this.point1.setAttribute({ fixed: true }); @@ -1552,16 +1637,18 @@ window.graphTool = (containerId, options) => { // and is not the same as the first point, then finalize the line. phase2(coords) { // Don't allow the second point to be created on top of the first or off the board - if ((this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) && - this.point1.Y() == gt.snapRound(coords[2], gt.snapSizeY)) || - !gt.boardHasPoint(coords[1], coords[2])) + if ( + (this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) && + this.point1.Y() == gt.snapRound(coords[2], gt.snapSizeY)) || + !gt.boardHasPoint(coords[1], coords[2]) + ) return; gt.board.off('up'); this.point1.setAttribute(gt.definingPointAttributes); - this.point1.on('down', () => gt.board.containerObj.style.cursor = 'none'); - this.point1.on('up', () => gt.board.containerObj.style.cursor = 'auto'); + this.point1.on('down', () => (gt.board.containerObj.style.cursor = 'none')); + this.point1.on('up', () => (gt.board.containerObj.style.cursor = 'auto')); const point2 = gt.createPoint(coords[1], coords[2], this.point1); gt.selectedObj = new gt.graphObjectTypes.line(this.point1, point2, gt.drawSolid); @@ -1605,13 +1692,17 @@ window.graphTool = (containerId, options) => { } else if (e instanceof JXG.Coords) { coords = e; this.hlObjs.hl_point?.setPosition(JXG.COORDS_BY_USER, [coords.usrCoords[1], coords.usrCoords[2]]); - } else - return false; + } else return false; if (!this.hlObjs.hl_point) { this.hlObjs.hl_point = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], { - size: 2, color: gt.color.underConstruction, snapToGrid: true, highlight: false, - snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false + size: 2, + color: gt.color.underConstruction, + snapToGrid: true, + highlight: false, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, + withLabel: false }); this.hlObjs.hl_point.rendNode.focus(); } @@ -1621,7 +1712,9 @@ window.graphTool = (containerId, options) => { if (this.center && !this.hlObjs.hl_circle) { this.hlObjs.hl_circle = gt.board.create('circle', [this.center, this.hlObjs.hl_point], { - fixed: true, strokeColor: gt.color.underConstruction, highlight: false, + fixed: true, + strokeColor: gt.color.underConstruction, + highlight: false, dash: gt.drawSolid ? 0 : 2 }); } @@ -1661,8 +1754,12 @@ window.graphTool = (containerId, options) => { gt.board.off('up'); this.center = gt.board.create('point', [coords[1], coords[2]], { - size: 2, withLabel: false, highlight: false, - snapToGrid: true, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY + size: 2, + withLabel: false, + highlight: false, + snapToGrid: true, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY }); this.center.setAttribute({ fixed: true }); @@ -1685,16 +1782,18 @@ window.graphTool = (containerId, options) => { // and is not the same as the center, then finalize the circle. phase2(coords) { // Don't allow the second point to be created on top of the center or off the board - if ((this.center.X() == gt.snapRound(coords[1], gt.snapSizeX) && - this.center.Y() == gt.snapRound(coords[2], gt.snapSizeY)) || - !gt.boardHasPoint(coords[1], coords[2])) + if ( + (this.center.X() == gt.snapRound(coords[1], gt.snapSizeX) && + this.center.Y() == gt.snapRound(coords[2], gt.snapSizeY)) || + !gt.boardHasPoint(coords[1], coords[2]) + ) return; gt.board.off('up'); this.center.setAttribute(gt.definingPointAttributes); - this.center.on('down', () => gt.board.containerObj.style.cursor = 'none'); - this.center.on('up', () => gt.board.containerObj.style.cursor = 'auto'); + this.center.on('down', () => (gt.board.containerObj.style.cursor = 'none')); + this.center.on('up', () => (gt.board.containerObj.style.cursor = 'auto')); const point = gt.createPoint(coords[1], coords[2], this.center); gt.selectedObj = new gt.graphObjectTypes.circle(this.center, point, gt.drawSolid); @@ -1709,13 +1808,15 @@ window.graphTool = (containerId, options) => { // Parabola graphing tool class ParabolaTool extends GenericTool { constructor(container, vertical, iconName, tooltip) { - super(container, + super( + container, iconName ? iconName : vertical ? 'vertical-parabola' : 'horizontal-parabola', tooltip ? tooltip : vertical ? 'Vertical Parabola Tool: Graph a vertical parabola.' - : 'Horizontal Parabola Tool: Graph an horizontal parabola.'); + : 'Horizontal Parabola Tool: Graph an horizontal parabola.' + ); this.vertical = vertical; this.supportsSolidDash = true; } @@ -1745,14 +1846,17 @@ window.graphTool = (containerId, options) => { } else if (e instanceof JXG.Coords) { coords = e; this.hlObjs.hl_point?.setPosition(JXG.COORDS_BY_USER, [coords.usrCoords[1], coords.usrCoords[2]]); - } else - return false; + } else return false; if (!this.hlObjs.hl_point) { this.hlObjs.hl_point = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], { - size: 2, color: gt.color.underConstruction, snapToGrid: true, - snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - highlight: false, withLabel: false + size: 2, + color: gt.color.underConstruction, + snapToGrid: true, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, + highlight: false, + withLabel: false }); this.hlObjs.hl_point.rendNode.focus(); } @@ -1762,8 +1866,13 @@ window.graphTool = (containerId, options) => { if (e instanceof Event) gt.adjustDragPositionRestricted(e, this.hlObjs.hl_point, this.vertex); if (this.vertex && !this.hlObjs.hl_parabola) { - this.hlObjs.hl_parabola = createParabola(this.vertex, this.hlObjs.hl_point, this.vertical, - gt.drawSolid, gt.color.underConstruction); + this.hlObjs.hl_parabola = createParabola( + this.vertex, + this.hlObjs.hl_point, + this.vertical, + gt.drawSolid, + gt.color.underConstruction + ); } gt.setTextCoords(this.hlObjs.hl_point.X(), this.hlObjs.hl_point.Y()); @@ -1800,8 +1909,12 @@ window.graphTool = (containerId, options) => { gt.board.off('up'); this.vertex = gt.board.create('point', [coords[1], coords[2]], { - size: 2, withLabel: false, highlight: false, - snapToGrid: true, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY + size: 2, + withLabel: false, + highlight: false, + snapToGrid: true, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY }); this.vertex.setAttribute({ fixed: true }); @@ -1828,16 +1941,18 @@ window.graphTool = (containerId, options) => { phase2(coords) { // Don't allow the second point to be created on the same // horizontal or vertical line as the vertex or off the board. - if (this.vertex.X() == gt.snapRound(coords[1], gt.snapSizeX) || + if ( + this.vertex.X() == gt.snapRound(coords[1], gt.snapSizeX) || this.vertex.Y() == gt.snapRound(coords[2], gt.snapSizeY) || - !gt.boardHasPoint(coords[1], coords[2])) + !gt.boardHasPoint(coords[1], coords[2]) + ) return; gt.board.off('up'); this.vertex.setAttribute(gt.definingPointAttributes); - this.vertex.on('down', () => gt.board.containerObj.style.cursor = 'none'); - this.vertex.on('up', () => gt.board.containerObj.style.cursor = 'auto'); + this.vertex.on('down', () => (gt.board.containerObj.style.cursor = 'none')); + this.vertex.on('up', () => (gt.board.containerObj.style.cursor = 'auto')); const point = gt.createPoint(coords[1], coords[2], this.vertex, true); gt.selectedObj = new gt.graphObjectTypes.parabola(this.vertex, point, this.vertical, gt.drawSolid); @@ -1864,8 +1979,11 @@ window.graphTool = (containerId, options) => { // Fill tool class FillTool extends GenericTool { constructor(container, iconName, tooltip) { - super(container, iconName ? iconName : 'fill', - tooltip ? tooltip : 'Region Shading Tool: Shade a region in the graph.'); + super( + container, + iconName ? iconName : 'fill', + tooltip ? tooltip : 'Region Shading Tool: Shade a region in the graph.' + ); } handleKeyEvent(e) { @@ -1891,24 +2009,35 @@ window.graphTool = (containerId, options) => { } else if (e instanceof JXG.Coords) { coords = e; this.hlObjs.hl_point?.setPosition(JXG.COORDS_BY_USER, [coords.usrCoords[1], coords.usrCoords[2]]); - } else - return false; + } else return false; if (!this.hlObjs.hl_point) { this.hlObjs.hl_point = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], { - size: 2, strokeColor: 'transparent', fillColor: 'transparent', strokeOpacity: 0, fillOpacity: 0, - highlight: false, withLabel: false, snapToGrid: true, - snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY + size: 2, + strokeColor: 'transparent', + fillColor: 'transparent', + strokeOpacity: 0, + fillOpacity: 0, + highlight: false, + withLabel: false, + snapToGrid: true, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY }); this.hlObjs.hl_point.rendNode.classList.add('hidden-fill-point'); - this.hlObjs.hl_icon = gt.board.create('image', [ - gt.fillIcon(gt.color.fill), [ - () => this.hlObjs.hl_point.X() - 12 / gt.board.unitX, - () => this.hlObjs.hl_point.Y() - 12 / gt.board.unitY + this.hlObjs.hl_icon = gt.board.create( + 'image', + [ + gt.fillIcon(gt.color.fill), + [ + () => this.hlObjs.hl_point.X() - 12 / gt.board.unitX, + () => this.hlObjs.hl_point.Y() - 12 / gt.board.unitY + ], + [() => 24 / gt.board.unitX, () => 24 / gt.board.unitY] ], - [() => 24 / gt.board.unitX, () => 24 / gt.board.unitY] - ], { withLabel: false, highlight: false, fixed: true, layer: 8 }); + { withLabel: false, highlight: false, fixed: true, layer: 8 } + ); this.hlObjs.hl_point.rendNode.focus(); } @@ -2029,10 +2158,11 @@ window.graphTool = (containerId, options) => { return (gt.selectedObj && gt.selectedObj.supportsSolidDash) || (gt.activeTool && gt.activeTool.supportsSolidDash) ? 'Use the ' + - '\\(\\rule[3px]{34px}{2px}\\) or ' + - '\\(\\rule[3px]{3px}{2px}' + '\\hspace{4px}\\rule[3px]{4px}{2px}'.repeat(3) + - '\\hspace{4px}\\rule[3px]{3px}{2px}\\)' + - ' button or type s or d to make the selected object solid or dashed.' + '\\(\\rule[3px]{34px}{2px}\\) or ' + + '\\(\\rule[3px]{3px}{2px}' + + '\\hspace{4px}\\rule[3px]{4px}{2px}'.repeat(3) + + '\\hspace{4px}\\rule[3px]{3px}{2px}\\)' + + ' button or type s or d to make the selected object solid or dashed.' : ''; } } @@ -2062,8 +2192,8 @@ window.graphTool = (containerId, options) => { if ('customTools' in options) { Object.keys(options.customTools).forEach((tool) => { const toolObject = options.customTools[tool]; - const parentTool = 'parent' in toolObject ? - (toolObject.parent ? gt.toolTypes[toolObject.parent] : null) : GenericTool; + const parentTool = + 'parent' in toolObject ? (toolObject.parent ? gt.toolTypes[toolObject.parent] : null) : GenericTool; const customTool = class extends parentTool { constructor(container) { if (parentTool) { @@ -2080,7 +2210,7 @@ window.graphTool = (containerId, options) => { handleKeyEvent(e) { if ('handleKeyEvent' in toolObject) toolObject.handleKeyEvent.call(this, gt, e); - if (parentTool) super.handleKeyEvent(); + if (parentTool) super.handleKeyEvent(e); } activate() { @@ -2108,7 +2238,7 @@ window.graphTool = (containerId, options) => { // These are methods that must be called with a class instance (as in this.method(...args)). if ('classMethods' in toolObject) { for (const method of Object.keys(toolObject.classMethods)) { - customTool.prototype[method] = function(...args) { + customTool.prototype[method] = function (...args) { return toolObject.classMethods[method].call(this, gt, ...args); }; } @@ -2117,7 +2247,7 @@ window.graphTool = (containerId, options) => { // These are static class methods. if ('helperMethods' in toolObject) { Object.keys(toolObject.helperMethods).forEach((method) => { - customTool[method] = function(...args) { + customTool[method] = function (...args) { return toolObject.helperMethods[method].apply(this, [gt, ...args]); }; }); @@ -2127,7 +2257,7 @@ window.graphTool = (containerId, options) => { }); } - gt.tools = [ gt.selectTool ]; + gt.tools = [gt.selectTool]; for (const tool of options.availableTools) { if (tool in gt.toolTypes) gt.tools.push(new gt.toolTypes[tool](gt.buttonBox)); else console.log(`Unknown tool: ${tool}`); @@ -2158,7 +2288,7 @@ window.graphTool = (containerId, options) => { gt.confirmationActive = false; gt.updateHelp(); - } + }; overlay.addEventListener('pointerdown', gt.confirm.dispose, { signal: controller.signal }); const questionElt = document.createElement('div'); @@ -2171,8 +2301,14 @@ window.graphTool = (containerId, options) => { yesButton.type = 'button'; yesButton.classList.add('gt-button', 'gt-text-button', 'gt-confirm-button'); yesButton.textContent = 'Yes'; - yesButton.addEventListener('click', - (e) => { yesAction(); gt.confirm.dispose(e); }, { signal: controller.signal }); + yesButton.addEventListener( + 'click', + (e) => { + yesAction(); + gt.confirm.dispose(e); + }, + { signal: controller.signal } + ); const noButton = document.createElement('button'); noButton.type = 'button'; @@ -2196,26 +2332,23 @@ window.graphTool = (containerId, options) => { gt.deleteSelected = () => { if (!gt.selectedObj) return; - gt.confirm( - 'Do you want to delete the selected object?', - () => { - const i = gt.graphedObjs.findIndex((obj) => obj.id() === gt.selectedObj.id()); - gt.graphedObjs[i].remove(); - gt.graphedObjs.splice(i, 1); + gt.confirm('Do you want to delete the selected object?', () => { + const i = gt.graphedObjs.findIndex((obj) => obj.id() === gt.selectedObj.id()); + gt.graphedObjs[i].remove(); + gt.graphedObjs.splice(i, 1); - if (i < gt.graphedObjs.length) gt.selectedObj = gt.graphedObjs[i]; - else if (gt.graphedObjs.length) gt.selectedObj = gt.graphedObjs[0]; - else delete gt.selectedObj; - delete gt.selectTool.lastSelected; + if (i < gt.graphedObjs.length) gt.selectedObj = gt.graphedObjs[i]; + else if (gt.graphedObjs.length) gt.selectedObj = gt.graphedObjs[0]; + else delete gt.selectedObj; + delete gt.selectTool.lastSelected; - // Toggle the select tool so that the focus order event handlers are realigned. - gt.selectTool.deactivate(); - gt.selectTool.activate(); + // Toggle the select tool so that the focus order event handlers are realigned. + gt.selectTool.deactivate(); + gt.selectTool.activate(); - gt.updateObjects(); - gt.updateText(); - } - ); + gt.updateObjects(); + gt.updateText(); + }); }; // Add a button to delete the selected object. @@ -2237,17 +2370,14 @@ window.graphTool = (containerId, options) => { gt.clearAll = () => { if (gt.graphedObjs.length == 0) return; - gt.confirm( - 'Do you want to remove all graphed objects?', - () => { - gt.graphedObjs.forEach((obj) => obj.remove()); - gt.graphedObjs = []; - delete gt.selectedObj; - delete gt.selectTool.lastSelected; - gt.selectTool.activate(); - gt.html_input.value = ''; - } - ); + gt.confirm('Do you want to remove all graphed objects?', () => { + gt.graphedObjs.forEach((obj) => obj.remove()); + gt.graphedObjs = []; + delete gt.selectedObj; + delete gt.selectTool.lastSelected; + gt.selectTool.activate(); + gt.html_input.value = ''; + }); }; // Add a button to remove all graphed objects. @@ -2266,28 +2396,104 @@ window.graphTool = (containerId, options) => { clearButtonContainer.append(gt.clearButton); gt.buttonBox.append(clearButtonContainer); + // Full screen mode handlers. + const fullscreenScale = () => { + gt.graphContainer.style.removeProperty('transform'); + + const gtRect = gt.graphContainer.getBoundingClientRect(); + const fsRect = gt.graphContainer.parentElement.getBoundingClientRect(); + + const scale = + gtRect.height / gtRect.width < fsRect.height / fsRect.width + ? (fsRect.width * 0.95) / gtRect.width + : (fsRect.height * 0.95) / gtRect.height; + + gt.graphContainer.style.transform = `matrix(${scale},0,0,${scale},0,${ + (fsRect.height - gtRect.height) * 0.5 + })`; + + // Update the jsxgraph css transforms so that mouse cursor position is reported correctly. + gt.board.updateCSSTransforms(); + }; + + let promiseSupported = false; + + const toggleFullscreen = () => { + const wrap_node = + document.getElementById(`gt-fullscreenwrap-${containerId}`) || document.createElement('div'); + + if (!wrap_node.classList.contains('gt-fullscreenwrap')) { + // When the graphtool container is taken out of the DOM and placed in the wrap node in fullscreen mode + // the size of the page changes, and thus the current scroll position can change. So save the current + // scroll position so it can be restored when fullscreen mode is exited. + wrap_node.currentScroll = { x: window.scrollX, y: window.scrollY }; + + wrap_node.classList.add('gt-fullscreenwrap'); + wrap_node.id = `gt-fullscreenwrap-${containerId}`; + gt.graphContainer.before(wrap_node); + wrap_node.appendChild(gt.graphContainer); + } + + if (document.fullscreenElement || document.webkitFullscreenElement) { + document.exitFullscreen?.(); + document.webkitExitFullscreen?.(); + } else { + wrap_node.requestFullscreen = wrap_node.requestFullscreen || wrap_node.webkitRequestFullscreen; + if (wrap_node.requestFullscreen) { + // Disable the jsxgraph resize observer. It conflicts with the local resize observer. + gt.board.stopResizeObserver(); + const fullscreenPromise = wrap_node.requestFullscreen(); + if (fullscreenPromise instanceof Promise) { + promiseSupported = true; + fullscreenPromise.then(fullscreenScale); + } + gt.resizeObserver = new ResizeObserver(fullscreenScale); + gt.resizeObserver.observe(wrap_node); + gt.resizeObserver.observe(gt.graphContainer); + } + } + }; + // Add a button to switch to full screen mode. gt.fullScreenButton = document.createElement('button'); let fullScreenButtonMessage = 'Switch to fullscreen.'; gt.fullScreenButton.type = 'button'; gt.fullScreenButton.classList.add('gt-button', 'gt-text-button'); gt.fullScreenButton.textContent = 'Fullscreen'; - gt.fullScreenButton.addEventListener('click', () => gt.board.toFullscreen(containerId)); + gt.fullScreenButton.addEventListener('click', () => toggleFullscreen()); gt.fullScreenButton.addEventListener('pointerover', () => gt.setMessageText(fullScreenButtonMessage)); gt.fullScreenButton.addEventListener('pointerout', () => gt.updateHelp()); gt.fullScreenButton.addEventListener('focus', () => gt.setMessageText(fullScreenButtonMessage)); gt.fullScreenButton.addEventListener('blur', () => gt.updateHelp()); - document.addEventListener('fullscreenchange', () => { - if (document.fullscreenElement?.classList.contains('JXG_wrap_private')) { - gt.fullScreenButton.textContent = 'Exit Fullscreen'; - fullScreenButtonMessage = 'Exit fullscreen.'; - if (!gt.helpEnabled) gt.messageBox.classList.add('gt-disabled-help'); - } else { - gt.fullScreenButton.textContent = 'Fullscreen'; - fullScreenButtonMessage = 'Switch to fullscreen.'; - gt.messageBox.classList.remove('gt-disabled-help'); - } - }); + for (const eventType of ['fullscreenchange', 'webkitfullscreenchange']) { + document.addEventListener(eventType, () => { + const wrap_node = document.getElementById(`gt-fullscreenwrap-${containerId}`); + if (!wrap_node) return; + if (document.fullscreenElement === wrap_node || document.webkitFullscreenElement === wrap_node) { + gt.fullScreenButton.textContent = 'Exit Fullscreen'; + fullScreenButtonMessage = 'Exit fullscreen.'; + if (!promiseSupported) fullscreenScale(); + } else { + if (gt.resizeObserver) gt.resizeObserver.disconnect(); + delete gt.resizeObserver; + wrap_node.replaceWith(gt.graphContainer); + gt.graphContainer.style.removeProperty('transform'); + gt.board.updateCSSTransforms(); + // Give resize control back to jsxgraph. + gt.board.startResizeObserver(); + if (wrap_node.currentScroll) { + window.scroll({ + left: wrap_node.currentScroll.x, + top: wrap_node.currentScroll.y, + behavior: 'instant' + }); + } + gt.fullScreenButton.textContent = 'Fullscreen'; + fullScreenButtonMessage = 'Switch to fullscreen.'; + } + gt.updateHelp(); + }); + } gt.buttonBox.append(gt.fullScreenButton); // Add a button to disable or enable help. @@ -2330,8 +2536,10 @@ window.graphTool = (containerId, options) => { gt.messageBox = document.createElement('div'); gt.messageBox.classList.add('gt-message-box'); - gt.messageBox.setAttribute('role', 'region') - gt.messageBox.setAttribute('aria-live', 'polite') + if (!gt.helpEnabled) gt.messageBox.classList.add('gt-disabled-help'); + gt.messageBox.setAttribute('role', 'region'); + gt.messageBox.setAttribute('aria-live', 'polite'); + gt.messageBox.dataset.iframeHeight = '1'; gt.graphContainer.append(gt.messageBox); gt.messageBox.addEventListener('keydown', (e) => { if (e.key === 'Escape') gt.confirm.dispose?.(e); diff --git a/htdocs/js/GraphTool/graphtool.scss b/htdocs/js/GraphTool/graphtool.scss index bb46301ea4..0d7b573ed1 100644 --- a/htdocs/js/GraphTool/graphtool.scss +++ b/htdocs/js/GraphTool/graphtool.scss @@ -43,10 +43,15 @@ text-align: center; padding: 0.375rem 0.75rem; border-radius: 4px; - box-shadow: inset 0 1px 0 #ffffff26, 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: + inset 0 1px 0 #ffffff26, + 0 1px 1px rgba(0, 0, 0, 0.075); display: inline-block; text-align: center; - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: + color 0.15s ease-in-out, + background-color 0.15s ease-in-out, + box-shadow 0.15s ease-in-out; user-select: none; vertical-align: middle; @@ -91,7 +96,9 @@ } &:disabled { - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: + inset 0 2px 4px rgba(0, 0, 0, 0.15), + 0 1px 2px rgba(0, 0, 0, 0.05); } &:hover { @@ -105,7 +112,9 @@ &:focus-visible { background-color: #d3d4d5; - box-shadow: inset 0 1px 0 #ffffff26, 0 1px 1px rgba(0, 0, 0, 0.075), + box-shadow: + inset 0 1px 0 #ffffff26, + 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 0.2rem rgba(211, 212, 213, 0.5); } @@ -298,7 +307,9 @@ &:focus-visible { background-color: #5c636a; border-color: #565e64; - box-shadow: inset 0 1px 0 #ffffff26, 0 1px 1px rgba(0, 0, 0, 0.075), + box-shadow: + inset 0 1px 0 #ffffff26, + 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 0.2rem rgba(130, 138, 145, 0.5); } } @@ -307,20 +318,31 @@ } } -.JXG_wrap_private:fullscreen .gt-message-box { - position: relative; - min-height: 92px; +.gt-fullscreenwrap:fullscreen { + background-color: #ccc; + padding: 0; width: 100%; - border-width: 1px; - margin-top: 0.5rem; - padding: 0.25rem 0.5rem; + height: 100%; - &.gt-disabled-help:empty { - min-height: 0; - opacity: 0; - margin-top: 0; - padding: 0; - border: none; + .graphtool-container { + margin: 0 auto; + + .gt-message-box { + position: relative; + min-height: 92px; + width: 100%; + border-width: 1px; + margin-top: 0.5rem; + padding: 0.25rem 0.5rem; + + &.gt-disabled-help:empty { + min-height: 0; + opacity: 0; + margin-top: 0; + padding: 0; + border: none; + } + } } } diff --git a/htdocs/js/GraphTool/intervaltools.js b/htdocs/js/GraphTool/intervaltools.js index ba39a04a72..fcf5eabc12 100644 --- a/htdocs/js/GraphTool/intervaltools.js +++ b/htdocs/js/GraphTool/intervaltools.js @@ -14,27 +14,25 @@ 'segment', [ [ - () => ( + () => gt.isNegInfX(point1.X()) ? gt.board.getBoundingBox()[0] + 8 / gt.board.unitX : gt.isPosInfX(point1.X()) ? gt.board.getBoundingBox()[2] - 8 / gt.board.unitX : gt.options.useBracketEnds ? point1.X() - : point1.X() + (point1.X() < point2.X() ? 4 : -4) / gt.board.unitX - ), + : point1.X() + (point1.X() < point2.X() ? 4 : -4) / gt.board.unitX, 0 ], [ - () => ( + () => gt.isNegInfX(point2.X()) ? gt.board.getBoundingBox()[0] + 8 / gt.board.unitX : gt.isPosInfX(point2.X()) ? gt.board.getBoundingBox()[2] - 8 / gt.board.unitX : gt.options.useBracketEnds ? point2.X() - : point2.X() + (point1.X() < point2.X() ? -4 : 4) / gt.board.unitX - ), + : point2.X() + (point1.X() < point2.X() ? -4 : 4) / gt.board.unitX, 0 ] ], @@ -124,8 +122,12 @@ // The default layer for text is 9. // Setting the layer moves the text in front of any other text objects. - point.text?.setAttribute( - { fontSize: 23, highlight: true, strokeColor: gt.color.underConstruction, layer: 9 }); + point.text?.setAttribute({ + fontSize: 23, + highlight: true, + strokeColor: gt.color.underConstruction, + layer: 9 + }); // The default layer for lines (of which arrows are a part) is 7. // Setting this moves the arrow to the front of arrows of other intervals. @@ -135,19 +137,23 @@ // This makes it so that if the pointer is over the point and it is a hidden point at infinity, // then it looks like the pointer is over the arrow. The end arrows don't actually receive // hover events, so this has to be done this way. - point.on('over', - () => point.arrow?.rendNodeTriangleEnd.setAttribute('fill', gt.color.pointHighlightDarker)); + point.on('over', () => + point.arrow?.rendNodeTriangleEnd.setAttribute('fill', gt.color.pointHighlightDarker) + ); point.on('out', () => point.arrow?.rendNodeTriangleEnd.setAttribute('fill', gt.color.point)); point.text?.on('down', () => { point.text.setAttribute({ - cssStyle: 'cursor:none;font-weight:900', strokeColor: gt.color.pointHighlightDarker + cssStyle: 'cursor:none;font-weight:900', + strokeColor: gt.color.pointHighlightDarker }); point.paired_point?.text?.setAttribute({ cssStyle: 'cursor:none;font-weight:900' }); }); point.text?.on('up', () => { - point.text.setAttribute( - { cssStyle: 'cursor:auto;font-weight:900', strokeColor: gt.color.underConstruction }); + point.text.setAttribute({ + cssStyle: 'cursor:auto;font-weight:900', + strokeColor: gt.color.underConstruction + }); point.paired_point?.text?.setAttribute({ cssStyle: 'cursor:auto;font-weight:900' }); }); } @@ -162,11 +168,10 @@ isEventTarget(_gt, e) { if (this.baseObj.rendNode === e.target) return true; return this.definingPts.some( - (point) => ( + (point) => point.rendNode === e.target || point.text?.rendNode === e.target || point.arrow?.rendNode === e.target - ) ); }, @@ -190,9 +195,10 @@ const rightX = gt.snapRound(rightEndPoint.X(), gt.snapSizeX); return `${gt.isNegInfX(leftX) || leftEndPoint.getAttribute('fillColor') === 'transparent' ? '(' : '['}${ - gt.isNegInfX(leftX) ? '-infinity' : leftX},${ - gt.isPosInfX(rightX) ? 'infinity' : rightX}${ - gt.isPosInfX(rightX) || rightEndPoint.getAttribute('fillColor') === 'transparent' ? ')' : ']'}`; + gt.isNegInfX(leftX) ? '-infinity' : leftX + },${gt.isPosInfX(rightX) ? 'infinity' : rightX}${ + gt.isPosInfX(rightX) || rightEndPoint.getAttribute('fillColor') === 'transparent' ? ')' : ']' + }`; }, setSolid() {}, @@ -205,14 +211,19 @@ }, restore(gt, string) { - const intervalParts = - string.match(new RegExp([ - /\s*([[(])\s*/, // left delimiter - /(-?(?:[0-9]*(?:\.[0-9]*)?|infinity))/, // left end point - /\s*,\s*/, // comma - /(-?(?:[0-9]*(?:\.[0-9]*)?|infinity))/, // right end point - /\s*([\])])\s*/ // right delimiter - ].map((r) => r.source).join(''))); + const intervalParts = string.match( + new RegExp( + [ + /\s*([[(])\s*/, // left delimiter + /(-?(?:[0-9]*(?:\.[0-9]*)?|infinity))/, // left end point + /\s*,\s*/, // comma + /(-?(?:[0-9]*(?:\.[0-9]*)?|infinity))/, // right end point + /\s*([\])])\s*/ // right delimiter + ] + .map((r) => r.source) + .join('') + ) + ); if (!intervalParts || intervalParts.length !== 5) return false; const bbox = gt.board.getBoundingBox(); @@ -239,28 +250,28 @@ this.focusPoint?.setAttribute({ fillColor: include ? gt.color.curve : 'transparent', highlightFillColor: include ? gt.color.pointHighlightDarker : gt.color.pointHighlight, - highlightFillOpacity: (gt.options.useBracketEnds || this.focusPoint.arrow) - ? 0 - : include ? 1 : 0.5 + highlightFillOpacity: gt.options.useBracketEnds || this.focusPoint.arrow ? 0 : include ? 1 : 0.5 }); }, setFocusBlurPointAttributes(gt, point) { - const attributes = this.focused ? { - size: 4, - strokeWidth: 3, - strokeColor: gt.color.underConstruction, - fillColor: gt.color.underConstruction, - fixed: false, - highlight: true - } : { - size: 3, - strokeWidth: 2, - strokeColor: gt.color.curve, - fillColor: gt.color.curve, - fixed: true, - highlight: false - }; + const attributes = this.focused + ? { + size: 4, + strokeWidth: 3, + strokeColor: gt.color.underConstruction, + fillColor: gt.color.underConstruction, + fixed: false, + highlight: true + } + : { + size: 3, + strokeWidth: 2, + strokeColor: gt.color.curve, + fillColor: gt.color.curve, + fixed: true, + highlight: false + }; if (!this.focused && point.getAttribute('highlightFillOpacity') !== 0) attributes.highlightFillOpacity = 1; if (point.getAttribute('fillColor') === 'transparent') { @@ -291,10 +302,10 @@ [ [ this.definingPts[index].X() + - (gt.isPosInfX(this.definingPts[index].X()) ? -26 : 26) / gt.board.unitX, + (gt.isPosInfX(this.definingPts[index].X()) ? -26 : 26) / gt.board.unitX, 0 ], - [ this.definingPts[index].X(), 0 ] + [this.definingPts[index].X(), 0] ], { fixed: true, @@ -325,7 +336,8 @@ !gt.options.useBracketEnds && this.focused && this.definingPts[index].getAttribute('fillColor') === 'transparent' - ? 0.5 : 1 + ? 0.5 + : 1 }); } if (this.definingPts[index].arrow) { @@ -348,7 +360,8 @@ createPoint(gt, x, _y, paired_point) { const point = gt.board.create('point', [gt.snapRound(x, gt.snapSizeX), 0], { - snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, ...gt.graphObjectTypes.interval.definingPointAttributes(), ...gt.graphObjectTypes.interval.maybeBracketAttributes() }); @@ -368,12 +381,19 @@ point.text = gt.board.create( 'text', [ - () => point.X() + (point.paired_point ? (point.paired_point.X() > point.X() ? 1 : -1) : 0) - / gt.board.unitX, + () => + point.X() + + (point.paired_point ? (point.paired_point.X() > point.X() ? 1 : -1) : 0) / + gt.board.unitX, () => 1 / gt.board.unitY, - () => point.paired_point && point.paired_point.X() < point.X() - ? (point.getAttribute('fillColor') === 'transparent' ? ')' : ']') - : (point.getAttribute('fillColor') === 'transparent' ? '(' : '[') + () => + point.paired_point && point.paired_point.X() < point.X() + ? point.getAttribute('fillColor') === 'transparent' + ? ')' + : ']' + : point.getAttribute('fillColor') === 'transparent' + ? '(' + : '[' ], { fontSize: 23, @@ -392,8 +412,9 @@ pointDown(gt, point) { if (gt.activeTool !== gt.selectTool) return; - const thisObj = - gt.graphedObjs.filter((obj) => obj.definingPts.filter((pt) => pt === point).length)[0]; + const thisObj = gt.graphedObjs.filter( + (obj) => obj.definingPts.filter((pt) => pt === point).length + )[0]; if (!thisObj) return; if (!thisObj.focused) { @@ -435,7 +456,7 @@ return gt.options.useBracketEnds ? { strokeOpacity: 0, fillOpacity: 0, highlightStrokeOpacity: 0, highlightFillOpacity: 0 } : {}; - }, + } } }, @@ -481,13 +502,19 @@ point.text = gt.board.create( 'text', [ - () => point.X() + - (point.paired_point ? (point.paired_point.X() > point.X() ? 1 : -1) : 0) - / gt.board.unitX, + () => + point.X() + + (point.paired_point ? (point.paired_point.X() > point.X() ? 1 : -1) : 0) / + gt.board.unitX, () => 1 / gt.board.unitY, - () => point.paired_point && point.paired_point.X() < point.X() - ? (point.getAttribute('fillColor') === 'transparent' ? ')' : ']') - : (point.getAttribute('fillColor') === 'transparent' ? '(' : '[') + () => + point.paired_point && point.paired_point.X() < point.X() + ? point.getAttribute('fillColor') === 'transparent' + ? ')' + : ']' + : point.getAttribute('fillColor') === 'transparent' + ? '(' + : '[' ], { fontSize: 23, @@ -514,7 +541,8 @@ this.updateHighlights(new JXG.Coords(JXG.COORDS_BY_USER, [newX, 0], gt.board)); - this.helpText = 'Plot the second endpoint. ' + + this.helpText = + 'Plot the second endpoint. ' + 'Move the point to the left end for \\(-\\infty\\), ' + 'or to the right end for \\(\\infty\\).'; gt.updateHelp(); @@ -526,8 +554,10 @@ this.phase2 = (coords) => { // Don't allow the second point to be created on the first point or off the board. - if (this.point1.X() === gt.snapRound(coords[1], gt.snapSizeX) - || !gt.boardHasPoint(coords[1], coords[2])) + if ( + this.point1.X() === gt.snapRound(coords[1], gt.snapSizeX) || + !gt.boardHasPoint(coords[1], coords[2]) + ) return; gt.board.off('up'); @@ -587,41 +617,42 @@ } else if (e instanceof JXG.Coords) { coords = e; this.hlObjs.hl_point?.setPosition(JXG.COORDS_BY_USER, [coords.usrCoords[1], coords.usrCoords[2]]); - } else - return false; + } else return false; if (!this.hlObjs.hl_point) { - this.hlObjs.hl_point = gt.board.create( - 'point', - [ coords.usrCoords[1], 0 ], - { - size: 4, - strokeWidth: 3, - highlight: false, - withLabel: false, - snapToGrid: true, - snapSizeX: gt.snapSizeX, - snapSizeY: gt.snapSizeY, - strokeColor: gt.color.underConstruction, - fillColor: gt.toolTypes.IncludeExcludePointTool.include - ? gt.color.underConstruction - : 'transparent', - ...gt.graphObjectTypes.interval.maybeBracketAttributes() - } - ); + this.hlObjs.hl_point = gt.board.create('point', [coords.usrCoords[1], 0], { + size: 4, + strokeWidth: 3, + highlight: false, + withLabel: false, + snapToGrid: true, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, + strokeColor: gt.color.underConstruction, + fillColor: gt.toolTypes.IncludeExcludePointTool.include + ? gt.color.underConstruction + : 'transparent', + ...gt.graphObjectTypes.interval.maybeBracketAttributes() + }); if (gt.options.useBracketEnds) { this.hlObjs.hl_point.rendNode.classList.add('hidden-end-point'); this.hlObjs.hl_text = gt.board.create( 'text', [ - () => this.hlObjs.hl_point.X() + + () => + this.hlObjs.hl_point.X() + (this.point1 ? (this.point1.X() > this.hlObjs.hl_point.X() ? 1 : -1) : 0) / gt.board.unitX, () => 1 / gt.board.unitY, - () => gt.toolTypes.IncludeExcludePointTool.include - ? (this.point1?.X() < this.hlObjs.hl_point?.X() ? ']' : '[') - : (this.point1?.X() < this.hlObjs.hl_point?.X() ? ')' : '(') + () => + gt.toolTypes.IncludeExcludePointTool.include + ? this.point1?.X() < this.hlObjs.hl_point?.X() + ? ']' + : '[' + : this.point1?.X() < this.hlObjs.hl_point?.X() + ? ')' + : '(' ], { fontSize: 23, @@ -647,32 +678,29 @@ 'segment', [ [ - () => ( - this.point1 ? ( - gt.isNegInfX(this.point1.X()) + () => + (this.point1 + ? gt.isNegInfX(this.point1.X()) ? gt.board.getBoundingBox()[0] + 8 / gt.board.unitX : gt.isPosInfX(this.point1.X()) ? gt.board.getBoundingBox()[2] - 8 / gt.board.unitX : this.point1.X() - ) - : 0 - ) + ( - gt.options.useBracketEnds || + : 0) + + (gt.options.useBracketEnds || gt.isNegInfX(this.point1?.X()) || gt.isPosInfX(this.point1?.X()) ? 0 - : (this.point1?.X() < this.hlObjs.hl_point?.X() ? 4 : -4) / gt.board.unitX - ), + : (this.point1?.X() < this.hlObjs.hl_point?.X() ? 4 : -4) / gt.board.unitX), 0 ], [ - () => (this.hlObjs.hl_point?.X() ?? 0) + ( - gt.options.useBracketEnds || + () => + (this.hlObjs.hl_point?.X() ?? 0) + + (gt.options.useBracketEnds || gt.isNegInfX(this.hlObjs.hl_point?.X()) || gt.isPosInfX(this.hlObjs.hl_point?.X()) ? 0 - : (this.point1?.X() < this.hlObjs.hl_point?.X() ? -4 : 4) / gt.board.unitX - ), + : (this.point1?.X() < this.hlObjs.hl_point?.X() ? -4 : 4) / gt.board.unitX), 0 ] ], @@ -685,13 +713,14 @@ } if (this.hlObjs.hl_segment) { - if (gt.isNegInfX(gt.snapRound(coords.usrCoords[1], gt.snapSizeX)) || - gt.isPosInfX(gt.snapRound(coords.usrCoords[1], gt.snapSizeX))) - { - this.hlObjs.hl_segment.setArrow( - this.hlObjs.hl_segment.getAttribute('firstArrow'), - { type: 2, size: 4 } - ); + if ( + gt.isNegInfX(gt.snapRound(coords.usrCoords[1], gt.snapSizeX)) || + gt.isPosInfX(gt.snapRound(coords.usrCoords[1], gt.snapSizeX)) + ) { + this.hlObjs.hl_segment.setArrow(this.hlObjs.hl_segment.getAttribute('firstArrow'), { + type: 2, + size: 4 + }); if (gt.options.useBracketEnds) this.hlObjs.hl_text.setAttribute({ strokeOpacity: 0 }); else this.hlObjs.hl_point.setAttribute({ strokeOpacity: 0, fillOpacity: 0 }); this.hlObjs.hl_point.rendNode.classList.add('hidden-inf-point'); @@ -702,9 +731,10 @@ this.hlObjs.hl_point.rendNode.classList.remove('hidden-inf-point'); } } else if (this.hlObjs.hl_point) { - if (gt.isNegInfX(gt.snapRound(coords.usrCoords[1], gt.snapSizeX)) || - gt.isPosInfX(gt.snapRound(coords.usrCoords[1], gt.snapSizeX))) - { + if ( + gt.isNegInfX(gt.snapRound(coords.usrCoords[1], gt.snapSizeX)) || + gt.isPosInfX(gt.snapRound(coords.usrCoords[1], gt.snapSizeX)) + ) { if (!this.hlObjs.hl_arrow) { this.hlObjs.hl_arrow = gt.board.create( 'arrow', @@ -714,7 +744,7 @@ (gt.isPosInfX(this.hlObjs.hl_point.X()) ? -26 : 26) / gt.board.unitX, 0 ], - [ this.hlObjs.hl_point.X(), 0 ] + [this.hlObjs.hl_point.X(), 0] ], { fixed: true, @@ -760,7 +790,8 @@ // Draw a highlight point on the board. this.updateHighlights(new JXG.Coords(JXG.COORDS_BY_USER, [0, 0], gt.board)); - this.helpText = 'Plot the first endpoint. ' + + this.helpText = + 'Plot the first endpoint. ' + 'Move the point to the left end for \\(-\\infty\\), ' + 'or to the right end for \\(\\infty\\).'; gt.updateHelp(); @@ -805,18 +836,17 @@ gt.toolTypes.IncludeExcludePointTool.includePointButton.addEventListener('click', (e) => gt.toolTypes.IncludeExcludePointTool.toggleIncludeExcludePoint(e, true) ); - gt.toolTypes.IncludeExcludePointTool.includePointButton - .addEventListener('focus', () => gt.setMessageText(includeButtonMessage)); - gt.toolTypes.IncludeExcludePointTool.includePointButton - .addEventListener('blur', () => gt.updateHelp()); + gt.toolTypes.IncludeExcludePointTool.includePointButton.addEventListener('focus', () => + gt.setMessageText(includeButtonMessage) + ); + gt.toolTypes.IncludeExcludePointTool.includePointButton.addEventListener('blur', () => gt.updateHelp()); includePointButtonDiv.append(gt.toolTypes.IncludeExcludePointTool.includePointButton); includePointBox.append(includePointButtonDiv); const excludePointButtonDiv = document.createElement('div'); const excludeButtonMessage = 'Exclude the selected point (e).'; excludePointButtonDiv.classList.add('gt-button-div', 'gt-tool-button-pair-bottom'); - excludePointButtonDiv.addEventListener('pointerover', - () => gt.setMessageText(excludeButtonMessage)); + excludePointButtonDiv.addEventListener('pointerover', () => gt.setMessageText(excludeButtonMessage)); excludePointButtonDiv.addEventListener('pointerout', () => gt.updateHelp()); gt.toolTypes.IncludeExcludePointTool.excludePointButton = document.createElement('button'); gt.toolTypes.IncludeExcludePointTool.excludePointButton.classList.add( @@ -832,10 +862,10 @@ gt.toolTypes.IncludeExcludePointTool.excludePointButton.addEventListener('click', (e) => gt.toolTypes.IncludeExcludePointTool.toggleIncludeExcludePoint(e, false) ); - gt.toolTypes.IncludeExcludePointTool.excludePointButton - .addEventListener('focus', () => gt.setMessageText(excludeButtonMessage)); - gt.toolTypes.IncludeExcludePointTool.excludePointButton - .addEventListener('blur', () => gt.updateHelp()); + gt.toolTypes.IncludeExcludePointTool.excludePointButton.addEventListener('focus', () => + gt.setMessageText(excludeButtonMessage) + ); + gt.toolTypes.IncludeExcludePointTool.excludePointButton.addEventListener('blur', () => gt.updateHelp()); excludePointButtonDiv.append(gt.toolTypes.IncludeExcludePointTool.excludePointButton); includePointBox.append(excludePointButtonDiv); container.append(includePointBox); @@ -855,11 +885,9 @@ helpText(gt) { return (gt.selectedObj && typeof gt.selectedObj.setIncludePoint === 'function') || (gt.activeTool && gt.activeTool.supportsIncludeExclude) - ? ( - `Use the ${gt.options.useBracketEnds ? '(' : '\\(\\circ\\)'} or ${ + ? `Use the ${gt.options.useBracketEnds ? '(' : '\\(\\circ\\)'} or ${ gt.options.useBracketEnds ? '[' : '\\(\\bullet\\)' } button or type e or i to exclude or include the selected endpoint.` - ) : ''; } }, diff --git a/htdocs/js/GraphTool/pointtool.js b/htdocs/js/GraphTool/pointtool.js index cbe7d0e231..3a7eb63959 100644 --- a/htdocs/js/GraphTool/pointtool.js +++ b/htdocs/js/GraphTool/pointtool.js @@ -7,9 +7,15 @@ Point: { preInit(gt, x, y) { return gt.board.create('point', [x, y], { - size: 2, snapToGrid: true, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false, - strokeColor: gt.color.curve, fixed: gt.isStatic, - highlightStrokeColor: gt.color.underConstruction, highlightFillColor: gt.color.pointHighlight + size: 2, + snapToGrid: true, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, + withLabel: false, + strokeColor: gt.color.curve, + fixed: gt.isStatic, + highlightStrokeColor: gt.color.underConstruction, + highlightFillColor: gt.color.pointHighlight }); }, @@ -22,24 +28,35 @@ this.focusPoint = this.baseObj; if (!gt.isStatic) { - this.on('down', () => gt.board.containerObj.style.cursor = 'none'); - this.on('up', () => gt.board.containerObj.style.cursor = 'auto'); - this.on('drag', (e) => { gt.adjustDragPosition(e, this.baseObj); gt.updateText(); }); + this.on('down', () => (gt.board.containerObj.style.cursor = 'none')); + this.on('up', () => (gt.board.containerObj.style.cursor = 'auto')); + this.on('drag', (e) => { + gt.adjustDragPosition(e, this.baseObj); + gt.updateText(); + }); } }, blur(gt) { this.focused = false; - this.baseObj.setAttribute( - { fixed: true, highlight: false, strokeColor: gt.color.curve, strokeWidth: 2 }); + this.baseObj.setAttribute({ + fixed: true, + highlight: false, + strokeColor: gt.color.curve, + strokeWidth: 2 + }); gt.updateHelp(); return false; }, focus(gt) { this.focused = true; - this.baseObj.setAttribute( - { fixed: false, highlight: true, strokeColor: gt.color.focusCurve, strokeWidth: 3 }); + this.baseObj.setAttribute({ + fixed: false, + highlight: true, + strokeColor: gt.color.focusCurve, + strokeWidth: 3 + }); this.focusPoint.rendNode.focus(); gt.updateHelp(); @@ -49,8 +66,10 @@ setSolid() {}, stringify(gt) { - return `(${ - gt.snapRound(this.baseObj.X(), gt.snapSizeX)},${gt.snapRound(this.baseObj.Y(), gt.snapSizeY)})`; + return `(${gt.snapRound(this.baseObj.X(), gt.snapSizeX)},${gt.snapRound( + this.baseObj.Y(), + gt.snapSizeY + )})`; }, updateTextCoords(gt, coords) { @@ -113,13 +132,17 @@ } else if (e instanceof JXG.Coords) { coords = e; this.hlObjs.hl_point?.setPosition(JXG.COORDS_BY_USER, [coords.usrCoords[1], coords.usrCoords[2]]); - } else - return false; + } else return false; if (!this.hlObjs.hl_point) { this.hlObjs.hl_point = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], { - size: 2, color: gt.color.underConstruction, snapToGrid: true, highlight: false, - snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false + size: 2, + color: gt.color.underConstruction, + snapToGrid: true, + highlight: false, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, + withLabel: false }); this.hlObjs.hl_point.rendNode.focus(); } diff --git a/htdocs/js/GraphTool/quadratictool.js b/htdocs/js/GraphTool/quadratictool.js index 51cbd8ba61..d2854a5a70 100644 --- a/htdocs/js/GraphTool/quadratictool.js +++ b/htdocs/js/GraphTool/quadratictool.js @@ -8,8 +8,8 @@ preInit(gt, point1, point2, point3, solid) { [point1, point2, point3].forEach((point) => { point.setAttribute(gt.definingPointAttributes); - point.on('down', () => gt.board.containerObj.style.cursor = 'none'); - point.on('up', () => gt.board.containerObj.style.cursor = 'auto'); + point.on('down', () => (gt.board.containerObj.style.cursor = 'none')); + point.on('up', () => (gt.board.containerObj.style.cursor = 'auto')); }); return gt.graphObjectTypes.quadratic.createQuadratic(point1, point2, point3, solid, gt.color.curve); }, @@ -41,32 +41,53 @@ } if (points.length < 3) return false; const point1 = gt.graphObjectTypes.quadratic.createPoint( - parseFloat(points[0][0]), parseFloat(points[0][1])); + parseFloat(points[0][0]), + parseFloat(points[0][1]) + ); const point2 = gt.graphObjectTypes.quadratic.createPoint( - parseFloat(points[1][0]), parseFloat(points[1][1]), [point1]); + parseFloat(points[1][0]), + parseFloat(points[1][1]), + [point1] + ); const point3 = gt.graphObjectTypes.quadratic.createPoint( - parseFloat(points[2][0]), parseFloat(points[2][1]), [point1, point2]); + parseFloat(points[2][0]), + parseFloat(points[2][1]), + [point1, point2] + ); return new gt.graphObjectTypes.quadratic(point1, point2, point3, /solid/.test(string)); }, helperMethods: { createQuadratic(gt, point1, point2, point3, solid, color) { - return gt.board.create('curve', [ - // x and y coordinates of point on curve - (x) => x, - (x) => { - const x1 = point1.X(), x2 = point2.X(), x3 = point3.X(), - y1 = point1.Y(), y2 = point2.Y(), y3 = point3.Y(); - return (x - x2) * (x - x3) * y1 / ((x1 - x2) * (x1 - x3)) - + (x - x1) * (x - x3) * y2 / ((x2 - x1) * (x2 - x3)) - + (x - x1) * (x - x2) * y3 / ((x3 - x1) * (x3 - x2)); - }, - // domain minimum and maximum - () => gt.board.getBoundingBox()[0], () => gt.board.getBoundingBox()[2] - ], { - strokeWidth: 2, highlight: false, strokeColor: color ? color : gt.color.underConstruction, - dash: solid ? 0 : 2 - }); + return gt.board.create( + 'curve', + [ + // x and y coordinates of point on curve + (x) => x, + (x) => { + const x1 = point1.X(), + x2 = point2.X(), + x3 = point3.X(), + y1 = point1.Y(), + y2 = point2.Y(), + y3 = point3.Y(); + return ( + ((x - x2) * (x - x3) * y1) / ((x1 - x2) * (x1 - x3)) + + ((x - x1) * (x - x3) * y2) / ((x2 - x1) * (x2 - x3)) + + ((x - x1) * (x - x2) * y3) / ((x3 - x1) * (x3 - x2)) + ); + }, + // domain minimum and maximum + () => gt.board.getBoundingBox()[0], + () => gt.board.getBoundingBox()[2] + ], + { + strokeWidth: 2, + highlight: false, + strokeColor: color ? color : gt.color.underConstruction, + dash: solid ? 0 : 2 + } + ); }, // Prevent a point from being moved off the board by a drag. If a group of other points is provided, @@ -94,7 +115,7 @@ } point.setPosition(JXG.COORDS_BY_USER, [ - left_x < bbox[0] ? right_x : (preferLeft || right_x > bbox[2]) ? left_x : right_x, + left_x < bbox[0] ? right_x : preferLeft || right_x > bbox[2] ? left_x : right_x, y ]); } @@ -110,7 +131,12 @@ const point = gt.board.create( 'point', [gt.snapRound(x, gt.snapSizeX), gt.snapRound(y, gt.snapSizeY)], - { size: 2, snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, withLabel: false } + { + size: 2, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, + withLabel: false + } ); point.setAttribute({ snapToGrid: true }); if (typeof grouped_points !== 'undefined' && grouped_points.length) { @@ -122,9 +148,12 @@ paired_point.on('drag', gt.graphObjectTypes.quadratic.groupedPointDrag); } paired_point.grouped_points.push(point); - if (!paired_point.eventHandlers.drag || - paired_point.eventHandlers.drag.every((dragHandler) => - dragHandler.handler !== gt.graphObjectTypes.quadratic.groupedPointDrag) + if ( + !paired_point.eventHandlers.drag || + paired_point.eventHandlers.drag.every( + (dragHandler) => + dragHandler.handler !== gt.graphObjectTypes.quadratic.groupedPointDrag + ) ) paired_point.on('drag', gt.graphObjectTypes.quadratic.groupedPointDrag); }); @@ -169,8 +198,10 @@ this.phase2 = (coords) => { // Don't allow the second point to be created on the same // vertical line as the first point or off the board. - if (this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) || - !gt.boardHasPoint(coords[1], coords[2])) + if ( + this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) || + !gt.boardHasPoint(coords[1], coords[2]) + ) return; gt.board.off('up'); @@ -199,15 +230,19 @@ this.phase3 = (coords) => { // Don't allow the third point to be created on the same vertical line as the // first point, on the same vertical line as the second point, or off the board. - if (this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) || + if ( + this.point1.X() == gt.snapRound(coords[1], gt.snapSizeX) || this.point2.X() == gt.snapRound(coords[1], gt.snapSizeX) || - !gt.boardHasPoint(coords[1], coords[2])) + !gt.boardHasPoint(coords[1], coords[2]) + ) return; gt.board.off('up'); - const point3 = gt.graphObjectTypes.quadratic.createPoint(coords[1], coords[2], - [this.point1, this.point2]); + const point3 = gt.graphObjectTypes.quadratic.createPoint(coords[1], coords[2], [ + this.point1, + this.point2 + ]); gt.selectedObj = new gt.graphObjectTypes.quadratic(this.point1, this.point2, point3, gt.drawSolid); gt.selectedObj.focusPoint = point3; gt.graphedObjs.push(gt.selectedObj); @@ -245,14 +280,17 @@ } else if (e instanceof JXG.Coords) { coords = e; this.hlObjs.hl_point?.setPosition(JXG.COORDS_BY_USER, [coords.usrCoords[1], coords.usrCoords[2]]); - } else - return false; + } else return false; if (!this.hlObjs.hl_point) { this.hlObjs.hl_point = gt.board.create('point', [coords.usrCoords[1], coords.usrCoords[2]], { - size: 2, color: gt.color.underConstruction, snapToGrid: true, - snapSizeX: gt.snapSizeX, snapSizeY: gt.snapSizeY, - highlight: false, withLabel: false + size: 2, + color: gt.color.underConstruction, + snapToGrid: true, + snapSizeX: gt.snapSizeX, + snapSizeY: gt.snapSizeY, + highlight: false, + withLabel: false }); this.hlObjs.hl_point.rendNode.focus(); } @@ -263,7 +301,7 @@ const groupedPoints = []; if (this.point1) groupedPoints.push(this.point1); if (this.point2) groupedPoints.push(this.point2); - gt.graphObjectTypes.quadratic.adjustDragPosition(e, this.hlObjs.hl_point, groupedPoints) + gt.graphObjectTypes.quadratic.adjustDragPosition(e, this.hlObjs.hl_point, groupedPoints); } if (this.point2 && !this.hlObjs.hl_quadratic) { @@ -274,10 +312,16 @@ } this.hlObjs.hl_quadratic = gt.graphObjectTypes.quadratic.createQuadratic( - this.point1, this.point2, this.hlObjs.hl_point, gt.drawSolid); + this.point1, + this.point2, + this.hlObjs.hl_point, + gt.drawSolid + ); } else if (this.point1 && !this.point2 && !this.hlObjs.hl_line) { this.hlObjs.hl_line = gt.board.create('line', [this.point1, this.hlObjs.hl_point], { - fixed: true, strokeColor: gt.color.underConstruction, highlight: false, + fixed: true, + strokeColor: gt.color.underConstruction, + highlight: false, dash: gt.drawSolid ? 0 : 2 }); } diff --git a/htdocs/js/ImageView/imageview.css b/htdocs/js/ImageView/imageview.css deleted file mode 100644 index 874d9647a8..0000000000 --- a/htdocs/js/ImageView/imageview.css +++ /dev/null @@ -1,60 +0,0 @@ -.image-view-elt:hover { - cursor: pointer; -} - -.image-view-dialog.modal { - padding: 0 !important; -} - -.image-view-dialog .modal-dialog { - margin: 0; -} - -.image-view-dialog .modal-body { - overflow: unset; - padding: 8px; - text-align: center; - box-sizing: content-box !important; -} - -.image-view-dialog .modal-header { - padding: 0.1rem 0.5rem; -} - -.image-view-dialog .modal-header .drag-handle { - cursor: pointer; - width: 100%; - height: 26px; - touch-action: none; -} - -.image-view-dialog .modal-header .btn { - padding: 0 0.2rem; - margin: 0 0.25rem 0 0; -} - -.image-view-dialog .modal-header .btn svg { - opacity: 0.5; -} - -.image-view-dialog .modal-header .btn svg:hover { - opacity: 1; -} - -.image-view-dialog .modal-header .btn-close { - padding: 0.25rem 0.25rem; - margin: -0.5rem 0 -0.5rem auto; -} - -.image-view-dialog .modal-body img { - max-width: 100%; - height: 100%; -} - -.image-view-dialog .modal-body svg { - overflow: visible; -} - -.image-view-dialog .btn-close { - cursor: pointer; -} diff --git a/htdocs/js/ImageView/imageview.js b/htdocs/js/ImageView/imageview.js index 4db35e24ba..be6ee5b1f3 100644 --- a/htdocs/js/ImageView/imageview.js +++ b/htdocs/js/ImageView/imageview.js @@ -3,7 +3,7 @@ /* global bootstrap */ (() => { - const imageViewDialog = function() { + const imageViewDialog = function () { const img = this.cloneNode(true); const imgType = img.tagName.toLowerCase(); img.classList.remove('image-view-elt'); @@ -28,7 +28,7 @@ const modal = document.createElement('div'); modal.classList.add('modal', 'image-view-dialog'); - modal.ariaLabel = 'image view dialog'; + modal.setAttribute('aria-label', 'image view dialog'); modal.tabIndex = -1; const dialog = document.createElement('div'); @@ -42,11 +42,10 @@ const zoomInButton = document.createElement('button'); zoomInButton.type = 'button'; - zoomInButton.classList.add('btn', 'zoom-in'); - zoomInButton.ariaLabel = 'zoom in'; + zoomInButton.classList.add('btn', 'btn-outline-secondary', 'btn-sm', 'zoom-in'); + zoomInButton.setAttribute('aria-label', 'zoom in'); const zoomInSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - zoomInSVG.classList.add('bi', 'bi-zoom-in'); zoomInSVG.setAttribute('width', 16); zoomInSVG.setAttribute('height', 16); zoomInSVG.setAttribute('fill', 'currentColor'); @@ -54,26 +53,31 @@ zoomInSVG.setAttribute('aria-hidden', true); const zoomInPath1 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); zoomInPath1.setAttribute('fill-rule', 'evenodd'); - zoomInPath1.setAttribute('d', - 'M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z'); + zoomInPath1.setAttribute( + 'd', + 'M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z' + ); const zoomInPath2 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - zoomInPath2.setAttribute('d', + zoomInPath2.setAttribute( + 'd', 'M10.344 11.742c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 ' + - '1.007 0 0 0-.115-.1 6.538 6.538 0 0 1-1.398 1.4z'); + '1.007 0 0 0-.115-.1 6.538 6.538 0 0 1-1.398 1.4z' + ); const zoomInPath3 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); zoomInPath3.setAttribute('fill-rule', 'evenodd'); - zoomInPath3.setAttribute('d', + zoomInPath3.setAttribute( + 'd', 'M6.5 3a.5.5 0 0 1 .5.5V6h2.5a.5.5 0 0 1 0 1H7v2.5a.5.5 0 0 1-1 0V7H3.5a.5.5 0 0 1 ' + - '0-1H6V3.5a.5.5 0 0 1 .5-.5z'); + '0-1H6V3.5a.5.5 0 0 1 .5-.5z' + ); zoomInSVG.append(zoomInPath1, zoomInPath2, zoomInPath3); const zoomOutButton = document.createElement('button'); zoomOutButton.type = 'button'; - zoomOutButton.classList.add('btn', 'zoom-in'); - zoomOutButton.ariaLabel = 'zoom in'; + zoomOutButton.classList.add('btn', 'btn-outline-secondary', 'btn-sm', 'zoom-in'); + zoomOutButton.setAttribute('aria-label', 'zoom in'); const zoomOutSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - zoomOutSVG.classList.add('bi', 'bi-zoom-out'); zoomOutSVG.setAttribute('width', 16); zoomOutSVG.setAttribute('height', 16); zoomOutSVG.setAttribute('fill', 'currentColor'); @@ -81,16 +85,19 @@ zoomOutSVG.setAttribute('aria-hidden', true); const zoomOutPath1 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); zoomOutPath1.setAttribute('fill-rule', 'evenodd'); - zoomOutPath1.setAttribute('d', - 'M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z'); + zoomOutPath1.setAttribute( + 'd', + 'M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z' + ); const zoomOutPath2 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - zoomOutPath2.setAttribute('d', + zoomOutPath2.setAttribute( + 'd', 'M10.344 11.742c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 ' + - '0 0 0-.115-.1 6.538 6.538 0 0 1-1.398 1.4z'); + '0 0 0-.115-.1 6.538 6.538 0 0 1-1.398 1.4z' + ); const zoomOutPath3 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); zoomOutPath3.setAttribute('fill-rule', 'evenodd'); - zoomOutPath3.setAttribute('d', - 'M3 6.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5z'); + zoomOutPath3.setAttribute('d', 'M3 6.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5z'); zoomOutSVG.append(zoomOutPath1, zoomOutPath2, zoomOutPath3); const dragHandle = document.createElement('span'); @@ -101,7 +108,7 @@ closeButton.type = 'button'; closeButton.classList.add('btn-close'); closeButton.dataset.bsDismiss = 'modal'; - closeButton.ariaLabel = 'close'; + closeButton.setAttribute('aria-label', 'close'); const body = document.createElement('div'); body.classList.add('modal-body'); @@ -126,8 +133,8 @@ const svg = body.querySelector('svg'); const viewBoxDims = svg.viewBox.baseVal; // This assumes the units of the view box dimensions are points. - naturalWidth = viewBoxDims.width * 4 / 3; - naturalHeight = viewBoxDims.height * 4 / 3; + naturalWidth = (viewBoxDims.width * 4) / 3; + naturalHeight = (viewBoxDims.height * 4) / 3; } const headerHeight = header.offsetHeight; @@ -137,8 +144,8 @@ let maxHeight = window.innerHeight - headerHeight - 18; // Dialog maximum width and height - dialog.style.maxWidth = (maxWidth + 18) + 'px'; - dialog.style.maxHeight = (maxHeight + headerHeight + 18) + 'px'; + dialog.style.maxWidth = maxWidth + 18 + 'px'; + dialog.style.maxHeight = maxHeight + headerHeight + 18 + 'px'; // Initial image width and height let width = naturalWidth; @@ -169,17 +176,17 @@ // Determine the width and height after applying the zoom factor. if (factor * width > maxWidth || factor * height > maxHeight) { width = maxWidth; - height = width * naturalHeight / naturalWidth; + height = (width * naturalHeight) / naturalWidth; if (height > maxHeight) { height = maxHeight; - width = height * naturalWidth / naturalHeight; + width = (height * naturalWidth) / naturalHeight; } } else if (factor * width < 100 || factor * height < 100) { width = 100; - height = width * naturalHeight / naturalWidth; + height = (width * naturalHeight) / naturalWidth; if (height < 100) { height = 100; - width = height * naturalWidth / naturalHeight; + width = (height * naturalWidth) / naturalHeight; } } else { width = factor * width; @@ -189,8 +196,8 @@ // Resize the modal body.style.width = width + 'px'; body.style.height = height + 'px'; - dialog.style.width = (width + 18) + 'px'; - dialog.style.height = (height + headerHeight + 18) + 'px'; + dialog.style.width = width + 18 + 'px'; + dialog.style.height = height + headerHeight + 18 + 'px'; // Re-position the modal. if (initial) { @@ -199,8 +206,6 @@ } else { repositionModal(left - (width - initialWidth) / 2, top - (height - initialHeight) / 2); } - - dialog.focus(); }; // Make the dialog draggable @@ -218,22 +223,29 @@ dragHandle.addEventListener('pointermove', imageViewDrag); dragHandle.setPointerCapture(e.pointerId); - dragHandle.addEventListener('lostpointercapture', (e) => { - e.preventDefault(); - dragHandle.removeEventListener('pointermove', imageViewDrag); - }, { once: true }); - + dragHandle.addEventListener( + 'lostpointercapture', + (e) => { + e.preventDefault(); + dragHandle.removeEventListener('pointermove', imageViewDrag); + }, + { once: true } + ); }); // Set up the zoom in and zoom out click handlers. - zoomInButton.addEventListener('click', () => { zoomInButton.blur(); zoom(1.25); }); - zoomOutButton.addEventListener('click', () => { zoomOutButton.blur(); zoom(0.8); }); + zoomInButton.addEventListener('click', () => { + zoom(1.25); + }); + zoomOutButton.addEventListener('click', () => { + zoom(0.8); + }); onWinResize = () => { maxWidth = window.innerWidth - 18; maxHeight = window.innerHeight - headerHeight - 18; - dialog.style.maxWidth = (maxWidth + 18) + 'px'; - dialog.style.maxHeight = (maxHeight + headerHeight + 18) + 'px'; + dialog.style.maxWidth = maxWidth + 18 + 'px'; + dialog.style.maxHeight = maxHeight + headerHeight + 18 + 'px'; // Update the dialog position and size zoom(1); @@ -252,6 +264,14 @@ height = naturalHeight; zoom(1); } + + const moveUnit = e.ctrlKey ? 50 : e.shiftKey ? 1 : 10; + if (e.shiftKey && ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) + e.preventDefault(); + if (e.key === 'ArrowLeft') repositionModal(dialog.offsetLeft - moveUnit, dialog.offsetTop); + if (e.key === 'ArrowRight') repositionModal(dialog.offsetLeft + moveUnit, dialog.offsetTop); + if (e.key === 'ArrowUp') repositionModal(dialog.offsetLeft, dialog.offsetTop - moveUnit); + if (e.key === 'ArrowDown') repositionModal(dialog.offsetLeft, dialog.offsetTop + moveUnit); }); // The mouse wheel zooms in and out also. @@ -278,7 +298,7 @@ bsModal.show(); }; - const keyHandler = function(e) { + const keyHandler = function (e) { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); imageViewDialog.call(this); diff --git a/htdocs/js/ImageView/imageview.scss b/htdocs/js/ImageView/imageview.scss new file mode 100644 index 0000000000..b0b3f51294 --- /dev/null +++ b/htdocs/js/ImageView/imageview.scss @@ -0,0 +1,67 @@ +.image-view-elt { + max-width: 100%; + + &:hover { + cursor: pointer; + } + + &.top { + vertical-align: text-top; + } + + &.middle { + vertical-align: middle; + } + + &.bottom { + vertical-align: baseline; + } +} + +.image-view-dialog { + &.modal { + padding: 0 !important; + } + + .modal-dialog { + margin: 0; + } + + .modal-body { + overflow: unset; + padding: 8px; + text-align: center; + box-sizing: content-box !important; + + img { + max-width: 100%; + height: 100%; + } + + svg { + overflow: visible; + } + } + + .modal-header { + padding: 0.1rem 0.5rem; + + .drag-handle { + cursor: pointer; + width: 100%; + height: 26px; + touch-action: none; + } + + .btn { + padding: 0 0.2rem; + margin: 0 0.25rem 0 0; + border: none; + } + + .btn-close { + padding: 0.25rem 0.25rem; + margin: -0.5rem 0 -0.5rem auto; + } + } +} diff --git a/htdocs/js/InputColor/color.js b/htdocs/js/InputColor/color.js deleted file mode 100644 index bd079acd6f..0000000000 --- a/htdocs/js/InputColor/color.js +++ /dev/null @@ -1,66 +0,0 @@ -// For coloring the input elements with the proper color based on whether they are correct or incorrect. - -(() => { - const setupAnswerLink = (answerLink) => { - const answerId = answerLink.dataset.answerId; - const answerInput = document.getElementById(answerId); - - const type = answerLink.parentNode.classList.contains('ResultsWithoutError') ? 'correct' : 'incorrect'; - const radioGroups = {}; - - // Color all of the inputs and selects associated with this answer. On the first pass radio inputs are - // collected into groups by name, and on the second pass the checked radio is highlighted, or if none are - // checked all are highlighted. - document.querySelectorAll(`input[name*=${answerId}],select[name*=${answerId}`) - .forEach((input) => { - if (input.type.toLowerCase() === 'radio') { - if (!radioGroups[input.name]) radioGroups[input.name] = []; - radioGroups[input.name].push(input); - } else { - input.classList.add(type); - } - }); - - Object.values(radioGroups).forEach((group) => { - if (group.every((radio) => { - if (radio.checked) { - radio.classList.add(type); - return false; - } - return true; - })) { - group.forEach((radio) => radio.classList.add(type)); - } - }); - - if (answerInput) { - answerLink.addEventListener('click', (e) => { - e.preventDefault(); - answerInput.focus(); - }); - } else { - answerLink.href = ''; - } - }; - - // Color inputs already on the page. - document.querySelectorAll('td a[data-answer-id]').forEach(setupAnswerLink); - - // Deal with inputs that are added to the page later. - const observer = new MutationObserver((mutationsList) => { - mutationsList.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if (node instanceof Element) { - if (node.type && node.type.toLowerCase() === 'td' && node.firstElementChild - && node.firstElementChild.type.toLowerCase() == 'a' && node.firstElementChild.dataset.answerId) - setupAnswerLink(node.firstElementChild); - else node.querySelectorAll('td a[data-answer-id]').forEach(setupAnswerLink); - } - }); - }); - }); - observer.observe(document.body, { childList: true, subtree: true }); - - // Stop the mutation observer when the window is closed. - window.addEventListener('unload', () => observer.disconnect()); -})(); diff --git a/htdocs/js/Knowls/knowl.css b/htdocs/js/Knowls/knowl.css deleted file mode 100644 index 0c65d3aae1..0000000000 --- a/htdocs/js/Knowls/knowl.css +++ /dev/null @@ -1,99 +0,0 @@ -.knowl { - display: inline; - border-bottom: 2px dotted #00a; - color: #00a; - cursor: pointer; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - margin: 0; - padding: 0 2px; -} - -.knowl-container { - margin-top: 1rem; - margin-bottom: 1rem; -} - -.knowl:hover, -.knowl.active { - border-bottom: 2px solid #aaf; - background: #ddf; - color: #006; - text-decoration: none; -} - -.knowl.active:hover { - background: #cce; -} - -div > .knowl, p > .knowl { - position: relative; -} - -.knowl-content { - padding: 10px; - border-bottom-left-radius: 10px; -} - -.knowl-content h1 { - margin: 0 0 10px 0; -} - -.knowl-content h2 { - margin: 0 0 5px 0; -} - -.knowl-content p { - margin-bottom: 0; - margin-top: 10px; -} - -.knowl-output { - background: #eef; - border: 10px solid #ddf; - border-radius: 10px; - padding: 0; - margin-top: 10px; - margin-bottom: 0; - margin-right: 0; -} - -.knowl-output h1, .knowl-output h2 { - margin: 5px 0; -} - -.knowl-output h1 { - color: #006; -} - -.knowl-output h2 { - color: #006; -} - -.knowl-output a { - display: inline; -} - -.knowl-error { - color: darkred; - border-bottom: 0; -} - -.knowl-footer { - position: relative; - bottom: -10px; - font-size: x-small; - background: #ddf; - color: #555; - padding: 4px 0 4px 10px; - margin: -10px 0 0 0; -} - -.knowl-footer a { - color: #006; -} - -.knowl-footer a:hover { - background: none; - color: #88f; -} diff --git a/htdocs/js/Knowls/knowl.js b/htdocs/js/Knowls/knowl.js index 21e25532e4..496b42a7f6 100644 --- a/htdocs/js/Knowls/knowl.js +++ b/htdocs/js/Knowls/knowl.js @@ -8,116 +8,111 @@ elt.innerHTML = html; elt.querySelectorAll('script').forEach((origScript) => { const newScript = document.createElement('script'); - Array.from(origScript.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value)); + Array.from(origScript.attributes).forEach((attr) => newScript.setAttribute(attr.name, attr.value)); newScript.appendChild(document.createTextNode(origScript.innerHTML)); origScript.parentNode.replaceChild(newScript, origScript); }); }; const initializeKnowl = (knowl) => { - if (getComputedStyle(knowl)?.display === '') { - setTimeout(() => initializeKnowl(knowl), 100); - return; - } - - knowl.dataset.bsToggle = 'collapse'; - if (!knowl.knowlContainer) { - knowl.knowlContainer = document.createElement('div'); - knowl.knowlContainer.id = `knowl-uid-${knowlUID++}`; - knowl.knowlContainer.classList.add('collapse'); + knowl.dataset.bsToggle = 'modal'; + if (!knowl.knowlModal) { + knowl.knowlModal = document.createElement('div'); + knowl.knowlModal.id = `knowl-uid-${knowlUID++}`; + knowl.knowlModal.classList.add('modal', 'fade'); + knowl.knowlModal.tabIndex = -1; + knowl.knowlModal.setAttribute('aria-labelledby', `${knowl.knowlModal.id}-title`); + knowl.knowlModal.setAttribute('aria-hidden', 'true'); - const knowlOutput = document.createElement('div'); - knowlOutput.classList.add('knowl-output'); + const knowlDialog = document.createElement('div'); + knowlDialog.classList.add( + 'knowl-dialog', + 'modal-dialog', + 'modal-dialog-centered', + 'modal-dialog-scrollable' + ); + knowlDialog.dataset.iframeHeight = '1'; + knowl.knowlModal.append(knowlDialog); const knowlContent = document.createElement('div'); - knowlContent.classList.add('knowl-content'); - knowlOutput.append(knowlContent); + knowlContent.classList.add('modal-content'); + knowlDialog.append(knowlContent); + + const knowlHeader = document.createElement('div'); + knowlHeader.classList.add('modal-header'); + + const knowlTitle = document.createElement('h1'); + knowlTitle.classList.add('modal-title', 'fs-5'); + knowlTitle.id = `${knowl.knowlModal.id}-title`; + knowlTitle.textContent = knowl.dataset.knowlTitle || knowl.textContent; + + const closeButton = document.createElement('button'); + closeButton.type = 'button'; + closeButton.classList.add('btn-close'); + closeButton.dataset.bsDismiss = 'modal'; + closeButton.setAttribute('aria-label', 'Close'); + + knowlHeader.append(knowlTitle, closeButton); + + const knowlBody = document.createElement('div'); + knowlBody.classList.add('modal-body'); + + knowlContent.append(knowlHeader, knowlBody); if (knowl.dataset.knowlUrl) { const knowlFooter = document.createElement('div'); - knowlFooter.classList.add('knowl-footer'); + knowlFooter.classList.add('modal-footer', 'knowl-footer', 'justify-content-center', 'p-1'); knowlFooter.textContent = knowl.dataset.knowlUrl; - knowlOutput.append(knowlFooter); + knowlContent.append(knowlFooter); } - knowl.knowlContainer.appendChild(knowlOutput); - - knowl.knowlContainer.addEventListener('show.bs.collapse', () => knowl.classList.add('active')); - knowl.knowlContainer.addEventListener('hide.bs.collapse', () => knowl.classList.remove('active')); - - // If the knowl is inside a table row, then insert a new row into the table after that one to contain - // the knowl content. If the knowl is inside a list element, then insert the content after the list - // element. Otherwise insert the content either before the first sibling that follows it that is - // display block, or append it to the first ancestor that is display block. - let insertElt = knowl.closest('tr'); - if (insertElt) { - const row = document.createElement('tr'); - const td = document.createElement('td'); - td.colSpan = insertElt.childElementCount; - td.appendChild(knowl.knowlContainer); - row.appendChild(td); - insertElt.after(row); - } else { - insertElt = knowl.closest('li'); - if (insertElt) { - const newDiv = document.createElement('div'); - newDiv.append(knowl.knowlContainer); - insertElt.append(newDiv); - } else { - let append = false; - insertElt = knowl; - do { - const lastElt = insertElt; - insertElt = lastElt.nextElementSibling; - if (!insertElt) { - insertElt = lastElt.parentNode; - append = true; - } - } while (getComputedStyle(insertElt)?.getPropertyValue('display') !== 'block'); + knowl.knowlModal.addEventListener('shown.bs.modal', () => { + const heightAdjust = Math.min( + 600, + knowlBody.scrollHeight + + knowlHeader.offsetHeight + + (knowlContent.querySelector('.modal-footer')?.offsetHeight || 0) + ); + if (knowlDialog.offsetHeight < heightAdjust) knowlDialog.style.height = `${heightAdjust}px`; + }); - if (append) insertElt.append(knowl.knowlContainer); - else insertElt.before(knowl.knowlContainer); - } - } + document.body.append(knowl.knowlModal); - knowl.dataset.bsTarget = `#${knowl.knowlContainer.id}`; + knowl.dataset.bsTarget = `#${knowl.knowlModal.id}`; if (knowl.dataset.knowlContents) { // Inline html - if (knowl.dataset.base64 == '1') { - if (window.Base64) - setInnerHTML(knowlContent, Base64.decode(knowl.dataset.knowlContents)); - else { - setInnerHTML(knowlContent, 'ERROR: Base64 decoding not available'); - knowlContent.classList.add('knowl-error'); - } - } else { - setInnerHTML(knowlContent, knowl.dataset.knowlContents); - } + setInnerHTML(knowlBody, knowl.dataset.knowlContents); + // If we are using MathJax, then render math content. if (window.MathJax) { - MathJax.startup.promise = - MathJax.startup.promise.then(() => MathJax.typesetPromise([knowlContent])); + MathJax.startup.promise = MathJax.startup.promise.then(() => MathJax.typesetPromise([knowlBody])); } } else if (knowl.dataset.knowlUrl) { // Retrieve url content. - fetch(knowl.dataset.knowlUrl).then((response) => response.ok ? response.text() : response) + fetch(knowl.dataset.knowlUrl) + .then((response) => (response.ok ? response.text() : response)) .then((data) => { if (typeof data == 'object') { - knowlContent.textContent = `ERROR: ${data.status} ${data.statusText}`; - knowlContent.classList.add('knowl-error'); + knowlBody.textContent = `ERROR: ${data.status} ${data.statusText}`; + knowlBody.classList.add('knowl-error'); } else { - setInnerHTML(knowlContent, data); + setInnerHTML(knowlBody, data); } // If we are using MathJax, then render math content. if (window.MathJax) { - MathJax.startup.promise = - MathJax.startup.promise.then(() => MathJax.typesetPromise([knowlContent])); + MathJax.startup.promise = MathJax.startup.promise.then(() => + MathJax.typesetPromise([knowlBody]) + ); } + }) + .catch((err) => { + knowlBody.textContent = `ERROR: ${err}`; + knowlBody.classList.add('knowl-error'); }); } else { - knowlContent.textContent = 'ERROR: knowl content not provided.'; - knowlContent.classList.add('knowl-error'); + knowlBody.textContent = 'ERROR: knowl content not provided.'; + knowlBody.classList.add('knowl-error'); } } }; diff --git a/htdocs/js/Knowls/knowl.scss b/htdocs/js/Knowls/knowl.scss new file mode 100644 index 0000000000..3fb0671691 --- /dev/null +++ b/htdocs/js/Knowls/knowl.scss @@ -0,0 +1,43 @@ +.knowl { + color: #00a; + background-color: #eef; + border: 1px solid #88f; + border-radius: 3px; + cursor: pointer; + + &:hover { + color: #006; + background-color: #ccf; + border-color: #33f; + } + + &:focus-visible { + border-color: #33f; + box-shadow: 0px 0px 0px 0.2rem #5555ff88; + outline: 0; + } +} + +li > .knowl { + margin: 0.2rem 0; +} + +.knowl-error { + color: darkred; +} + +.knowl-footer { + font-size: x-small; + background: #eef; + color: #555; +} + +// MathJax sets the z-index to 200 which is far below a modal dialog at 1055. So raise that above the modal dialog. +// This is really a bug in MathJax. MathJax should handle this differently. +.CtxtMenu_MenuFrame { + z-index: 1060 !important; + + .CtxtMenu_Menu { + z-index: 1060; + } +} diff --git a/htdocs/js/LiveGraphics/liveGraphics.js b/htdocs/js/LiveGraphics/liveGraphics.js index 6e6ce88e88..75daa7cfce 100644 --- a/htdocs/js/LiveGraphics/liveGraphics.js +++ b/htdocs/js/LiveGraphics/liveGraphics.js @@ -1,913 +1,547 @@ -// liveGraphics.js -// This is a javascript based replacement for the LiveGraphics3D java library -// -// 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. - -var LiveGraphics3D = function (container, options) { - var my = this; - - // define x3d container and scene - var x3d = $("").appendTo(container) - .css('width',options.width+'px') - .css('height',options.height+'px') - .css('border','none') - .css('overflow','hidden') - .attr('swfpath','/webwork2_files/js/vendor/x3dom/x3dom.swf'); - - $("
                      ").addClass('sr-only') - .text('A manipulable 3d graph.') - .prependTo(container); - - // disable mousewheel on container because its used for zoom - $(x3d).bind('DOMMouseScroll mousewheel',function(event) { - event.preventDefault(); - }); - - var scene = $("").appendTo(x3d); - - // extend options by default values - var defaults = { - width : 200, - height : 200, - // Controls if axis are shown or not - showAxes : false, - // If the axis are shown determines if a full cube is drawn or just - // the three axis lines - showAxesCube : true, - numTicks : 4, - tickSize : .1, - tickFontSize : .15, - axisKey : ['X','Y','Z'], - // Determines if the polygons forming the surface have their edges - // drawn - drawMesh : true, - }; - - var options = $.extend({}, defaults, options); - - //global variables - //arrays of colors and thicknesses drawn from input - var colors = {}; - var lineThickness = {}; - - //scale elements capturing scale of plotted data - var windowScale; - var coordMins; - var coordMaxs; - - //block indexes are used to associate objects to colors and thicknesses - var blockIndex = 0; - var surfaceBlockIndex = 0; - - //data from input - var surfaceCoords = []; - var surfaceIndex = []; - var lineCoords = []; - var lonePoints = []; - var loneLabels = []; - - // This is the color map for shading surfaces based on elevation - var colormap = [ - [0.00000, 0.00000, 0.50000], - [0.00000, 0.00000, 0.56349], - [0.00000, 0.00000, 0.62698], - [0.00000, 0.00000, 0.69048], - [0.00000, 0.00000, 0.75397], - [0.00000, 0.00000, 0.81746], - [0.00000, 0.00000, 0.88095], - [0.00000, 0.00000, 0.94444], - [0.00000, 0.00794, 1.00000], - [0.00000, 0.07143, 1.00000], - [0.00000, 0.13492, 1.00000], - [0.00000, 0.19841, 1.00000], - [0.00000, 0.26190, 1.00000], - [0.00000, 0.32540, 1.00000], - [0.00000, 0.38889, 1.00000], - [0.00000, 0.45238, 1.00000], - [0.00000, 0.51587, 1.00000], - [0.00000, 0.57937, 1.00000], - [0.00000, 0.64286, 1.00000], - [0.00000, 0.70635, 1.00000], - [0.00000, 0.76984, 1.00000], - [0.00000, 0.83333, 1.00000], - [0.00000, 0.89683, 1.00000], - [0.00000, 0.96032, 1.00000], - [0.02381, 1.00000, 0.97619], - [0.08730, 1.00000, 0.91270], - [0.15079, 1.00000, 0.84921], - [0.21429, 1.00000, 0.78571], - [0.27778, 1.00000, 0.72222], - [0.34127, 1.00000, 0.65873], - [0.40476, 1.00000, 0.59524], - [0.46825, 1.00000, 0.53175], - [0.53175, 1.00000, 0.46825], - [0.59524, 1.00000, 0.40476], - [0.65873, 1.00000, 0.34127], - [0.72222, 1.00000, 0.27778], - [0.78571, 1.00000, 0.21429], - [0.84921, 1.00000, 0.15079], - [0.91270, 1.00000, 0.08730], - [0.97619, 1.00000, 0.02381], - [1.00000, 0.96032, 0.00000], - [1.00000, 0.89683, 0.00000], - [1.00000, 0.83333, 0.00000], - [1.00000, 0.76984, 0.00000], - [1.00000, 0.70635, 0.00000], - [1.00000, 0.64286, 0.00000], - [1.00000, 0.57937, 0.00000], - [1.00000, 0.51587, 0.00000], - [1.00000, 0.45238, 0.00000], - [1.00000, 0.38889, 0.00000], - [1.00000, 0.32540, 0.00000], - [1.00000, 0.26190, 0.00000], - [1.00000, 0.19841, 0.00000], - [1.00000, 0.13492, 0.00000], - [1.00000, 0.07143, 0.00000], - [1.00000, 0.00794, 0.00000], - [0.94444, 0.00000, 0.00000], - [0.88095, 0.00000, 0.00000], - [0.81746, 0.00000, 0.00000], - [0.75397, 0.00000, 0.00000], - [0.69048, 0.00000, 0.00000], - [0.62698, 0.00000, 0.00000], - [0.56349, 0.00000, 0.00000], - [0.50000, 0.00000, 0.00000]]; - - // intialization function. This takes the mathmatica data string - // and actually sets up the dom structure for the graph. - // the actual graphing is done automatically by x3dom - var initialize = function (datastring) { - - // parse matlab string - parseLive3DData(datastring); - - // find extremum for axis and window scale - setExtremum(); - - // set up scene veiwpoint to be along the x axis looking to the - // origin - scene.append($(""). - attr('rotation',[1,0,0,Math.PI/2]) - .append($("") - .attr( "fieldofview", .9) - .attr( "position", [2*windowScale,0,0] ) - .attr( "orientation", [0,1,0,Math.PI/2]))); - - scene.append($('').attr('skycolor','1 1 1')); - - // draw components of scene - if (options.showAxes) { - drawAxes(); - } - - drawSurface(); - drawLines(); - drawLonePoints(); - drawLoneLabels(); - - }; - - var parseLive3DData = function(text) { - // Set up variables - $.each(options.vars, function (name, data) { - eval(name+'='+data); - }); +'use strict'; + +(() => { + const liveGraphics3D = (container) => { + const options = JSON.parse(container.dataset.options); + + const width = options.width || 200; + const height = options.height || 200; + + const maxTicks = options.maxTicks instanceof Array ? options.maxTicks : Array(3).fill(options.maxTicks ?? 6); + while (maxTicks.length < 3) maxTicks.push(6); + + const screenReaderOnly = document.createElement('span'); + screenReaderOnly.classList.add('visually-hidden'); + screenReaderOnly.textContent = 'A manipulable 3d graph.'; + container.append(screenReaderOnly); + + // General options that can be overriden by settings in the input data. + let lighting = true; + let showAxes = false; + let axesLabels = ['x', 'y', 'z']; + + // Inital view point and up vector. + const eye = { x: 1.25, y: 1.25, z: 1.25 }; + const up = { x: 0, y: 0, z: 1 }; + + // Initial graphics state. This is pushed onto the state stack when a block at depth 0 is executed. At each + // successive block depth this state is copied and pushed onto the state stack. The options in the state apply + // to all graphics primitives in that block. The state can be changed within a block by the RGBColor, + // Thickness, and GrayLevel commands. All graphics primitives in the block after the change are affected. Also + // note that if these are called inside an EdgeForm command, then the edge options are changed instead. + const initialState = { + color: null, + lineThickness: null, + edgeColor: 'black', + edgeThickness: 0.001, + pointSize: 0.01, + edgeForm: false, + drawEdges: true + }; + const state = []; + + // The block index is used to group parts of surfaces, lines, and edges in the same block. + let blockIndex = 0; + + // Data from input (translated into plotly traces). + const surfaces = {}; + const edges = {}; + const lines = {}; + const points = []; + const labels = []; + + let variables = ''; + + const executeCommand = (command) => { + const currentState = state.slice(-1)[0]; + + if (command.id === 'Point') { + if (command.blocks.length !== 1 || command.blocks[0].length < 3) { + console.log('Error parsing point: A point must have three coordinates.'); + return; + } - // this parses axes commands. - if (text.match(/Axes\s*->\s*True/)) { - options.showAxes = true; - } - - // get some initial global configuration - var labels = text.match(/AxesLabel\s*->\s*\{\s*(\w+),\s*(\w+),\s*(\w+)\s*\}/); - - if (labels) { - options.axisKey = [labels[1],labels[2],labels[3]]; - } - - // split the input into blocks and parse - var blocks = recurseMathematicaBlocks(text); - - parseMathematicaBlocks(blocks); - - }; - - // find max and min of all mesh coordinate points and - // the maximum coordinate value for the scale. - var setExtremum = function () { - var min = [0,0,0]; - var max = [0,0,0]; - - surfaceCoords.forEach(function(point) { - for (var i=0; i< 3; i++) { - if (point[i] < min[i]) { - min[i] = point[i]; - } else if (point[i]>max[i]) { - max[i] = point[i]; - } - } - }); - - lineCoords.forEach(function(line) { - for (var i=0; i<2; i++) { - for (var j=0; j<3; j++) { - if (line[i][j] < min[j]) { - min[j] = line[i][j]; - } else if (line[i][j]>max[j]) { - max[j] = line[i][j]; - } - } - } - }); - coordMins = min; - coordMaxs = max; - - var sum = 0; - - for (var i=0; i< 3; i++) { - sum += max[i]-min[i]; - } - - windowScale = sum/3; - }; - - var drawLines = function() { - if (lineCoords.length==0) { - return; - } - - // Add surface to scene as an indexedfaceset - - var linegroup = $(''); - - lineCoords.forEach(function(line){ - - // lines are cylinders that start centered at the origin - // along the y axis. We have to translate and rotate them - // into place - var length = Math.sqrt(Math.pow((line[0][0]-line[1][0]),2)+ - Math.pow((line[0][1]-line[1][1]),2)+ - Math.pow((line[0][2]-line[1][2]),2)); - var rotation = []; - - if (length == 0) { - return; - } - - rotation[0] = (line[1][2]-line[0][2]); - rotation[1] = 0; - rotation[2] = (line[0][0]-line[1][0]); - rotation[3] = Math.acos((line[1][1]-line[0][1])/length); - - var trans = [0,0,0]; - - for (var i=0; i < 3; i++) { - trans[i] = (line[1][i] + line[0][i])/2; - } - - var shape = $("").appendTo($("") - .attr('translation',trans) - .attr('rotation',rotation) - .appendTo(linegroup)); - var color = [0,0,0]; - var radius = .005; - - // line[2] contains the block index - if (line[2] in colors) { - color = colors[line[2]]; - } - - if (line[2] in lineThickness) { - radius = Math.max(lineThickness[line[2]],.005); - } - - $("").appendTo(shape) - .append($("") - .attr('diffusecolor',color)); - - shape.append($("") - .attr("height", length) - .attr("radius", radius*2)); - }); - - scene.append(linegroup); - } - - var drawSurface = function() { - var coordstr = ''; - var indexstr = ''; - var colorstr = ''; - var colorindstr = ''; - - if (surfaceCoords.length == 0) { - return; - } - - // build a string with all the surface coodinates - surfaceCoords.forEach(function(point) { - coordstr += point[0]+' '+point[1]+' '+point[2]+' '; - }); - - // build a string with all the surface indexes - // at the same time build a string with color data - // and the associated color indexes - surfaceIndex.forEach(function(index) { - indexstr += index+' '; - - if (index == -1) { - colorindstr += '-1 '; - return; - } - - var cindex = parseInt((surfaceCoords[index][2]-coordMins[2])/(coordMaxs[2]-coordMins[2])*colormap.length); - - if (cindex == colormap.length) { - cindex--; - } - - colorindstr += cindex+' '; - - }); - - colormap.forEach(function(color) { - for (var i=0; i<3; i++) { - color[i] += .2; - color[i] = Math.min(color[i],1); - } - - colorstr += color[0]+' '+color[1]+' '+color[2]+' '; - }); - - var flatcolor = false; - var color = []; - - if (surfaceBlockIndex in colors) { - flatcolor = true; - color = colors[surfaceBlockIndex]; - } - - // Add surface to scene as an indexedfaceset - var shape = $("").appendTo(scene); - - var appearance = $("").appendTo(shape); - - appearance .append($("") - .attr("ambientIntensity",'0') - .attr('convex','false') - .attr('creaseangle',Math.PI) - .attr('diffusecolor',color) - .attr("shininess",".015")); - - var indexedfaceset = $("") - .attr('coordindex',indexstr) - .attr('solid','false'); - - indexedfaceset.append($("") - .attr('point',coordstr)); - - if (!flatcolor) { - indexedfaceset.attr('colorindex',colorindstr); - indexedfaceset.append($("") - .attr('color',colorstr)); - } - - // append the indexed face set to the shape after its assembled. - // otherwise sometimes x3d tries to access the various data before - // its ready - indexedfaceset.appendTo(shape); - - if (options.drawMesh) { - - shape = $("").appendTo(scene); - - appearance = $("").appendTo(shape); - - appearance .append($("") - .attr('diffusecolor',[0,0,0])); - - var indexedlineset = $("") - .attr('coordindex',indexstr) - .attr('solid','true'); - - indexedlineset.append($("") - .attr('point',coordstr)); - - indexedlineset.appendTo(shape); - } - - } - - var drawAxes = function() { - - // build x axis and add the ticks. - // all of this is done in two dimensions and then rotated and shifted - // into place - var xgroup = $("").appendTo($("") - .appendTo(scene) - .attr('translation',[0,coordMins[1],coordMins[2]])); - - var xaxis = $("").append($("") - .append($("") - .attr("emissiveColor", 'black') - )) - .append($("") - .attr("lineSegments", coordMins[0]+' 0 '+coordMaxs[0]+' 0')); - xgroup.append(xaxis); - - $.each(makeAxisTicks(0),function() { - this.appendTo(xgroup)}); - - if (options.showAxesCube) { - - var trans = [[0,coordMins[1],coordMaxs[2]], - [0,coordMaxs[1],coordMins[2]], - [0,coordMaxs[1],coordMaxs[2]]]; - - trans.forEach(function (tran) { - $("").attr('translation',tran) - .appendTo(scene) - .append($("").append($("") - .append($("") - .attr("emissiveColor", 'black') - )) - .append($("") - .attr("lineSegments", coordMins[0]+' 0 '+coordMaxs[0]+' 0'))); - }); - } - - // build y axis and add the ticks - var ygroup = $("").appendTo($("") - .appendTo(scene) - .attr('translation',[coordMins[0],0,coordMins[2]]) - .attr('rotation',[0,0,1,Math.PI/2])); - - var yaxis = $("").append($("") - .append($("") - .attr("emissiveColor", 'black') - )) - .append($("") - .attr("lineSegments", coordMins[1]+' 0 '+coordMaxs[1]+' 0')); - ygroup.append(yaxis); - - $.each(makeAxisTicks(1),function() { - this.appendTo(ygroup)}); - - if (options.showAxesCube) { - - var trans = [[coordMins[0],0,coordMaxs[2]], - [coordMaxs[0],0,coordMins[2]], - [coordMaxs[0],0,coordMaxs[2]]]; - - trans.forEach(function (tran) { - $("").attr('translation',tran) - .attr('rotation',[0,0,1,Math.PI/2]) - .appendTo(scene) - .append($("").append($("") - .append($("") - .attr("emissiveColor", 'black') - )) - .append($("") - .attr("lineSegments", coordMins[1]+' 0 '+coordMaxs[1]+' 0'))); - }); - } - - // build z axis and add the ticks - var zgroup = $("").appendTo($("") - .appendTo(scene) - .attr('translation',[coordMins[0],coordMins[1],0]) - .attr('rotation',[0,1,0,-Math.PI/2])); - - var zaxis = $("").append($("") - .append($("") - .attr("emissiveColor", 'black') - )) - .append($("") - .attr("lineSegments", coordMins[2]+' 0 '+coordMaxs[2]+' 0')); - - zgroup.append(zaxis); - - $.each(makeAxisTicks(2),function() { - this.appendTo(zgroup)}); - - if (options.showAxesCube) { - - var trans = [[coordMins[0],coordMaxs[1],0], - [coordMaxs[0],coordMins[1],0], - [coordMaxs[0],coordMaxs[1],0]]; - - trans.forEach(function (tran) { - $("").attr('translation',tran) - .attr('rotation',[0,1,0,-Math.PI/2]) - .appendTo(scene) - .append($("").append($("") - .append($("") - .attr("emissiveColor", 'black') - )) - .append($("") - .attr("lineSegments", coordMins[2]+' 0 '+coordMaxs[2]+' 0'))); - }); - } - - } - - // biuilds the ticks, the tick labels, and the axis label for - // axisindex I - var makeAxisTicks = function (I) { - var shapes = []; - - for(var i=0; i").append($($("") - .append($("") - .attr("diffuseColor","black")))); - tick.appendTo($("") - .attr('translation',[coord,0,0])); - - tick.append($("") - .attr('size', options.tickSize+' ' - +options.tickSize+' '+ - options.tickSize)); - - shapes.push(tick.parent()); - - // labels have two decimal places and always point towards view - var ticklabel = $("").append($($("") - .append($("") - .attr("diffuseColor","black")))); - - ticklabel.appendTo($("") - .attr("axisOfRotation", "0 0 0") - .appendTo($("") - .attr('translation',[coord,.1,0]))); - - ticklabel.append($("") - .attr('string',coord.toFixed(2)) - .attr('solid','true') - .append($("") - .attr('size',options.tickFontSize*windowScale) - .attr('family', "mono") - .attr('style', 'bold') - .attr('justify', 'MIDDLE'))); - - shapes.push(ticklabel.parent().parent()); - - } - - // axis label goes on the end of the axis. - var axislabel = $("").append($($("") - .append($("") - .attr("diffuseColor","black")))); - - axislabel.appendTo($("") - .attr("axisOfRotation", "0 0 0") - .appendTo($("") - .attr('translation',[coordMaxs[I],.1,0]))); - - axislabel.append($("") - .attr('string',options.axisKey[I]) - .attr('solid','true') - .append($("") - .attr('size',options.tickFontSize*windowScale) - .attr('family', "mono") - .attr('style', 'bold') - .attr('justify', 'MIDDLE'))); - - shapes.push(axislabel.parent().parent()); - - return shapes; - } - - var drawLonePoints = function () { - - lonePoints.forEach(function (point) { - - var color = 'black'; - if (point.rgb) { - color=point.rgb; - } - - // lone points are drawn as spheres so they have mass - var sphere = $("").append($($("") - .append($("") - .attr("diffuseColor",color)))); - sphere.appendTo($("") - .attr('translation',point.coords)); - - sphere.append($("") - .attr('radius',point.radius*2.25)); - - sphere.parent().appendTo(scene); - - }); - - } - - var drawLoneLabels = function () { - - loneLabels.forEach(function (label) { - - // the text is a billboard that automatically faces the user - var text = $("").append($($("") - .append($("") - .attr("diffuseColor",'black')))); - - text.appendTo($("") - .attr("axisOfRotation", "0 0 0") - .appendTo($("") - .attr('translation',label.coords))); - - var size = '.5'; - if (label.size) { - //mathematica label sizes are fontsizes, where - //the units for x3dom are local coord sizes - size = label.size/(1.5*windowScale); - } - - text.append($("") - .attr('string',label.text) - .attr('solid','true') - .append($("") - .attr('size',size) - .attr('family', "mono") - .attr('justify', 'MIDDLE'))); - - text.parent().parent().appendTo(scene); - - }); - - } - - var parseMathematicaBlocks = function (blocks) { - - blocks.forEach(function(block) { - blockIndex++; - - if (block.match(/^\s*\{/)) { - // This is a block inside of a block. - // so recurse - var subblocks = recurseMathematicaBlocks(block); - parseMathematicaBlocks(subblocks); - - } else if (block.match(/Point/)) { - // now find any individual points that need to be plotted - // points are defined by short blocks so we dont split into - // individual commands - var str = block.match(/Point\[\s*\{\s*(-?\d*\.?\d*)\s*,\s*(-?\d*\.?\d*)\s*,\s*(-?\d*\.?\d*)\s*\}/); - var point = {}; - - if (!str) { - console.log('Error Parsing Point'); - return; - } - - point.coords = [parseFloat(str[1]),parseFloat(str[2]),parseFloat(str[3])]; - - str = block.match(/PointSize\[\s*(\d*\.?\d*)\s*\]/); - - if (str) { - point.radius = parseFloat(str[1]); - } - - str = block.match(/RGBColor\[\s*(\d*\.?\d*)\s*,\s*(\d*\.?\d*)\s*,\s*(\d*\.?\d*)\s*\]/); - - if (str) { - point.rgb = [parseFloat(str[1]),parseFloat(str[2]),parseFloat(str[3])]; - } - - lonePoints.push(point); - - } else { - // Otherwise its a list of commands that we need to - // process individually - var commands = splitMathematicaBlocks(block); - - commands.forEach(function(command) { - if (command.match(/^\s*\{/)) { - // This is a block inside of a block. - // so recurse - var subblocks = recurseMathematicaBlocks(block); - parseMathematicaBlocks(subblocks); - } else if (command.match(/Polygon/)) { - if (!surfaceBlockIndex) { - surfaceBlockIndex = blockIndex; - } + // Points are implemented as the top and bottom half of a sphere. Note that using a marker in a + // scatter3d trace results in bad clipping since markers are really only two dimensional circles. + const point = { type: 'mesh3d', x: [], y: [], z: [], color: 'black', hoverinfo: 'none' }; + + const x = command.blocks[0][0], + y = command.blocks[0][1], + z = command.blocks[0][2]; + + const r = currentState.pointSize * 2.5; + + point.color = `rgb(${currentState.color[0] * 255},${currentState.color[1] * 255},${ + currentState.color[2] * 255 + })`; + + const samples = 20; + + const phiValues = Array(samples) + .fill(0) + .map((_v, i) => (i * (Math.PI / 2)) / (samples - 1)); + + const thetaValues = Array(samples) + .fill(0) + .map((_v, i) => (2 * i * Math.PI) / (samples - 1)); - var polystring = command.replace(/Polygon\[([^\]]*)\]/,"$1"); - var pointstrings = recurseMathematicaBlocks(polystring,-1); - // for each polygon extract all the points - pointstrings.forEach(function(pointstring) { - pointstring = pointstring.replace(/\{([^\{]*)\}/,"$1"); - - var splitstring = pointstring.split(','); - var point = []; - - for (var i=0; i < 3; i++) { - point[i] = parseFloat(eval(splitstring[i])); - } - - // find the index of the point in surfaceCoords. If - // the point is not in surfaceCoords, add it - for (var i=0; i 2 * z - v) }); + } else if (command.id === 'Polygon') { + if (command.blocks.length !== 1 || command.blocks[0].length < 3) { + console.log('Error parsing polygon: Polygons must have at least three points.'); + return; + } + + if (!surfaces[blockIndex]) { + surfaces[blockIndex] = { + type: 'mesh3d', + x: [], + y: [], + z: [], + i: [], + j: [], + k: [], + showscale: false, + hoverinfo: 'none' + }; + + if (!lighting) { + // Set the ambient lighting to the max, and disable all others. Ambient lighting is needed + // unless you just want a black blob for the surface. + surfaces[blockIndex].lighting = { + ambient: 1, + diffuse: 0, + fresnel: 0, + roughness: 0, + specular: 0 + }; + } + + if (currentState.color instanceof Array) { + surfaces[blockIndex].color = `rgb(${currentState.color[0] * 255},${ + currentState.color[1] * 255 + },${currentState.color[2] * 255})`; + } else { + surfaces[blockIndex].intensity = surfaces[blockIndex].z; + surfaces[blockIndex].colorscale = 'RdBu'; + } + } + + if (currentState.drawEdges && !edges[blockIndex]) { + edges[blockIndex] = { + type: 'scatter3d', + mode: 'lines', + x: [], + y: [], + z: [], + line: { width: currentState.edgeThickness * 1000, color: 'black' }, + hoverinfo: 'none' + }; + + if (currentState.edgeColor instanceof Array) { + edges[blockIndex].line.color = `rgb(${currentState.edgeColor[0] * 255},${ + currentState.edgeColor[1] * 255 + },${currentState.edgeColor[2] * 255})`; + } + } + + const polygonIndices = []; + + for (const point of command.blocks[0]) { + if (point.length !== 3) { + console.log('Error parsing polygon: Points must have three coordinates.'); + return; + } + + for (let i = 0; i < point.length; ++i) { + try { + point[i] = new Function(`'use strict'; { ${variables} return ${point[i]} }`)(); + } catch (e) { + console.log(`Failed to evaluate variable quantity in coordinate of point: ${point[i]}`); + return; + } + } + + // Find the index of the point in the surface x, y, z coordinate arrays. + // If the point is not in the arrays, then add it. + let pointIndex = 0; + for (; pointIndex < surfaces[blockIndex].x.length; ++pointIndex) { + if ( + surfaces[blockIndex].x[pointIndex] === point[0] && + surfaces[blockIndex].y[pointIndex] === point[1] && + surfaces[blockIndex].z[pointIndex] === point[2] + ) + break; + } + + if (pointIndex === surfaces[blockIndex].x.length) { + surfaces[blockIndex].x.push(point[0]); + surfaces[blockIndex].y.push(point[1]); + surfaces[blockIndex].z.push(point[2]); + } + + edges[blockIndex]?.x.push(point[0]); + edges[blockIndex]?.y.push(point[1]); + edges[blockIndex]?.z.push(point[2]); + + polygonIndices.push(pointIndex); + } + + // Split the polygon into triangle faces, and add the indices of the vertices of these + // triangles to the face index arrays. + for (let i = 1; i < polygonIndices.length - 1; ++i) { + surfaces[blockIndex].i.push(polygonIndices[0]); + surfaces[blockIndex].j.push(polygonIndices[i]); + surfaces[blockIndex].k.push(polygonIndices[i + 1]); + } + + edges[blockIndex]?.x.push('None'); + edges[blockIndex]?.y.push('None'); + edges[blockIndex]?.z.push('None'); + } else if (command.id === 'Line') { + if (command.blocks.length !== 1 || command.blocks[0].length < 2) { + console.log('Error parsing line: Lines must have at least two points.'); + return; + } + + const x = [], + y = [], + z = []; + + for (const point of command.blocks[0]) { + if (point.length !== 3) { + console.log('Error parsing line: Points must have three coordinates.'); + return; + } + + for (let i = 0; i < point.length; ++i) { + try { + point[i] = new Function(`'use strict'; { ${variables} return ${point[i]} }`)(); + } catch (e) { + console.log(`Failed to evaluate variable quantity in coordinate of point: ${point[i]}`); + return; + } + } + + x.push(point[0]); + y.push(point[1]); + z.push(point[2]); + } + + x.push('None'); + y.push('None'); + z.push('None'); + + if (lines[blockIndex]) { + lines[blockIndex].x.push(...x); + lines[blockIndex].y.push(...y); + lines[blockIndex].z.push(...z); + } else { + lines[blockIndex] = { + type: 'scatter3d', + mode: 'lines', + x, + y, + z, + line: { width: 5 }, + hoverinfo: 'none' + }; + } + + if (currentState.color instanceof Array) { + lines[blockIndex].line.color = `rgb(${currentState.color[0] * 255},${currentState.color[1] * 255},${ + currentState.color[2] * 255 + })`; + } + + if (currentState.lineThickness !== null) + lines[blockIndex].line.width = Math.max(currentState.lineThickness, 0.005) * 400; + } else if (command.id === 'EdgeForm') { + if (!command.blocks.length) currentState.drawEdges = false; + else currentState.drawEdges = true; + + currentState.edgeForm = true; + executeBlocks(command.blocks); + currentState.edgeForm = false; + } else if (command.id === 'RGBColor') { + if (command.blocks.length === 3) { + if (currentState.edgeForm) currentState.edgeColor = command.blocks; + else currentState.color = command.blocks; + } + } else if (command.id === 'GrayLevel') { + if (command.blocks.length === 1) { + if (currentState.edgeForm) + currentState.edgeColor = [command.blocks[0], command.blocks[0], command.blocks[0]]; + else currentState.color = [command.blocks[0], command.blocks[0], command.blocks[0]]; + } + } else if (command.id === 'Thickness') { + if (command.blocks.length === 1) { + if (currentState.edgeForm) currentState.edgeThickness = command.blocks[0]; + else currentState.lineThickness = command.blocks[0]; + } + } else if (command.id === 'PointSize') { + if (command.blocks.length === 1) currentState.pointSize = command.blocks[0]; + } else if (command.id === 'Text') { + if (command.blocks.length < 2 || command.blocks[1].length !== 3) { + console.log('Error parsing label: Missing arguments.'); + return; + } + + const label = { + type: 'scatter3d', + mode: 'text', + textfont: { color: 'black', size: 12, family: 'mono' }, + textposition: 'top center', + hoverinfo: 'none' + }; + + label.x = [command.blocks[1][0]]; + label.y = [command.blocks[1][1]]; + label.z = [command.blocks[1][2]]; + + if (command.blocks[0].id !== 'StyleForm' || command.blocks[0].blocks.length !== 1) { + console.log('Error parsing label: No text provided.'); + return; + } + + label.text = command.blocks[0].blocks[0]; + if (command.blocks[0].attributes.FontSize) label.textfont.size = command.blocks[0].attributes.FontSize; + + labels.push(label); } + }; - line.push(blockIndex); + const executeBlocks = (blocks) => { + if (!state.slice(-1)[0]?.edgeForm) { + if (state.length) state.push(Object.assign({}, state.slice(-1)[0])); + else state.push(initialState); + ++blockIndex; + } - lineCoords.push(line); + for (const block of blocks) { + if (block instanceof Array) { + for (const subBlock of block) { + if (subBlock instanceof Array) executeBlocks(subBlock); + else executeCommand(subBlock); + } + } else executeCommand(block); + } - } else if (command.match(/RGBColor/)) { - var str = command.match(/RGBColor\[\s*(\d*\.?\d*)\s*,\s*(\d*\.?\d*)\s*,\s*(\d*\.?\d*)\s*\]/); + if (!state.slice(-1)[0].edgeForm) state.pop(); + }; - colors[blockIndex] = [parseFloat(str[1]),parseFloat(str[2]),parseFloat(str[3])]; + const parseLive3DData = (data) => { + // Set up variables. + for (const [name, data] of Object.entries(options.vars)) { + variables += `const ${name} = ${data};`; + } - } else if (command.match(/Thickness/)) { - var str = command.match(/Thickness\[\s*(\d*\.?\d*)\s*\]/); + if (data.attributes.Axes === true) showAxes = true; - lineThickness[blockIndex] = parseFloat(str[1]); - - } else if (command.match(/Text/)) { - // now find any individual labels that need to be plotted - var str = command.match(/\{\s*(-?\d*\.?\d*)\s*,\s*(-?\d*\.?\d*)\s*,\s*(-?\d*\.?\d*)\s*\}/); - var label = {}; - - if (!str) { - console.log('Error Parsing Label'); - return; + if (data.attributes.AxesLabel instanceof Array && data.attributes.AxesLabel.length === 3) + axesLabels = data.attributes.AxesLabel; + + if (data.attributes.ViewPoint instanceof Array && data.attributes.ViewPoint.length === 3) { + eye.x = data.attributes.ViewPoint[0]; + eye.y = data.attributes.ViewPoint[1]; + eye.z = data.attributes.ViewPoint[2]; } - - label.coords = [parseFloat(str[1]),parseFloat(str[2]),parseFloat(str[3])]; - str = command.match(/StyleForm\[\s*(\w+),\s*FontSize\s*->\s*(\d+)\s*\]/); - - if (!str) { - console.log('Error Parsing Label'); - return; + + if (data.attributes.ViewVertical instanceof Array && data.attributes.ViewVertical.length === 3) { + up.x = data.attributes.ViewVertical[0]; + up.y = data.attributes.ViewVertical[1]; + up.z = data.attributes.ViewVertical[2]; } - - label.text = str[1]; - label.fontSize = str[2]; - - loneLabels.push(label); - - } - }); - - } - }); - } - - var splitMathematicaBlocks = function (text) { - // This splits a list of mathematica commands on the commas - - var bracketcount = 0; - var blocks = []; - var block = ''; - - for (var i=0; i < text.length; i++) { - - block += text.charAt(i); - - if (text.charAt(i) === '[') { - bracketcount++; - } - - if (text.charAt(i) == ']') { - bracketcount--; - if (bracketcount == 0) { - i++; - blocks.push(block); - block = ''; - } - } - } + if ('Lighting' in data.attributes) lighting = data.attributes.Lighting; - return blocks; - } + executeBlocks(data.blocks); + }; + const parseArray = (stream, parent, delim = '[') => { + let delimCount = 1; + let block = []; + const oppDelim = delim === '[' ? ']' : delim === '{' ? '}' : '\n'; - var recurseMathematicaBlocks = function (text,initialcount) { - // the mathematica code comes in blocks encolsed by {} - // this code makes an array of those blocks. The largest of them will - // be the polygon block which defines the surface. - var bracketcount = 0; - var blocks = []; - var block = ''; - - if (initialcount) { - bracketcount = initialcount; - } + while (stream.length && stream[0] !== delim) stream.shift(); + stream.shift(); - for (var i=0; i < text.length; i++) { + while (stream.length) { + const char = stream.shift(); - if (text.charAt(i) === '{') { - bracketcount++; - } + if (char === oppDelim) { + --delimCount; + if (delimCount == 0) break; + } - if (bracketcount > 0) { - block += text.charAt(i); - } + block.push(char); - if (text.charAt(i) == '}') { - bracketcount--; - if (bracketcount == 0) { - blocks.push(block.substring(1,block.length-1)); - block = ''; - } + if (char === delim) ++delimCount; + } + + const array = []; + + while (block.length) { + const element = extractNext(block, parent); + if (typeof element !== 'undefined') array.push(element); + if (block.length && block[0] === ',') block.shift(); + } + + return array; + }; + + const extractNext = (stream, parent = {}) => { + if (!stream.length) return; + + // Block + if (stream[0] === '{') return parseArray(stream, parent, '{'); + + let identifier = ''; + while (stream.length && stream[0].match(/^[A-Za-z]/)) identifier += stream.shift(); + + if (identifier) { + while (stream.length && stream[0].match(/^[A-Za-z0-9]/)) identifier += stream.shift(); + + // Attribute + if (stream.length && stream[0] === '-' && stream[1] === '>') { + stream.shift(); + stream.shift(); + const value = extractNext(stream, parent); + if (!parent.attributes) parent.attributes = {}; + parent.attributes[identifier] = value; + return; + } + + // Command + if (stream.length && stream[0] === '[') { + const command = { id: identifier }; + command.blocks = parseArray(stream, command); + return command; + } + } - } - } - - return blocks; - } - - - // This section of code is run whenever the object is created - // run intialize with the mathematica string, possibly getting the string - // form an ajax call if necessary - - if (options.input) { - initialize(options.input); - } else if (options.archive) { - // If an archive file is provided then that is the file we get - // the file name is then the file we want inside the archive. - JSZipUtils.getBinaryContent(options.archive, function (error, data) { - if (error) { - console.log(error); - $(container).html('Failed to get input archive'); - } - - JSZip.loadAsync(data).then((zip) => { - zip.file(options.file).async('string').then((string) => initialize(string)); + // The last case is that of a scalar argument for a command or attribute. So accumulate everything + // up to the next comma or the end of the stream if that comes first. + while (stream.length && stream[0] !== ',') identifier += stream.shift(); + + if (identifier.toLowerCase() === 'true') return true; + if (identifier.toLowerCase() === 'false') return false; + + if (/^[+-]?\d+\.?\d*$/.test(identifier)) return parseFloat(identifier); + + return identifier; + }; + + const parseLive3DString = (text) => { + const stream = text.replaceAll(/\s*/g, '').split(''); + return extractNext(stream); + }; + + // Parse the data string and translate it into plotly traces. + const initialize = (datastring) => { + try { + // Parse LiveGraphics3D string into a javascript object. + const data = parseLive3DString(datastring); + if (!data || data.id !== 'Graphics3D') throw 'Unable to parse live graphics 3d data string.'; + // Evaluate data. + parseLive3DData(data); + } catch (e) { + console.log(e); + return; + } + + Plotly.newPlot( + container, + [...Object.values(surfaces), ...Object.values(edges), ...Object.values(lines), ...points, ...labels], + { + width, + height, + margin: { l: 5, r: 5, b: 5, t: 5 }, + showlegend: false, + paper_bgcolor: 'white', + scene: { + xaxis: { + visible: showAxes, + title: axesLabels[0], + nticks: maxTicks[0], + showspikes: false + }, + yaxis: { + visible: showAxes, + title: axesLabels[1], + nticks: maxTicks[1], + showspikes: false + }, + zaxis: { + visible: showAxes, + title: axesLabels[2], + nticks: maxTicks[2], + showspikes: false + }, + camera: { eye, up } + } + }, + { displaylogo: false } + ); + }; + + // This section of code is run whenever the object is created. It obtains the data either from direct input, a + // zip file, or a data file. Then it calls intialize with the data string. + + if (options.input) { + initialize(options.input); + } else if (options.archive) { + // If an archive file is provided then retrieve that file. + // The file name is the file inside the archive that contains the data. + JSZipUtils.getBinaryContent(options.archive, (error, data) => { + if (error) { + console.log(error); + container.innerHTML = 'Failed to get input archive'; + } + + JSZip.loadAsync(data).then((zip) => { + zip.file(options.file) + .async('string') + .then((string) => initialize(string)); + }); + }); + } else if (options.file) { + fetch(options.file) + .then((response) => (response.ok ? response.text() : response)) + .then((data) => initialize(data)) + .catch((error) => { + console.log(error); + container.innerHTML = 'Failed to get input file'; + }); + } else { + container.innerHTML = 'No input data provided'; + } + }; + + // Deal with live graphics 3d elements that are already on the page. + document.querySelectorAll('.live-graphics-3d-container').forEach(liveGraphics3D); + + // Deal with live graphics 3d elements that are added to the page later. + const observer = new MutationObserver((mutationsList) => { + mutationsList.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node instanceof Element) { + if (node.classList.contains('live-graphics-3d-container')) liveGraphics3D(node); + else node.querySelectorAll('.live-graphics-3d-container').forEach(liveGraphics3D); + } + }); }); }); - - - } else if (options.file) { - - $.ajax({ - url : options.file, - dataType : 'text', - async : 'true', - success : function(data) { - initialize(data); - }, - error : function(x,y,error) { - console.log(error); - $(container).html('Failed to get input file'); - }}); - - } else { - $(container).html('No input data provided'); - } -} + observer.observe(document.body, { childList: true, subtree: true }); +})(); diff --git a/htdocs/js/MathQuill/mqeditor.js b/htdocs/js/MathQuill/mqeditor.js index 7aeff39819..5b02d78e6f 100644 --- a/htdocs/js/MathQuill/mqeditor.js +++ b/htdocs/js/MathQuill/mqeditor.js @@ -18,11 +18,11 @@ const answerLabel = mq_input.id.replace(/^MaThQuIlL_/, ''); const input = document.getElementById(answerLabel); const inputType = input?.type; - if (typeof(inputType) !== 'string' - || ( - (inputType.toLowerCase() !== 'text' || !input.classList.contains('codeshard')) - && (inputType.toLowerCase() !== 'textarea' || !input.classList.contains('latexentryfield')) - )) + if ( + typeof inputType !== 'string' || + ((inputType.toLowerCase() !== 'text' || !input.classList.contains('codeshard')) && + (inputType.toLowerCase() !== 'textarea' || !input.classList.contains('latexentryfield'))) + ) return; const answerQuill = document.createElement('span'); @@ -31,6 +31,14 @@ input.classList.add('mq-edit'); answerQuill.latexInput = mq_input; + // Give the mathquill answer box the correct/incorrect colors. + if (input.classList.contains('correct')) answerQuill.classList.add('correct'); + if (input.classList.contains('incorrect')) answerQuill.classList.add('incorrect'); + if (input.classList.contains('partially-correct')) answerQuill.classList.add('partially-correct'); + + // Find the feedback button for this input if there is one on the page. + const feedbackBtn = document.querySelector(`button[data-answer-label="${answerLabel}"`); + // Default options. const cfgOptions = { spaceBehavesLikeTab: true, @@ -39,8 +47,10 @@ sumStartsWithNEquals: true, supSubsRequireOperand: true, autoCommands: ['pi', 'sqrt', 'root', 'vert', 'inf', 'union', 'abs', 'deg', 'AA', 'angstrom', 'ln', 'log'] - .concat(['sin', 'cos', 'tan', 'sec', 'csc', 'cot'].reduce((a, t) => - a.concat([t, `arc${t}`]), [])).join(' '), + .concat( + ['sin', 'cos', 'tan', 'sec', 'csc', 'cot'].reduce((a, t) => a.concat([t, `arc${t}`, `a${t}`]), []) + ) + .join(' '), rootsAreExponents: true, logsChangeBase: true, maxDepth: 10 @@ -55,12 +65,12 @@ // Disable the toolbar when a text block is entered. textBlockEnter: () => { if (answerQuill.toolbar) - answerQuill.toolbar.querySelectorAll('button').forEach((button) => button.disabled = true); + answerQuill.toolbar.querySelectorAll('button').forEach((button) => (button.disabled = true)); }, // Re-enable the toolbar when a text block is exited. textBlockExit: () => { if (answerQuill.toolbar) - answerQuill.toolbar.querySelectorAll('button').forEach((button) => button.disabled = false); + answerQuill.toolbar.querySelectorAll('button').forEach((button) => (button.disabled = false)); } }; @@ -130,9 +140,9 @@ button.append(icon); // Find the preview button container, and add the equation editor button to that. - const buttonContainer = container.nextElementSibling; - if (buttonContainer && buttonContainer.classList.contains('latexentry-button-container')) { - buttonContainer.classList.add('d-flex', 'gap-1'); + const buttonContainer = document.getElementById(`${answerLabel}-latexentry-button-container`); + if (buttonContainer) { + buttonContainer.classList.add('d-flex', 'gap-2'); buttonContainer.prepend(button); innerContainer.append(buttonContainer); } else { @@ -141,7 +151,7 @@ // Create a collapse to hold the editor. const collapse = document.createElement('div'); - collapse.classList.add('collapse', 'mt-1'); + collapse.classList.add('collapse', 'mt-2'); collapse.id = `${answerLabel}-equation-editor`; let blinkInterval; @@ -160,8 +170,15 @@ contents.classList.add('card'); const cardHeader = document.createElement('div'); - cardHeader.classList.add('card-header', 'd-flex', 'justify-content-between', 'align-items-center', - 'px-2', 'py-1', 'text-bg-secondary'); + cardHeader.classList.add( + 'card-header', + 'd-flex', + 'justify-content-between', + 'align-items-center', + 'px-2', + 'py-1', + 'text-bg-secondary' + ); const title = document.createElement('span'); title.textContent = 'Equation Editor'; @@ -200,11 +217,19 @@ input.focus(); } setSelection(); - } + }; const cardFooter = document.createElement('div'); - cardFooter.classList.add('card-footer', 'd-flex', 'pt-0', 'pb-2', 'px-2', 'gap-1', - 'bg-white', 'border-top-0'); + cardFooter.classList.add( + 'card-footer', + 'd-flex', + 'pt-0', + 'pb-2', + 'px-2', + 'gap-2', + 'bg-white', + 'border-top-0' + ); const insertButton = document.createElement('button'); insertButton.type = 'button'; @@ -241,6 +266,9 @@ answerQuill.input.value = ''; answerQuill.latexInput.value = ''; } + + // If the feedback popover is open, then update its position. + if (feedbackBtn) bootstrap.Popover.getInstance(feedbackBtn)?.update(); }; input.after(answerQuill); @@ -309,23 +337,22 @@ button.classList.add('symbol-button', 'btn', 'btn-dark'); button.dataset.latex = buttonData.latex; button.dataset.bsToggle = 'tooltip'; - button.dataset.bsTitle = buttonData.tooltip; + button.title = buttonData.tooltip; const icon = document.createElement('span'); icon.id = `icon-${buttonData.id}-${answerQuill.id}`; icon.textContent = buttonData.icon; + icon.setAttribute('aria-hidden', 'true'); button.append(icon); answerQuill.toolbar.append(button); MQ.StaticMath(icon, { mouseEvents: false }); - answerQuill.toolbar.tooltips.push(new bootstrap.Tooltip(button, { - placement: 'left', trigger: 'hover', delay: { show: 500, hide: 0 } - })); + answerQuill.toolbar.tooltips.push(new bootstrap.Tooltip(button, { placement: 'left' })); button.addEventListener('click', () => { answerQuill.mathField.cmd(button.dataset.latex); answerQuill.textarea.focus(); - }) + }); } answerQuill.toolbar.addEventListener('keydown', (e) => { @@ -375,15 +402,15 @@ if (window.scrollY + elRect.top + elRect.height / 2 < toolbarHeight / 2) { answerQuill.toolbar.style.top = `-${window.scrollY + parentRect.top}px`; - answerQuill.toolbar.style.bottom = toolbarHeight > pageHeight ? - `${window.scrollY + parentRect.bottom - pageHeight}px` - : null; + answerQuill.toolbar.style.bottom = + toolbarHeight > pageHeight ? `${window.scrollY + parentRect.bottom - pageHeight}px` : null; } else if (window.scrollY + elRect.top + elRect.height / 2 + toolbarHeight / 2 > pageHeight) { answerQuill.toolbar.style.top = null; answerQuill.toolbar.style.bottom = `${window.scrollY + parentRect.bottom - pageHeight}px`; } else { - answerQuill.toolbar.style.top = - `${elRect.top + elRect.height / 2 - toolbarHeight / 2 - parentRect.top}px`; + answerQuill.toolbar.style.top = `${ + elRect.top + elRect.height / 2 - toolbarHeight / 2 - parentRect.top + }px`; answerQuill.toolbar.style.bottom = null; } } else { @@ -395,18 +422,21 @@ const elRect = answerQuill.getBoundingClientRect(); const top = window.scrollY + elRect.bottom - elRect.height / 2 - toolbarHeight / 2; const bottom = top + toolbarHeight; - answerQuill.toolbar.style.top = - `${top < 0 ? 0 : bottom > pageHeight ? pageHeight - toolbarHeight : top}px`; + answerQuill.toolbar.style.top = `${ + top < 0 ? 0 : bottom > pageHeight ? pageHeight - toolbarHeight : top + }px`; answerQuill.toolbar.style.height = null; } } - } + }; window.addEventListener('resize', answerQuill.toolbar.setPosition); answerQuill.toolbar.setPosition(); answerQuill.after(answerQuill.toolbar); - setTimeout(() => { if (answerQuill.toolbar) answerQuill.toolbar.style.opacity = 1; }, 0); + setTimeout(() => { + if (answerQuill.toolbar) answerQuill.toolbar.style.opacity = 1; + }, 0); }); // Add a context menu to toggle whether the toolbar is enabled or not. @@ -434,22 +464,30 @@ li.append(action); container.append(menuEl); - const menu = - new bootstrap.Dropdown(hiddenLink, { reference: answerQuill, offset: [answerQuill.offsetWidth, 0] }); + const menu = new bootstrap.Dropdown(hiddenLink, { + reference: answerQuill, + offset: [answerQuill.offsetWidth, 0] + }); menu.show(); hiddenLink.addEventListener('hidden.bs.dropdown', () => { - menu.dispose(); menuEl.remove(); container.remove(); + menu.dispose(); + menuEl.remove(); + container.remove(); }); - action.addEventListener('click', (e) => { - e.preventDefault(); - toolbarEnabled = !toolbarEnabled; - localStorage.setItem('MQEditorToolbarEnabled', toolbarEnabled) - if (!toolbarEnabled && answerQuill.toolbar) toolbarRemove(); - menu.hide(); - answerQuill.textarea.focus(); - }, { once: true }); + action.addEventListener( + 'click', + (e) => { + e.preventDefault(); + toolbarEnabled = !toolbarEnabled; + localStorage.setItem('MQEditorToolbarEnabled', toolbarEnabled); + if (!toolbarEnabled && answerQuill.toolbar) toolbarRemove(); + menu.hide(); + answerQuill.textarea.focus(); + }, + { once: true } + ); }); answerQuill.textarea.addEventListener('focusout', (e) => { @@ -483,35 +521,19 @@ // For gateway quizzes, always the preview button document.querySelector('input[name=previewAnswers]')?.click(); // For ww3 - const previewButtonId = - answerQuill.textarea.closest('[name=problemMainForm]')?.id - .replace('problemMainForm', 'previewAnswers'); + const previewButtonId = answerQuill.textarea + .closest('[name=problemMainForm]') + ?.id.replace('problemMainForm', 'previewAnswers'); if (previewButtonId) document.getElementById(previewButtonId)?.click(); } }; answerQuill.addEventListener('keydown', answerQuill.keydownHandler); - answerQuill.mathField.latex(answerQuill.latexInput.value); - answerQuill.mathField.moveToLeftEnd(); - answerQuill.mathField.blur(); - - // Look for a result in the attempts table for this answer. - for (const tableLink of document.querySelectorAll('td a[data-answer-id]')) { - // Give the mathquill answer box the correct/incorrect colors. - if (answerLabel.includes(tableLink.dataset.answerId)) { - if (tableLink.parentNode.classList.contains('ResultsWithoutError')) - answerQuill.classList.add('correct'); - else answerQuill.classList.add('incorrect'); - } - - // Make a click on the results table link give focus to the mathquill answer box. - if (answerLabel === tableLink.dataset.answerId) { - tableLink.addEventListener('click', (e) => { - e.preventDefault(); - answerQuill.textarea.focus(); - }); - } - } + setTimeout(() => { + answerQuill.mathField.latex(answerQuill.latexInput.value); + answerQuill.mathField.moveToLeftEnd(); + answerQuill.mathField.blur(); + }, 100); }; // Set up MathQuill inputs that are already in the page. diff --git a/htdocs/js/MathQuill/mqeditor.scss b/htdocs/js/MathQuill/mqeditor.scss index ec2f517c38..6d780bdd38 100644 --- a/htdocs/js/MathQuill/mqeditor.scss +++ b/htdocs/js/MathQuill/mqeditor.scss @@ -3,27 +3,11 @@ span[id^='mq-answer'] { /*rtl:ignore*/ direction: ltr; - padding: 4px 5px 2px 5px; + padding: 4px; border-radius: 4px !important; background-color: white; margin-right: 0; margin-left: 0; - - &.correct { - border-color: rgba(81, 153, 81, 0.8); - outline: 0; - outline: thin dotted \9; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(81, 153, 81, 0.6); - color: inherit; - } - - &.incorrect { - border-color: rgba(191, 84, 84, 0.8); - outline: 0; - outline: thin dotted \9; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(191, 84, 84, 0.6); - color: inherit; - } } input[type='text'].codeshard.mq-edit { @@ -48,7 +32,11 @@ input[type='text'].codeshard.mq-edit { vertical-align: middle; font-weight: 400; line-height: 18px; - font-family: Helvetica Neue, Helvetica, Arial, sans-serif; + font-family: + Helvetica Neue, + Helvetica, + Arial, + sans-serif; } .mq-latex-editor-backdrop-container { @@ -68,7 +56,12 @@ input[type='text'].codeshard.mq-edit { line-height: 18px; white-space: pre-wrap; word-wrap: break-word; - font-family: Helvetica Neue, Helvetica, Arial, Helvetica, sans-serif; + font-family: + Helvetica Neue, + Helvetica, + Arial, + Helvetica, + sans-serif; color: transparent; visibility: hidden; @@ -116,9 +109,12 @@ input[type='text'].codeshard.mq-edit { border-radius: 4px; border: 2px solid darkgray; background-color: white; + /*rtl:ignore*/ right: 10px; z-index: 1001; + overflow-x: hidden; overflow-y: auto; + scrollbar-width: thin; opacity: 1; transition: opacity 500ms ease; @@ -133,7 +129,9 @@ input[type='text'].codeshard.mq-edit { height: 45px; border-radius: 4px; background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.15), + 0 1px 1px rgba(0, 0, 0, 0.075); &:focus { z-index: 9999; diff --git a/htdocs/js/MathView/mathview.js b/htdocs/js/MathView/mathview.js index 4581143153..1952526154 100644 --- a/htdocs/js/MathView/mathview.js +++ b/htdocs/js/MathView/mathview.js @@ -94,7 +94,7 @@ // Find the preview button container, and wrap it in the inner container. const buttonContainer = container.nextElementSibling; if (buttonContainer && buttonContainer.classList.contains('latexentry-button-container')) { - buttonContainer.classList.add('d-flex', 'justify-content-end', 'gap-1'); + buttonContainer.classList.add('d-flex', 'justify-content-end', 'gap-2'); buttonContainer.append(this.button); innerContainer.append(buttonContainer); } else { @@ -171,7 +171,7 @@ inputGroup.append(this.inputTextBox); const footer = document.createElement('div'); - footer.classList.add('d-flex', 'justify-content-end', 'gap-1', 'mt-2'); + footer.classList.add('d-flex', 'justify-content-end', 'gap-2', 'mt-2'); const insertButton = document.createElement('button'); insertButton.type = 'button'; @@ -219,7 +219,7 @@ // Only do this while the popover is visible. const inputRegenPreview = () => this.regenPreview(); this.button.addEventListener('shown.bs.popover', () => { - this.inputTextBox.addEventListener('keyup', inputRegenPreview) + this.inputTextBox.addEventListener('keyup', inputRegenPreview); if (!this.options.decoratedTextBoxAsInput) { this.inputTextBox.focus(); @@ -238,7 +238,7 @@ }); this.button.addEventListener('hide.bs.popover', () => { this.popover.tip.dispatchEvent(new Event('focusout')); - this.inputTextBox.removeEventListener('keyup', inputRegenPreview) + this.inputTextBox.removeEventListener('keyup', inputRegenPreview); }); const closeOther = () => { @@ -278,7 +278,7 @@ // Regenerate the preview in the math viewer whenever the input value changes. regenPreview() { - let text = this.inputTextBox.value.replace(/\*\*/g, '^'); + let text = this.inputTextBox.value; if (this.renderingMode === 'LATEX') this.mviewer.textContent = `\\(${text}\\)`; else this.mviewer.textContent = `\`${text}\``; diff --git a/htdocs/js/MathView/mathview.scss b/htdocs/js/MathView/mathview.scss index 8801b4dfe1..0592a52fc0 100644 --- a/htdocs/js/MathView/mathview.scss +++ b/htdocs/js/MathView/mathview.scss @@ -30,7 +30,11 @@ vertical-align: middle; font-weight: 400; line-height: 18px; - font-family: Helvetica Neue, Helvetica, Arial, sans-serif; + font-family: + Helvetica Neue, + Helvetica, + Arial, + sans-serif; } .mv-backdrop-container { @@ -50,7 +54,12 @@ line-height: 18px; white-space: pre-wrap; word-wrap: break-word; - font-family: Helvetica Neue, Helvetica, Arial, Helvetica, sans-serif; + font-family: + Helvetica Neue, + Helvetica, + Arial, + Helvetica, + sans-serif; color: transparent; visibility: hidden; diff --git a/htdocs/js/Problem/details-accordion.js b/htdocs/js/Problem/details-accordion.js new file mode 100644 index 0000000000..621de1e95d --- /dev/null +++ b/htdocs/js/Problem/details-accordion.js @@ -0,0 +1,39 @@ +(() => { + const setupAccordion = (accordion) => { + const collapseEl = accordion.querySelector('.collapse'); + const button = accordion.querySelector('summary.accordion-button'); + const details = accordion.querySelector('details.accordion-item'); + if (!collapseEl || !button || !details) return; + + const collapse = new bootstrap.Collapse(collapseEl, { toggle: false }); + button.addEventListener('click', () => collapse.toggle()); + + details.addEventListener('click', (e) => e.preventDefault()); + collapseEl.addEventListener('show.bs.collapse', () => { + details.open = true; + button.classList.remove('collapsed'); + }); + collapseEl.addEventListener('hide.bs.collapse', () => button.classList.add('collapsed')); + collapseEl.addEventListener('hidden.bs.collapse', () => (details.open = false)); + }; + + // Deal with solution/hint details that are already on the page. + document.querySelectorAll('.solution.accordion, .hint.accordion').forEach(setupAccordion); + + // Deal with solution/hint details that are added to the page later. + const observer = new MutationObserver((mutationsList) => { + mutationsList.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node instanceof Element) { + if ( + (node.classList.contains('solution') || node.classList.contains('hint')) && + node.classList.contains('accordion') + ) + setupAccordion(node); + else node.querySelectorAll('.solution.accordion, .hint.accordion').forEach(setupAccordion); + } + }); + }); + }); + observer.observe(document.body, { childList: true, subtree: true }); +})(); diff --git a/htdocs/js/Problem/problem.scss b/htdocs/js/Problem/problem.scss index e4c4e7d487..dc77f3ea01 100644 --- a/htdocs/js/Problem/problem.scss +++ b/htdocs/js/Problem/problem.scss @@ -1,5 +1,5 @@ /* WeBWorK Online Homework Delivery System - * Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork + * Copyright © 2000-2024 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 @@ -12,7 +12,7 @@ * Artistic License for more details. */ -/* Styles used in pg problems and by the attempts table. */ +/* Styles used in pg problems. */ .problem-main-form { margin: 0.5rem 0 0; @@ -33,13 +33,18 @@ } /* Problem elements */ - label, input[type=text], select, textarea { + label, + input[type='text'], + select, + textarea { font-weight: normal; line-height: 18px; width: auto; } - select, textarea, input[type=text] { + select, + textarea, + input[type='text'] { display: inline-block; padding: 4px 6px; margin-bottom: 0; @@ -50,40 +55,57 @@ background-color: white; } - textarea, input[type=text] { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + textarea, + input[type='text'] { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } - input[type=text] { + input[type='text'] { height: 30px; font-size: 14px; line-height: 20px; } - select, input[type=text], input[type=radio], input[type=checkbox] { - &.correct { + select, + input[type='text'], + input[type='radio'], + input[type='checkbox'], + span[id^='mq-answer'], + .graphtool-container { + &.correct:not(:focus):not(.mq-focused) { border-color: rgba(81, 153, 81, 0.8); /* green */ outline: 0; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px 2px rgba(81, 153, 81, 0.6); - color: inherit; + box-shadow: + inset 0 0 2px 1px rgba(0, 0, 0, 0.25), + 0 0 0 0.2rem rgba(81, 153, 81, 0.5); } - &.incorrect { + &.incorrect:not(:focus):not(.mq-focused) { border-color: rgba(191, 84, 84, 0.8); /* red */ outline: 0; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px 2px rgba(191, 84, 84, 0.6); - color:inherit; + box-shadow: + inset 0 0 2px 1px rgba(0, 0, 0, 0.25), + 0 0 0 0.2rem rgba(191, 84, 84, 0.5); + } + + &.partially-correct:not(:focus):not(.mq-focused) { + border-color: rgba(255, 193, 7, 1); /* yellow */ + outline: 0; + box-shadow: + inset 0 0 2px 1px rgba(0, 0, 0, 0.25), + 0 0 0 0.2rem rgba(255, 193, 7, 0.8); } } - input[type=radio] { + input[type='radio'] { margin-right: 0.25rem; } select { cursor: pointer; - &[multiple], &[size] { + &[multiple], + &[size] { height: auto; } } @@ -92,164 +114,249 @@ max-width: 100%; } - input[type=text], input[type=radio], textarea, select { + input[type='text'], + input[type='radio'], + input[type='checkbox'], + textarea, + select { &:focus { - border-color: rgba(82, 168, 236, 0.8); + border-color: rgba(112, 154, 192, 0.8); outline: 0; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px 2px rgba(82, 168, 236, 0.6); + box-shadow: + inset 0 1px 1px rgba(0, 0, 0, 0.25), + 0 0 0 0.2rem rgba(136, 187, 221, 0.8); } } .pg-table { text-align: center; - thead, tbody, tfoot, tr, td, th { + thead, + tbody, + tfoot, + tr, + td, + th { padding: 0.25rem; border: 1px solid black; } } -} -/* rtl:raw: -.problem-content[dir=ltr] input[type=radio] { - margin-right: 0.25rem; -} -*/ + .ww-feedback-btn { + width: 25px; + line-height: 1; + position: relative; + --bs-btn-padding-x: 2px; + --bs-btn-padding-y: 2px; + + i { + &::before { + content: url("data:image/svg+xml,"); + } + + &.correct::before { + content: url("data:image/svg+xml,"); + } + + &.incorrect::before { + content: url("data:image/svg+xml,"); + } + + &.partially-correct::before { + content: url("data:image/svg+xml,"); + } + } -/* Answer template */ + &.with-message::before { + content: ' '; + position: absolute; + transform: translate(-50%, -50%); + top: 0; + left: 100%; + border: 1px solid black; + border-radius: 50%; + background-color: var(--bs-warning); + padding: 0.25rem; + } + } -.attemptResultsHeader { - margin-bottom: 0.5rem; - padding: 0; - font-size: 1.4875rem; - font-weight: bold; - line-height: 1.2; -} + .radio-buttons-container, + .checkboxes-container, + .applet-container, + .graphtool-outer-container, + .ww-feedback-container { + position: relative; + width: fit-content; + + .ww-feedback-btn { + position: absolute; + left: 100%; + top: 0; + margin-left: 0.25rem; + } -table.attemptResults { - border-style: outset; - border-width: 1px; - margin-bottom: 1em; - border-spacing: 1px; - width: 100%; - border: 1px solid #ddd; - border-collapse: separate; - border-radius: 4px; + &.ww-fb-align-middle { + .ww-feedback-btn { + top: 50%; + transform: translateY(-50%); + } + } - thead:first-child tr:first-child > th:first-child, - tbody:first-child tr:first-child > td:first-child, - tbody:first-child tr:first-child > th:first-child { - border-top-left-radius: 4px; + &.ww-fb-align-bottom { + .ww-feedback-btn { + top: unset; + bottom: 0; + } + } } +} - thead:first-child tr:first-child > th:last-child, - tbody:first-child tr:first-child > td:last-child, - tbody:first-child tr:first-child > th:last-child { - border-top-right-radius: 4px; - } +/* rtl:raw: +.problem-content[dir=ltr] input[type=radio] { + margin-right: 0.25rem; +} +*/ - thead:last-child tr:last-child > th:first-child, - tbody:last-child tr:last-child > td:first-child, - tbody:last-child tr:last-child > th:first-child, - tfoot:last-child tr:last-child > td:first-child, - tfoot:last-child tr:last-child > th:first-child { - border-bottom-left-radius: 4px; - } +/* Feedback */ - thead:last-child tr:last-child > th:last-child, - tbody:last-child tr:last-child > td:last-child, - tbody:last-child tr:last-child > th:last-child, - tfoot:last-child tr:last-child > td:last-child, - tfoot:last-child tr:last-child > th:last-child { - border-bottom-right-radius: 4px; - } +.ww-feedback-popover { + --bs-popover-body-padding-x: 0; + --bs-popover-body-padding-y: 0; + --bs-popover-max-width: 600px; + --bs-popover-zindex: 17; + position: absolute; - td, th { - border-style: inset; - border-width: 1px; + .popover-header { text-align: center; - vertical-align: middle; - padding: 2px 5px; - color: inherit; - border-color: #ddd; - background-color: #ddd; - } + cursor: pointer; + --bs-popover-header-bg: var(--bs-info); + --bs-popover-header-color: white; - .ArrayLayout { - td { - border-style: none; - border-width: 0px; - padding: 0px; - background-color: transparent; + .btn-close { + --bs-btn-close-opacity: 0.75; + --bs-btn-close-hover-opacity: 1; + --bs-btn-close-focus-shadow: 0 0 0 0.15rem #00000080; } } - .parsehilight { - color: inherit; - background-color: yellow; - } - - .popover { - max-width: 100%; - } - - td { - &.FeedbackMessage { - background-color: #ede275; /* Harvest Gold */ - } - - &.ResultsWithoutError { - background-color: #8f8; + &.correct { + .popover-header { + --bs-popover-header-bg: var(--bs-success); + --bs-popover-header-color: white; } + } - &.ResultsWithError { - background-color: #d69191; /* Light Red */ - color: black; + &.incorrect { + .popover-header { + --bs-popover-header-bg: var(--bs-danger); + --bs-popover-header-color: white; } } - div.answer-preview, - span.answer-preview { - display: block; - width: 100%; - height: 100%; + &.partially-correct { + .popover-header { + --bs-popover-header-bg: var(--bs-warning); + --bs-popover-header-color: black; + } } - a, a span { - color: #038; - text-decoration: none; + &:not(.correct-only) { + min-width: 200px; - &:hover { - text-decoration: underline; + .popover-body { + .card { + border-top-left-radius: 0; + border-top-right-radius: 0; + --bs-card-spacer-y: 0.5rem; + } } } -} - -div, label, span { - &.ResultsWithoutError { - color: #0f5132; /* Dark Green */ - background-color: #8f8; /* Light Green */ - padding: 0.25rem; - border: 1px solid transparent; - border-radius: 0.25rem; - box-shadow: 3px 3px 3px darkgray; - } - &.ResultsWithError { - color: #400; /* Dark Red */ - background-color: #d69191; /* Light Red */ - padding: 0.25rem; - border: 1px solid transparent; - border-radius: 0.25rem; - box-shadow: 3px 3px 3px darkgray; + &.correct-only { + .popover-body { + .card { + --bs-card-spacer-x: 0.25rem; + --bs-card-spacer-y: 0.25rem; + } + } } - &.ResultsAlert { - color: #0e038c; /* Dark Blue */ - background-color: #fbd2b8; /* Light Orange */ - padding: 0.25rem; - border: 1px solid transparent; - border-radius: 0.25rem; - box-shadow: 3px 3px 3px darkgray; + .popover-body { + .card { + --bs-card-cap-bg: #ddd; + + .card-header { + border-radius: 0; + + &:not(:first-child) { + border-top: var(--bs-card-border-width) solid var(--bs-card-border-color); + } + } + + .card-body { + mjx-container { + margin: 0; + } + + .parsehilight { + background-color: yellow; + } + + .ArrayLayout { + td { + border-style: none; + border-width: 0px; + padding: 0px; + background-color: transparent; + } + } + + &.feedback-message { + background-color: #ede275; + &:not(:last-child) { + border-bottom: 1px solid black; + } + &:last-child { + border-bottom-left-radius: var(--bs-card-inner-border-radius); + border-bottom-right-radius: var(--bs-card-inner-border-radius); + } + } + + &.resize-transition { + overflow: clip; + transition: + max-height 1s ease-in, + max-width 1s ease-in; + } + + .fade-out { + animation: fade-out 0.25s ease-in; + + @keyframes fade-out { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } + } + } + + .fade-in { + animation: fade-in 0.5s ease-in; + + @keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } + } + } + } + } } } diff --git a/htdocs/js/QuickMatrixEntry/quickmatrixentry.js b/htdocs/js/QuickMatrixEntry/quickmatrixentry.js new file mode 100644 index 0000000000..beb784f4cc --- /dev/null +++ b/htdocs/js/QuickMatrixEntry/quickmatrixentry.js @@ -0,0 +1,132 @@ +/* global bootstrap */ + +'use strict'; + +(() => { + const setupQuickMatrixEntryBtn = (button) => { + button.addEventListener('click', () => { + const name = button.name; + + const modal = document.createElement('div'); + modal.classList.add('modal'); + modal.tabIndex = -1; + modal.setAttribute('aria-labelledby', 'matrix-entry-dialog-title'); + modal.setAttribute('aria-hidden', 'true'); + + const modalDialog = document.createElement('div'); + modalDialog.classList.add('modal-dialog', 'modal-dialog-centered'); + const modalContent = document.createElement('div'); + modalContent.classList.add('modal-content'); + + const modalHeader = document.createElement('div'); + modalHeader.classList.add('modal-header'); + + const title = document.createElement('h1'); + title.classList.add('fs-3', 'm-0'); + title.id = 'matrix-entry-dialog-title'; + title.textContent = 'Enter matrix'; + + const closeButton = document.createElement('button'); + closeButton.type = 'button'; + closeButton.classList.add('btn-close'); + closeButton.dataset.bsDismiss = 'modal'; + closeButton.setAttribute('aria-label', 'close'); + + modalHeader.append(title, closeButton); + + const modalBody = document.createElement('div'); + modalBody.classList.add('modal-body'); + const modalBodyContent = document.createElement('div'); + modalBody.append(modalBodyContent); + + const textarea = document.createElement('textarea'); + textarea.classList.add('form-control'); + textarea.rows = 10; + modalBodyContent.append(textarea); + + const modalFooter = document.createElement('div'); + modalFooter.classList.add('modal-footer'); + + const enterButton = document.createElement('button'); + enterButton.classList.add('btn', 'btn-primary'); + enterButton.textContent = 'Enter'; + + modalFooter.append(enterButton); + modalContent.append(modalHeader, modalBody, modalFooter); + modalDialog.append(modalContent); + modal.append(modalDialog); + + const insert_value = (i, j, entry) => { + const input = document.getElementById(i == 0 && j == 0 ? name : `MaTrIx_${name}_${i}_${j}`); + if (!input) return; + input.value = entry; + if (window.answerQuills && window.answerQuills[input.name]) + answerQuills[input.name].mathField.latex(entry); + }; + + const extract_value = (i, j) => + document.getElementById(i == 0 && j == 0 ? name : `MaTrIx_${name}_${i}_${j}`)?.value || 0; + + const rows = parseInt(button.dataset.rows); + const columns = parseInt(button.dataset.columns); + + // Enter something that indicates how many columns to fill. + const entries = []; + for (let i = 0; i < rows; ++i) { + entries.push([]); + for (let j = 0; j < columns; ++j) { + entries[entries.length - 1].push(extract_value(i, j)); + } + } + textarea.value = entries.map((row) => row.join(' ')).join('\n'); + + enterButton.addEventListener('click', () => { + // Get the textarea value, and then remove initial and trailing white space, replace commas with a + // space, replace end brackets with a new line, and remove start brackets. Then split on new lines. + const matrix = []; + for (const row of textarea.value + .replace(/^\s*|\s*$/, '') + .replace(/,/g, ' ') + .replace(/\]/g, '\n') + .replace(/\[/g, '') + .split(/\n/)) { + matrix.push(row.replace(/^\s*/, '').split(/\s+/)); + } + + for (let i = 0; i < matrix.length; ++i) { + for (let j = 0; j < matrix[i].length; ++j) { + insert_value(i, j, matrix[i][j]); + } + } + + bsModal.hide(); + }); + + const bsModal = new bootstrap.Modal(modal); + bsModal.show(); + document.querySelector('.modal-backdrop')?.style.setProperty('--bs-backdrop-opacity', '0.2'); + + modal.addEventListener('hidden.bs.modal', () => { + bsModal.dispose(); + modal.remove(); + }); + }); + }; + + // Deal with uncheckable radios already in the page. + document.querySelectorAll('.quick-matrix-entry-btn').forEach(setupQuickMatrixEntryBtn); + + // Deal with radios that are added to the page later. + const observer = new MutationObserver((mutationsList) => { + for (const mutation of mutationsList) { + for (const node of mutation.addedNodes) { + if (node instanceof Element) { + if (node.classList.contains('quick-matrix-entry-btn')) setupQuickMatrixEntryBtn(node); + else node.querySelectorAll('.quick-matrix-entry-btn').forEach(setupQuickMatrixEntryBtn); + } + } + } + }); + observer.observe(document.body, { childList: true, subtree: true }); + window.addEventListener('unload', () => observer.disconnect()); +})(); diff --git a/htdocs/js/RadioButtons/RadioButtons.js b/htdocs/js/RadioButtons/RadioButtons.js index 18f82279ce..42ceebe30f 100644 --- a/htdocs/js/RadioButtons/RadioButtons.js +++ b/htdocs/js/RadioButtons/RadioButtons.js @@ -1,6 +1,6 @@ // ################################################################################ // # WeBWorK Online Homework Delivery System -// # Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +// # Copyright © 2000-2024 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 @@ -15,20 +15,28 @@ 'use strict'; (() => { + const radioGroups = {}; + // Setup uncheckable radios. const setupUncheckableRadio = (radio) => { if (!radio.dataset.uncheckableRadioButton) return; delete radio.dataset.uncheckableRadioButton; + if (!radioGroups[radio.name]) radioGroups[radio.name] = [radio]; + else radioGroups[radio.name].push(radio); + if (radio.checked) radio.dataset.currentlyChecked = '1'; radio.addEventListener('click', (e) => { + for (const groupRadio of radioGroups[radio.name]) { + if (groupRadio === radio) continue; + delete groupRadio.dataset.currentlyChecked; + } if (radio.dataset.shift && !e.shiftKey) { radio.dataset.currentlyChecked = '1'; return; } - const currentlyChecked = radio.dataset.currentlyChecked; - if (currentlyChecked) { + if (radio.dataset.currentlyChecked) { delete radio.dataset.currentlyChecked; radio.checked = false; } else { diff --git a/htdocs/js/RadioMultiAnswer/RadioMultiAnswer.js b/htdocs/js/RadioMultiAnswer/RadioMultiAnswer.js index 27202f458a..b3bc389e4d 100644 --- a/htdocs/js/RadioMultiAnswer/RadioMultiAnswer.js +++ b/htdocs/js/RadioMultiAnswer/RadioMultiAnswer.js @@ -1,6 +1,6 @@ // ################################################################################ // # WeBWorK Online Homework Delivery System -// # Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +// # Copyright © 2000-2024 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 @@ -39,12 +39,14 @@ // If MathQuill is enabled, then this will be the MathQuill input. const answerInputs = [document.getElementById(`mq-answer-${answerRule}`) ?? input]; - // If this is a radio answer, then save the other radio inputs so they can be also be disabled/enabled - // appropriately depending on which radio input in the radio multianswer group is selected. - if (input.type && input.type.toLowerCase() == 'radio') { + // If this is a radio or checkbox answer, then save the other radio or checkbox inputs so they can be also + // be disabled/enabled appropriately depending on which radio input in the radio multianswer group is + // selected. + const type = input.type?.toLowerCase(); + if (type && (type === 'radio' || type === 'checkbox')) { answerInputs.push( - ...Array.from(document.querySelectorAll(`input[type="radio"][name="${answerRule}"]`)).filter( - (radio) => radio.id !== answerRule + ...Array.from(document.querySelectorAll(`input[type="${type}"][name="${answerRule}"]`)).filter( + (input) => input.id !== answerRule ) ); } diff --git a/htdocs/js/RadioMultiAnswer/RadioMultiAnswer.scss b/htdocs/js/RadioMultiAnswer/RadioMultiAnswer.scss index 6d6d23d3c0..e44a24ce2e 100644 --- a/htdocs/js/RadioMultiAnswer/RadioMultiAnswer.scss +++ b/htdocs/js/RadioMultiAnswer/RadioMultiAnswer.scss @@ -1,6 +1,6 @@ // ################################################################################ // # WeBWorK Online Homework Delivery System -// # Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +// # Copyright © 2000-2024 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 @@ -13,33 +13,52 @@ // # Artistic License for more details. // ################################################################################ -.radio-container { - display: flex; - align-items: baseline; +.radio-multianswer-container { + position: relative; + width: fit-content; + max-width: calc(100% - 1rem - 25px); - .radio-content { - display: inline-block; - margin-left: 5px; - line-height: 26px; - } + .radio-container { + display: flex; + align-items: baseline; + position: relative; + width: fit-content; - input[type='radio'] { - flex-shrink: 0; - margin-left: 0; - line-height: 26px; - align-self: first baseline; - } + .radio-content { + display: inline-block; + margin-left: 5px; + line-height: 26px; + } - label { - flex-shrink: 0; - line-height: 26px; - } + input[type='radio'] { + flex-shrink: 0; + margin-left: 0; + line-height: 26px; + align-self: first baseline; + } + + label { + flex-shrink: 0; + line-height: 26px; + } + + input[disabled] { + pointer-events: none; + } + + .rma-state-disabled { + opacity: 0.35; + } - input[disabled] { - pointer-events: none; + .ww-feedback-btn { + top: 50%; + transform: translateY(-50%); + } } - .rma-state-disabled { - opacity: 0.35; + .ww-feedback-btn { + position: absolute; + left: 100%; + top: 0; } } diff --git a/htdocs/js/Scaffold/scaffold.js b/htdocs/js/Scaffold/scaffold.js index d36b986291..060c00747b 100644 --- a/htdocs/js/Scaffold/scaffold.js +++ b/htdocs/js/Scaffold/scaffold.js @@ -4,12 +4,19 @@ section.addEventListener('shown.bs.collapse', () => { // Reflow MathQuill answer boxes so that their contents are rendered correctly if (window.answerQuills) { - Object.keys(answerQuills).forEach( - (quill) => { if (section.querySelector('#' + quill)) answerQuills[quill].mathField.reflow(); } - ); + Object.keys(answerQuills).forEach((quill) => { + if (section.querySelector('#' + quill)) answerQuills[quill].mathField.reflow(); + }); } }); - }) + + section.addEventListener('hide.bs.collapse', () => { + // Close any open feedback popovers in this scaffold. + for (const button of section.querySelectorAll('.ww-feedback-btn')) { + bootstrap.Popover.getInstance(button)?.hide(); + } + }); + }); }; // Set up any scaffolds already on the page. diff --git a/htdocs/package-lock.json b/htdocs/package-lock.json index f8b13defe4..6a2c620b88 100644 --- a/htdocs/package-lock.json +++ b/htdocs/package-lock.json @@ -1,2794 +1,2823 @@ { - "name": "pg.javascript_package_manager", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "pg.javascript_package_manager", - "license": "GPL-2.0+", - "dependencies": { - "jsxgraph": "^1.5.0", - "jszip": "^3.10.1", - "jszip-utils": "^0.1.0", - "mathquill": "github:openwebwork/mathquill#WeBWorK-2.18", - "plotly.js-dist-min": "^2.18.2", - "sortablejs": "^1.15.0", - "x3dom": "^1.8.1" - }, - "devDependencies": { - "autoprefixer": "^10.4.13", - "chokidar": "^3.5.3", - "cssnano": "^6.0.0", - "postcss": "^8.4.31", - "rtlcss": "^4.0.0", - "sass": "^1.57.1", - "terser": "^5.16.1", - "yargs": "^17.6.2" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.13", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", - "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - ], - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-lite": "^1.0.30001426", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001547", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz", - "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "dev": true - }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/css-declaration-sorter": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz", - "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==", - "dev": true, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.0.tgz", - "integrity": "sha512-RGlcbzGhzEBCHuQe3k+Udyj5M00z0pm9S+VurHXFEOXxH+y0sVrJH2sMzoyz2d8N1EScazg+DVvmgyx0lurwwA==", - "dev": true, - "dependencies": { - "cssnano-preset-default": "^6.0.0", - "lilconfig": "^2.1.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-preset-default": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.0.0.tgz", - "integrity": "sha512-BDxlaFzObRDXUiCCBQUNQcI+f1/aX2mgoNtXGjV6PG64POcHoDUoX+LgMWw+Q4609QhxwkcSnS65YFs42RA6qQ==", - "dev": true, - "dependencies": { - "css-declaration-sorter": "^6.3.1", - "cssnano-utils": "^4.0.0", - "postcss-calc": "^8.2.3", - "postcss-colormin": "^6.0.0", - "postcss-convert-values": "^6.0.0", - "postcss-discard-comments": "^6.0.0", - "postcss-discard-duplicates": "^6.0.0", - "postcss-discard-empty": "^6.0.0", - "postcss-discard-overridden": "^6.0.0", - "postcss-merge-longhand": "^6.0.0", - "postcss-merge-rules": "^6.0.0", - "postcss-minify-font-values": "^6.0.0", - "postcss-minify-gradients": "^6.0.0", - "postcss-minify-params": "^6.0.0", - "postcss-minify-selectors": "^6.0.0", - "postcss-normalize-charset": "^6.0.0", - "postcss-normalize-display-values": "^6.0.0", - "postcss-normalize-positions": "^6.0.0", - "postcss-normalize-repeat-style": "^6.0.0", - "postcss-normalize-string": "^6.0.0", - "postcss-normalize-timing-functions": "^6.0.0", - "postcss-normalize-unicode": "^6.0.0", - "postcss-normalize-url": "^6.0.0", - "postcss-normalize-whitespace": "^6.0.0", - "postcss-ordered-values": "^6.0.0", - "postcss-reduce-initial": "^6.0.0", - "postcss-reduce-transforms": "^6.0.0", - "postcss-svgo": "^6.0.0", - "postcss-unique-selectors": "^6.0.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.0.tgz", - "integrity": "sha512-Z39TLP+1E0KUcd7LGyF4qMfu8ZufI0rDzhdyAMsa/8UyNUU8wpS0fhdBxbQbv32r64ea00h4878gommRVg2BHw==", - "dev": true, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", - "dev": true, - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://www.patreon.com/infusion" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "node_modules/immutable": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.2.tgz", - "integrity": "sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==", - "dev": true - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/jsxgraph": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/jsxgraph/-/jsxgraph-1.5.0.tgz", - "integrity": "sha512-mXsW1LG9AEZpiYDM+nScs5G5ZKw6xrQIrVLszRXeXFX+XYbS1Abx9oMmk7WMDbCXerYFzNo5/vZ9MRZpFeABpQ==", - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/jszip-utils/-/jszip-utils-0.1.0.tgz", - "integrity": "sha512-tBNe0o3HAf8vo0BrOYnLPnXNo5A3KsRMnkBFYjh20Y3GPYGfgyoclEMgvVchx0nnL+mherPi74yLPIusHUQpZg==" - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true - }, - "node_modules/mathquill": { - "version": "0.10.1", - "resolved": "git+ssh://git@github.com/openwebwork/mathquill.git#ebd17ed4b3de1ec79a3dd866cb9b2691d19e1dbc", - "license": "MPL-2.0" - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", - "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plotly.js-dist-min": { - "version": "2.18.2", - "resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-2.18.2.tgz", - "integrity": "sha512-1XPu4ykjDUavENN/guKcMC4XqzTP1712ccEO95sflXnVcRUyruYQYc4XsXi9yF5UwvOHKZMeH0fGK6j/jv89kw==" - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-calc": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0" - }, - "peerDependencies": { - "postcss": "^8.2.2" - } - }, - "node_modules/postcss-colormin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.0.0.tgz", - "integrity": "sha512-EuO+bAUmutWoZYgHn2T1dG1pPqHU6L4TjzPlu4t1wZGXQ/fxV16xg2EJmYi0z+6r+MGV1yvpx1BHkUaRrPa2bw==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-convert-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.0.0.tgz", - "integrity": "sha512-U5D8QhVwqT++ecmy8rnTb+RL9n/B806UVaS3m60lqle4YDFcpbS3ae5bTQIh3wOGUSDHSEtMYLs/38dNG7EYFw==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-comments": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.0.tgz", - "integrity": "sha512-p2skSGqzPMZkEQvJsgnkBhCn8gI7NzRH2683EEjrIkoMiwRELx68yoUJ3q3DGSGuQ8Ug9Gsn+OuDr46yfO+eFw==", - "dev": true, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.0.tgz", - "integrity": "sha512-bU1SXIizMLtDW4oSsi5C/xHKbhLlhek/0/yCnoMQany9k3nPBq+Ctsv/9oMmyqbR96HYHxZcHyK2HR5P/mqoGA==", - "dev": true, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-empty": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.0.tgz", - "integrity": "sha512-b+h1S1VT6dNhpcg+LpyiUrdnEZfICF0my7HAKgJixJLW7BnNmpRH34+uw/etf5AhOlIhIAuXApSzzDzMI9K/gQ==", - "dev": true, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.0.tgz", - "integrity": "sha512-4VELwssYXDFigPYAZ8vL4yX4mUepF/oCBeeIT4OXsJPYOtvJumyz9WflmJWTfDwCUcpDR+z0zvCWBXgTx35SVw==", - "dev": true, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.0.tgz", - "integrity": "sha512-4VSfd1lvGkLTLYcxFuISDtWUfFS4zXe0FpF149AyziftPFQIWxjvFSKhA4MIxMe4XM3yTDgQMbSNgzIVxChbIg==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^6.0.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-merge-rules": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.0.0.tgz", - "integrity": "sha512-rCXkklftzEkniyv3f4mRCQzxD6oE4Quyh61uyWTUbCJ26Pv2hoz+fivJSsSBWxDBeScR4fKCfF3HHTcD7Ybqnw==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^4.0.0", - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.0.0.tgz", - "integrity": "sha512-zNRAVtyh5E8ndZEYXA4WS8ZYsAp798HiIQ1V2UF/C/munLp2r1UGHwf1+6JFu7hdEhJFN+W1WJQKBrtjhFgEnA==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.0.tgz", - "integrity": "sha512-wO0F6YfVAR+K1xVxF53ueZJza3L+R3E6cp0VwuXJQejnNUH0DjcAFe3JEBeTY1dLwGa0NlDWueCA1VlEfiKgAA==", - "dev": true, - "dependencies": { - "colord": "^2.9.1", - "cssnano-utils": "^4.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-params": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.0.0.tgz", - "integrity": "sha512-Fz/wMQDveiS0n5JPcvsMeyNXOIMrwF88n7196puSuQSWSa+/Ofc1gDOSY2xi8+A4PqB5dlYCKk/WfqKqsI+ReQ==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "cssnano-utils": "^4.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.0.tgz", - "integrity": "sha512-ec/q9JNCOC2CRDNnypipGfOhbYPuUkewGwLnbv6omue/PSASbHSU7s6uSQ0tcFRVv731oMIx8k0SP4ZX6be/0g==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.0.tgz", - "integrity": "sha512-cqundwChbu8yO/gSWkuFDmKrCZ2vJzDAocheT2JTd0sFNA4HMGoKMfbk2B+J0OmO0t5GUkiAkSM5yF2rSLUjgQ==", - "dev": true, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.0.tgz", - "integrity": "sha512-Qyt5kMrvy7dJRO3OjF7zkotGfuYALETZE+4lk66sziWSPzlBEt7FrUshV6VLECkI4EN8Z863O6Nci4NXQGNzYw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.0.tgz", - "integrity": "sha512-mPCzhSV8+30FZyWhxi6UoVRYd3ZBJgTRly4hOkaSifo0H+pjDYcii/aVT4YE6QpOil15a5uiv6ftnY3rm0igPg==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.0.tgz", - "integrity": "sha512-50W5JWEBiOOAez2AKBh4kRFm2uhrT3O1Uwdxz7k24aKtbD83vqmcVG7zoIwo6xI2FZ/HDlbrCopXhLeTpQib1A==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-string": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.0.tgz", - "integrity": "sha512-KWkIB7TrPOiqb8ZZz6homet2KWKJwIlysF5ICPZrXAylGe2hzX/HSf4NTX2rRPJMAtlRsj/yfkrWGavFuB+c0w==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.0.tgz", - "integrity": "sha512-tpIXWciXBp5CiFs8sem90IWlw76FV4oi6QEWfQwyeREVwUy39VSeSqjAT7X0Qw650yAimYW5gkl2Gd871N5SQg==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.0.0.tgz", - "integrity": "sha512-ui5crYkb5ubEUDugDc786L/Me+DXp2dLg3fVJbqyAl0VPkAeALyAijF2zOsnZyaS1HyfPuMH0DwyY18VMFVNkg==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-url": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.0.tgz", - "integrity": "sha512-98mvh2QzIPbb02YDIrYvAg4OUzGH7s1ZgHlD3fIdTHLgPLRpv1ZTKJDnSAKr4Rt21ZQFzwhGMXxpXlfrUBKFHw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.0.tgz", - "integrity": "sha512-7cfE1AyLiK0+ZBG6FmLziJzqQCpTQY+8XjMhMAz8WSBSCsCNNUKujgIgjCAmDT3cJ+3zjTXFkoD15ZPsckArVw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-ordered-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.0.tgz", - "integrity": "sha512-K36XzUDpvfG/nWkjs6d1hRBydeIxGpKS2+n+ywlKPzx1nMYDYpoGbcjhj5AwVYJK1qV2/SDoDEnHzlPD6s3nMg==", - "dev": true, - "dependencies": { - "cssnano-utils": "^4.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", - "integrity": "sha512-s2UOnidpVuXu6JiiI5U+fV2jamAw5YNA9Fdi/GRK0zLDLCfXmSGqQtzpUPtfN66RtCbb9fFHoyZdQaxOB3WxVA==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.0.tgz", - "integrity": "sha512-FQ9f6xM1homnuy1wLe9lP1wujzxnwt1EwiigtWwuyf8FsqqXUDUp2Ulxf9A5yjlUOTdCJO6lonYjg1mgqIIi2w==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.0.tgz", - "integrity": "sha512-r9zvj/wGAoAIodn84dR/kFqwhINp5YsJkLoujybWG59grR/IHx+uQ2Zo+IcOwM0jskfYX3R0mo+1Kip1VSNcvw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^3.0.2" - }, - "engines": { - "node": "^14 || ^16 || >= 18" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.0.tgz", - "integrity": "sha512-EPQzpZNxOxP7777t73RQpZE5e9TrnCrkvp7AH7a0l89JmZiPnS82y216JowHXwpBCQitfyxrof9TK3rYbi7/Yw==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rtlcss": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.0.0.tgz", - "integrity": "sha512-j6oypPP+mgFwDXL1JkLCtm6U/DQntMUqlv5SOhpgHhdIE+PmBcjrtAHIpXfbIup47kD5Sgja9JDsDF1NNOsBwQ==", - "dev": true, - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0", - "postcss": "^8.4.6", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "rtlcss": "bin/rtlcss.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/sass": { - "version": "1.57.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz", - "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", - "dev": true, - "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "node_modules/sortablejs": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz", - "integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stylehacks": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.0.0.tgz", - "integrity": "sha512-+UT589qhHPwz6mTlCLSt/vMNTJx8dopeJlZAlBMJPWA3ORqu6wmQY7FBXf+qD+FsqoBJODyqNxOUP3jdntFRdw==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4", - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/svgo": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz", - "integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==", - "dev": true, - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.2.1", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/terser": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.1.tgz", - "integrity": "sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==", - "dev": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist-lint": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/x3dom": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/x3dom/-/x3dom-1.8.1.tgz", - "integrity": "sha512-rlKMA0pWkBUqoC75fjSm1rRJhdlnqXcCgEP5MHsspcO5pdkr0J52oO5EmmR1eOCzex5U4qAlyG8aFVoZy8wtfA==" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - } - }, - "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true - }, - "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "autoprefixer": { - "version": "10.4.13", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", - "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", - "dev": true, - "requires": { - "browserslist": "^4.21.4", - "caniuse-lite": "^1.0.30001426", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - } - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "requires": { - "fill-range": "^7.1.1" - } - }, - "browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001547", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz", - "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==", - "dev": true - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "dev": true - }, - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "css-declaration-sorter": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz", - "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==", - "dev": true, - "requires": {} - }, - "css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - } - }, - "css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, - "requires": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - } - }, - "css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "cssnano": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.0.tgz", - "integrity": "sha512-RGlcbzGhzEBCHuQe3k+Udyj5M00z0pm9S+VurHXFEOXxH+y0sVrJH2sMzoyz2d8N1EScazg+DVvmgyx0lurwwA==", - "dev": true, - "requires": { - "cssnano-preset-default": "^6.0.0", - "lilconfig": "^2.1.0" - } - }, - "cssnano-preset-default": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.0.0.tgz", - "integrity": "sha512-BDxlaFzObRDXUiCCBQUNQcI+f1/aX2mgoNtXGjV6PG64POcHoDUoX+LgMWw+Q4609QhxwkcSnS65YFs42RA6qQ==", - "dev": true, - "requires": { - "css-declaration-sorter": "^6.3.1", - "cssnano-utils": "^4.0.0", - "postcss-calc": "^8.2.3", - "postcss-colormin": "^6.0.0", - "postcss-convert-values": "^6.0.0", - "postcss-discard-comments": "^6.0.0", - "postcss-discard-duplicates": "^6.0.0", - "postcss-discard-empty": "^6.0.0", - "postcss-discard-overridden": "^6.0.0", - "postcss-merge-longhand": "^6.0.0", - "postcss-merge-rules": "^6.0.0", - "postcss-minify-font-values": "^6.0.0", - "postcss-minify-gradients": "^6.0.0", - "postcss-minify-params": "^6.0.0", - "postcss-minify-selectors": "^6.0.0", - "postcss-normalize-charset": "^6.0.0", - "postcss-normalize-display-values": "^6.0.0", - "postcss-normalize-positions": "^6.0.0", - "postcss-normalize-repeat-style": "^6.0.0", - "postcss-normalize-string": "^6.0.0", - "postcss-normalize-timing-functions": "^6.0.0", - "postcss-normalize-unicode": "^6.0.0", - "postcss-normalize-url": "^6.0.0", - "postcss-normalize-whitespace": "^6.0.0", - "postcss-ordered-values": "^6.0.0", - "postcss-reduce-initial": "^6.0.0", - "postcss-reduce-transforms": "^6.0.0", - "postcss-svgo": "^6.0.0", - "postcss-unique-selectors": "^6.0.0" - } - }, - "cssnano-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.0.tgz", - "integrity": "sha512-Z39TLP+1E0KUcd7LGyF4qMfu8ZufI0rDzhdyAMsa/8UyNUU8wpS0fhdBxbQbv32r64ea00h4878gommRVg2BHw==", - "dev": true, - "requires": {} - }, - "csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, - "requires": { - "css-tree": "~2.2.0" - }, - "dependencies": { - "css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, - "requires": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - } - }, - "mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true - } - } - }, - "dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "requires": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - } - }, - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true - }, - "domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "requires": { - "domelementtype": "^2.3.0" - } - }, - "domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", - "dev": true, - "requires": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" - } - }, - "electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "immutable": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.2.tgz", - "integrity": "sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==", - "dev": true - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "jsxgraph": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/jsxgraph/-/jsxgraph-1.5.0.tgz", - "integrity": "sha512-mXsW1LG9AEZpiYDM+nScs5G5ZKw6xrQIrVLszRXeXFX+XYbS1Abx9oMmk7WMDbCXerYFzNo5/vZ9MRZpFeABpQ==" - }, - "jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "requires": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "jszip-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/jszip-utils/-/jszip-utils-0.1.0.tgz", - "integrity": "sha512-tBNe0o3HAf8vo0BrOYnLPnXNo5A3KsRMnkBFYjh20Y3GPYGfgyoclEMgvVchx0nnL+mherPi74yLPIusHUQpZg==" - }, - "lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "requires": { - "immediate": "~3.0.5" - } - }, - "lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true - }, - "mathquill": { - "version": "git+ssh://git@github.com/openwebwork/mathquill.git#ebd17ed4b3de1ec79a3dd866cb9b2691d19e1dbc", - "from": "mathquill@github:openwebwork/mathquill#WeBWorK-2.18" - }, - "mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true - }, - "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true - }, - "node-releases": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", - "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true - }, - "nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "requires": { - "boolbase": "^1.0.0" - } - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "plotly.js-dist-min": { - "version": "2.18.2", - "resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-2.18.2.tgz", - "integrity": "sha512-1XPu4ykjDUavENN/guKcMC4XqzTP1712ccEO95sflXnVcRUyruYQYc4XsXi9yF5UwvOHKZMeH0fGK6j/jv89kw==" - }, - "postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-calc": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-colormin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.0.0.tgz", - "integrity": "sha512-EuO+bAUmutWoZYgHn2T1dG1pPqHU6L4TjzPlu4t1wZGXQ/fxV16xg2EJmYi0z+6r+MGV1yvpx1BHkUaRrPa2bw==", - "dev": true, - "requires": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-convert-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.0.0.tgz", - "integrity": "sha512-U5D8QhVwqT++ecmy8rnTb+RL9n/B806UVaS3m60lqle4YDFcpbS3ae5bTQIh3wOGUSDHSEtMYLs/38dNG7EYFw==", - "dev": true, - "requires": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-discard-comments": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.0.tgz", - "integrity": "sha512-p2skSGqzPMZkEQvJsgnkBhCn8gI7NzRH2683EEjrIkoMiwRELx68yoUJ3q3DGSGuQ8Ug9Gsn+OuDr46yfO+eFw==", - "dev": true, - "requires": {} - }, - "postcss-discard-duplicates": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.0.tgz", - "integrity": "sha512-bU1SXIizMLtDW4oSsi5C/xHKbhLlhek/0/yCnoMQany9k3nPBq+Ctsv/9oMmyqbR96HYHxZcHyK2HR5P/mqoGA==", - "dev": true, - "requires": {} - }, - "postcss-discard-empty": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.0.tgz", - "integrity": "sha512-b+h1S1VT6dNhpcg+LpyiUrdnEZfICF0my7HAKgJixJLW7BnNmpRH34+uw/etf5AhOlIhIAuXApSzzDzMI9K/gQ==", - "dev": true, - "requires": {} - }, - "postcss-discard-overridden": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.0.tgz", - "integrity": "sha512-4VELwssYXDFigPYAZ8vL4yX4mUepF/oCBeeIT4OXsJPYOtvJumyz9WflmJWTfDwCUcpDR+z0zvCWBXgTx35SVw==", - "dev": true, - "requires": {} - }, - "postcss-merge-longhand": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.0.tgz", - "integrity": "sha512-4VSfd1lvGkLTLYcxFuISDtWUfFS4zXe0FpF149AyziftPFQIWxjvFSKhA4MIxMe4XM3yTDgQMbSNgzIVxChbIg==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^6.0.0" - } - }, - "postcss-merge-rules": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.0.0.tgz", - "integrity": "sha512-rCXkklftzEkniyv3f4mRCQzxD6oE4Quyh61uyWTUbCJ26Pv2hoz+fivJSsSBWxDBeScR4fKCfF3HHTcD7Ybqnw==", - "dev": true, - "requires": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^4.0.0", - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-minify-font-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.0.0.tgz", - "integrity": "sha512-zNRAVtyh5E8ndZEYXA4WS8ZYsAp798HiIQ1V2UF/C/munLp2r1UGHwf1+6JFu7hdEhJFN+W1WJQKBrtjhFgEnA==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-minify-gradients": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.0.tgz", - "integrity": "sha512-wO0F6YfVAR+K1xVxF53ueZJza3L+R3E6cp0VwuXJQejnNUH0DjcAFe3JEBeTY1dLwGa0NlDWueCA1VlEfiKgAA==", - "dev": true, - "requires": { - "colord": "^2.9.1", - "cssnano-utils": "^4.0.0", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-minify-params": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.0.0.tgz", - "integrity": "sha512-Fz/wMQDveiS0n5JPcvsMeyNXOIMrwF88n7196puSuQSWSa+/Ofc1gDOSY2xi8+A4PqB5dlYCKk/WfqKqsI+ReQ==", - "dev": true, - "requires": { - "browserslist": "^4.21.4", - "cssnano-utils": "^4.0.0", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-minify-selectors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.0.tgz", - "integrity": "sha512-ec/q9JNCOC2CRDNnypipGfOhbYPuUkewGwLnbv6omue/PSASbHSU7s6uSQ0tcFRVv731oMIx8k0SP4ZX6be/0g==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-normalize-charset": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.0.tgz", - "integrity": "sha512-cqundwChbu8yO/gSWkuFDmKrCZ2vJzDAocheT2JTd0sFNA4HMGoKMfbk2B+J0OmO0t5GUkiAkSM5yF2rSLUjgQ==", - "dev": true, - "requires": {} - }, - "postcss-normalize-display-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.0.tgz", - "integrity": "sha512-Qyt5kMrvy7dJRO3OjF7zkotGfuYALETZE+4lk66sziWSPzlBEt7FrUshV6VLECkI4EN8Z863O6Nci4NXQGNzYw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-positions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.0.tgz", - "integrity": "sha512-mPCzhSV8+30FZyWhxi6UoVRYd3ZBJgTRly4hOkaSifo0H+pjDYcii/aVT4YE6QpOil15a5uiv6ftnY3rm0igPg==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-repeat-style": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.0.tgz", - "integrity": "sha512-50W5JWEBiOOAez2AKBh4kRFm2uhrT3O1Uwdxz7k24aKtbD83vqmcVG7zoIwo6xI2FZ/HDlbrCopXhLeTpQib1A==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-string": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.0.tgz", - "integrity": "sha512-KWkIB7TrPOiqb8ZZz6homet2KWKJwIlysF5ICPZrXAylGe2hzX/HSf4NTX2rRPJMAtlRsj/yfkrWGavFuB+c0w==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-timing-functions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.0.tgz", - "integrity": "sha512-tpIXWciXBp5CiFs8sem90IWlw76FV4oi6QEWfQwyeREVwUy39VSeSqjAT7X0Qw650yAimYW5gkl2Gd871N5SQg==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-unicode": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.0.0.tgz", - "integrity": "sha512-ui5crYkb5ubEUDugDc786L/Me+DXp2dLg3fVJbqyAl0VPkAeALyAijF2zOsnZyaS1HyfPuMH0DwyY18VMFVNkg==", - "dev": true, - "requires": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-url": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.0.tgz", - "integrity": "sha512-98mvh2QzIPbb02YDIrYvAg4OUzGH7s1ZgHlD3fIdTHLgPLRpv1ZTKJDnSAKr4Rt21ZQFzwhGMXxpXlfrUBKFHw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-normalize-whitespace": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.0.tgz", - "integrity": "sha512-7cfE1AyLiK0+ZBG6FmLziJzqQCpTQY+8XjMhMAz8WSBSCsCNNUKujgIgjCAmDT3cJ+3zjTXFkoD15ZPsckArVw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-ordered-values": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.0.tgz", - "integrity": "sha512-K36XzUDpvfG/nWkjs6d1hRBydeIxGpKS2+n+ywlKPzx1nMYDYpoGbcjhj5AwVYJK1qV2/SDoDEnHzlPD6s3nMg==", - "dev": true, - "requires": { - "cssnano-utils": "^4.0.0", - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-reduce-initial": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", - "integrity": "sha512-s2UOnidpVuXu6JiiI5U+fV2jamAw5YNA9Fdi/GRK0zLDLCfXmSGqQtzpUPtfN66RtCbb9fFHoyZdQaxOB3WxVA==", - "dev": true, - "requires": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0" - } - }, - "postcss-reduce-transforms": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.0.tgz", - "integrity": "sha512-FQ9f6xM1homnuy1wLe9lP1wujzxnwt1EwiigtWwuyf8FsqqXUDUp2Ulxf9A5yjlUOTdCJO6lonYjg1mgqIIi2w==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0" - } - }, - "postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-svgo": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.0.tgz", - "integrity": "sha512-r9zvj/wGAoAIodn84dR/kFqwhINp5YsJkLoujybWG59grR/IHx+uQ2Zo+IcOwM0jskfYX3R0mo+1Kip1VSNcvw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.2.0", - "svgo": "^3.0.2" - } - }, - "postcss-unique-selectors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.0.tgz", - "integrity": "sha512-EPQzpZNxOxP7777t73RQpZE5e9TrnCrkvp7AH7a0l89JmZiPnS82y216JowHXwpBCQitfyxrof9TK3rYbi7/Yw==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.5" - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true - }, - "rtlcss": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.0.0.tgz", - "integrity": "sha512-j6oypPP+mgFwDXL1JkLCtm6U/DQntMUqlv5SOhpgHhdIE+PmBcjrtAHIpXfbIup47kD5Sgja9JDsDF1NNOsBwQ==", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0", - "postcss": "^8.4.6", - "strip-json-comments": "^3.1.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "sass": { - "version": "1.57.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz", - "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==", - "dev": true, - "requires": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "sortablejs": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz", - "integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "stylehacks": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.0.0.tgz", - "integrity": "sha512-+UT589qhHPwz6mTlCLSt/vMNTJx8dopeJlZAlBMJPWA3ORqu6wmQY7FBXf+qD+FsqoBJODyqNxOUP3jdntFRdw==", - "dev": true, - "requires": { - "browserslist": "^4.21.4", - "postcss-selector-parser": "^6.0.4" - } - }, - "svgo": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.2.tgz", - "integrity": "sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==", - "dev": true, - "requires": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.2.1", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - } - }, - "terser": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.1.tgz", - "integrity": "sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==", - "dev": true, - "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - } - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "x3dom": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/x3dom/-/x3dom-1.8.1.tgz", - "integrity": "sha512-rlKMA0pWkBUqoC75fjSm1rRJhdlnqXcCgEP5MHsspcO5pdkr0J52oO5EmmR1eOCzex5U4qAlyG8aFVoZy8wtfA==" - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", - "dev": true, - "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - } - }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true - } - } + "name": "pg.javascript_package_manager", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "pg.javascript_package_manager", + "license": "GPL-2.0+", + "dependencies": { + "jsxgraph": "^1.9.2", + "jszip": "^3.10.1", + "jszip-utils": "^0.1.0", + "mathquill": "github:openwebwork/mathquill#WeBWorK-2.19", + "plotly.js-dist-min": "^2.32.0", + "sortablejs": "^1.15.2" + }, + "devDependencies": { + "autoprefixer": "^10.4.19", + "chokidar": "^3.6.0", + "cssnano": "^6.1.2", + "postcss": "^8.4.38", + "prettier": "^3.2.5", + "rtlcss": "^4.1.1", + "sass": "^1.75.0", + "terser": "^5.30.4", + "yargs": "^17.7.2" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001612", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", + "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/css-declaration-sorter": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", + "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "dev": true, + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.747", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.747.tgz", + "integrity": "sha512-+FnSWZIAvFHbsNVmUxhEqWiaOiPMcfum1GQzlWCg/wLigVtshOsjXHyEFfmt6cFK6+HkS3QOJBv6/3OPumbBfw==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jsxgraph": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/jsxgraph/-/jsxgraph-1.9.2.tgz", + "integrity": "sha512-vaZe7PRY6lCtLHzDQJUEZj7qJhi58aXMvZN8eP2U2955y1y13myphDaQjsHuNuj2mdlANohtFzz/bdoifebV+g==", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/jszip-utils/-/jszip-utils-0.1.0.tgz", + "integrity": "sha512-tBNe0o3HAf8vo0BrOYnLPnXNo5A3KsRMnkBFYjh20Y3GPYGfgyoclEMgvVchx0nnL+mherPi74yLPIusHUQpZg==" + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/mathquill": { + "version": "0.10.1", + "resolved": "git+ssh://git@github.com/openwebwork/mathquill.git#6ef3e23833194c47d7a521c39b44d1f02327d50f" + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plotly.js-dist-min": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-2.32.0.tgz", + "integrity": "sha512-UVznwUQVc7NeFih0tnIbvCpxct+Jxt6yxOGTYJF4vkKIUyujvyiTrH+XazglvcXdybFLERMu/IKt6Lhz3+BqMQ==" + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "dev": true, + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "dev": true, + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rtlcss": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.1.tgz", + "integrity": "sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ==", + "dev": true, + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/sass": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz", + "integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/sortablejs": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz", + "integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/svgo": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", + "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/terser": { + "version": "5.30.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.4.tgz", + "integrity": "sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + } + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true + }, + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "requires": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "requires": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001612", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", + "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", + "dev": true + }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "css-declaration-sorter": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", + "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "dev": true, + "requires": {} + }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "requires": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "dev": true, + "requires": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + } + }, + "cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "dev": true, + "requires": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + } + }, + "cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "dev": true, + "requires": {} + }, + "csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "requires": { + "css-tree": "~2.2.0" + }, + "dependencies": { + "css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "requires": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + } + }, + "mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true + } + } + }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "electron-to-chromium": { + "version": "1.4.747", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.747.tgz", + "integrity": "sha512-+FnSWZIAvFHbsNVmUxhEqWiaOiPMcfum1GQzlWCg/wLigVtshOsjXHyEFfmt6cFK6+HkS3QOJBv6/3OPumbBfw==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "jsxgraph": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/jsxgraph/-/jsxgraph-1.9.2.tgz", + "integrity": "sha512-vaZe7PRY6lCtLHzDQJUEZj7qJhi58aXMvZN8eP2U2955y1y13myphDaQjsHuNuj2mdlANohtFzz/bdoifebV+g==" + }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "jszip-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/jszip-utils/-/jszip-utils-0.1.0.tgz", + "integrity": "sha512-tBNe0o3HAf8vo0BrOYnLPnXNo5A3KsRMnkBFYjh20Y3GPYGfgyoclEMgvVchx0nnL+mherPi74yLPIusHUQpZg==" + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, + "lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "mathquill": { + "version": "git+ssh://git@github.com/openwebwork/mathquill.git#6ef3e23833194c47d7a521c39b44d1f02327d50f", + "from": "mathquill@github:openwebwork/mathquill#WeBWorK-2.19" + }, + "mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true + }, + "node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "plotly.js-dist-min": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-2.32.0.tgz", + "integrity": "sha512-UVznwUQVc7NeFih0tnIbvCpxct+Jxt6yxOGTYJF4vkKIUyujvyiTrH+XazglvcXdybFLERMu/IKt6Lhz3+BqMQ==" + }, + "postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + } + }, + "postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "dev": true, + "requires": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "dev": true, + "requires": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "dev": true, + "requires": {} + }, + "postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "dev": true, + "requires": {} + }, + "postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "dev": true, + "requires": {} + }, + "postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "dev": true, + "requires": {} + }, + "postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + } + }, + "postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "dev": true, + "requires": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + } + }, + "postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "dev": true, + "requires": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "dev": true, + "requires": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.16" + } + }, + "postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "dev": true, + "requires": {} + }, + "postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "dev": true, + "requires": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "dev": true, + "requires": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "dev": true, + "requires": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + } + }, + "postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + } + }, + "postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.16" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "rtlcss": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.1.tgz", + "integrity": "sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "sass": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz", + "integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "sortablejs": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz", + "integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "dev": true, + "requires": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + } + }, + "svgo": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", + "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==", + "dev": true, + "requires": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + } + }, + "terser": { + "version": "5.30.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.4.tgz", + "integrity": "sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } } diff --git a/htdocs/package.json b/htdocs/package.json index 530a7a9da7..d2df67944c 100644 --- a/htdocs/package.json +++ b/htdocs/package.json @@ -8,26 +8,28 @@ }, "scripts": { "generate-assets": "node generate-assets", - "prepare": "npm run generate-assets" + "prepare": "npm run generate-assets", + "prettier-format": "prettier --ignore-path=../.gitignore --write \"**/*.{js,css,scss,html}\" \"../**/*.dist.yml\"", + "prettier-check": "prettier --ignore-path=../.gitignore --check \"**/*.{js,css,scss,html}\" \"../**/*.dist.yml\"" }, "dependencies": { - "jsxgraph": "^1.5.0", + "jsxgraph": "^1.9.2", "jszip": "^3.10.1", "jszip-utils": "^0.1.0", - "mathquill": "github:openwebwork/mathquill#WeBWorK-2.18", - "plotly.js-dist-min": "^2.18.2", - "sortablejs": "^1.15.0", - "x3dom": "^1.8.1" + "mathquill": "github:openwebwork/mathquill#WeBWorK-2.19", + "plotly.js-dist-min": "^2.32.0", + "sortablejs": "^1.15.2" }, "devDependencies": { - "autoprefixer": "^10.4.13", - "chokidar": "^3.5.3", - "cssnano": "^6.0.0", - "postcss": "^8.4.31", - "rtlcss": "^4.0.0", - "sass": "^1.57.1", - "terser": "^5.16.1", - "yargs": "^17.6.2" + "autoprefixer": "^10.4.19", + "chokidar": "^3.6.0", + "cssnano": "^6.1.2", + "postcss": "^8.4.38", + "prettier": "^3.2.5", + "rtlcss": "^4.1.1", + "sass": "^1.75.0", + "terser": "^5.30.4", + "yargs": "^17.7.2" }, "browserslist": [ "last 10 Chrome versions", diff --git a/lib/AnswerHash.pm b/lib/AnswerHash.pm index 0a8d9f2146..0f6efeec36 100755 --- a/lib/AnswerHash.pm +++ b/lib/AnswerHash.pm @@ -5,7 +5,7 @@ ## for the hash, but that might change ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -124,11 +124,6 @@ The answer hash class is guaranteed to contain the following instance variables: =cut -BEGIN { - # main::be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. - -} - package AnswerHash; use Exporter; use PGUtil qw(not_null pretty_print); diff --git a/lib/AnswerIO.pm b/lib/AnswerIO.pm index f986c4a47d..f9df86a1ee 100644 --- a/lib/AnswerIO.pm +++ b/lib/AnswerIO.pm @@ -19,12 +19,10 @@ macros. =cut -BEGIN { - be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. -} - package AnswerIO; +use strict; + # Code for saving Answers to a file # function, not a method # Code in .pm files can access the disk. diff --git a/lib/Applet.pm b/lib/Applet.pm index ecdad095f6..c1d7bbaadc 100644 --- a/lib/Applet.pm +++ b/lib/Applet.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -524,7 +524,11 @@ sub insertObject { my $objectText = $self->{objectText}; $objectText =~ s/(\$\w+)/$1/gee; - return $objectText; + return + qq{
                      ' + . $objectText + . '
                      '; } # These methods are defined so that they can be used in the derived objects in the AppletObjects.pl macro file. diff --git a/lib/ChoiceList.pm b/lib/ChoiceList.pm index 8312227d7f..fb1f695bb7 100644 --- a/lib/ChoiceList.pm +++ b/lib/ChoiceList.pm @@ -126,13 +126,10 @@ format and (as with Match.pm), if necessary, can be appended in order at the end =cut -BEGIN { - be_strict(); -} -#use strict; - package ChoiceList; +use strict; + @ChoiceList::ISA = qw( Exporter ); my %fields = ( @@ -294,7 +291,7 @@ sub complement { =head3 qa Usage: $ml->qa( qw( question1 answer1 question2 answer2 ) ); - + =cut sub qa { @@ -340,11 +337,11 @@ sub ra_correct_ans { =head3 cmp Usage ANS($ml -> cmp); - + provides a MathObject like comparison method returns a string of comparison methods for checking the list object -=cut +=cut sub cmp { my $self = shift; diff --git a/lib/Chromatic.pm b/lib/Chromatic.pm index ce4faaaba4..f48996ffcd 100644 --- a/lib/Chromatic.pm +++ b/lib/Chromatic.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/Circle.pm b/lib/Circle.pm index 9a92fd5266..49eba47d2c 100644 --- a/lib/Circle.pm +++ b/lib/Circle.pm @@ -43,12 +43,10 @@ This module defines a circle which can be inserted as a stamp in a graph (WWPlot =cut -BEGIN { - be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. -} - package Circle; +use strict; + #use WWPlot; #Because of the way problem modules are loaded 'use' is disabled. diff --git a/lib/Complex.pm b/lib/Complex.pm index c97364f079..bb4b3a5898 100644 --- a/lib/Complex.pm +++ b/lib/Complex.pm @@ -1,9 +1,7 @@ -BEGIN { - be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. +package Complex; -} +use strict; -package Complex; *i = *Complex1::i; @Complex::ISA = qw(Complex1); diff --git a/lib/DragNDrop.pm b/lib/DragNDrop.pm index 3bcf18c084..62a0ea4ec6 100644 --- a/lib/DragNDrop.pm +++ b/lib/DragNDrop.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -160,7 +160,8 @@ sub HTML { $out .= qq{ data-label-format="$self->{bucketLabelFormat}"} if $self->{bucketLabelFormat}; $out .= '>'; - $out .= '
                      '; + $out .= '
                      }; $out .= qq{}; $out .= qq{} if ($self->{allowNewBuckets}); diff --git a/lib/Fraction.pm b/lib/Fraction.pm index 6c1a0af0f8..2e7ffc5a8e 100644 --- a/lib/Fraction.pm +++ b/lib/Fraction.pm @@ -18,7 +18,7 @@ numerator #numerator of fraction denominator #denominator of fraction - Arithmetic Methods #these will all accept a scalar value or + Arithmetic Methods #these will all accept a scalar value or #another fraction as an argument plus #returns the sum of the fraction and argument @@ -33,7 +33,7 @@ Other methods - + reduce #reduces to lowest terms, and makes sure denominator is positive scalar #returns the scalar value numerator/denominator print #prints the fraction @@ -45,17 +45,15 @@ The fraction object stores two variables, numerator and denominator. The basic arithmatic methods listed above can be performed on a fraction, and it can return its own -scalar value for use with functions expecting a scalar (ie, sqrt($frac->scalar) ). +scalar value for use with functions expecting a scalar (ie, sqrt($frac->scalar) ). =cut -BEGIN { - be_strict(); -} - package Fraction; +use strict; + my %fields = ( numerator => undef, denominator => undef, diff --git a/lib/Fun.pm b/lib/Fun.pm index c5ee7b0532..53a7bd34c4 100644 --- a/lib/Fun.pm +++ b/lib/Fun.pm @@ -47,7 +47,7 @@ =head1 DESCRIPTION This module defines a parametric or non-parametric function object. The function object is designed to -be inserted into a graph object defined by WWPlot. +be inserted into a graph object defined by WWPlot. The following functions are provided: @@ -55,30 +55,30 @@ The following functions are provided: =head2 new (non-parametric version) -=over 4 +=over 4 =item $fn = new Fun( rule_reference); rule_reference is a reference to a subroutine which accepts a numerical value and returns a numerical value. -The Fun object will draw the graph associated with this subroutine. +The Fun object will draw the graph associated with this subroutine. For example: $rule = sub { my $x= shift; $x**2}; will produce a plot of the x squared. The new method returns a reference to the function object. =item $fn = new Fun( rule_reference , graph_reference); -The function is also placed into the printing queue of the graph object pointed to by graph_reference and the +The function is also placed into the printing queue of the graph object pointed to by graph_reference and the domain of the function object is set to the domain of the graph. =back -=head2 new (parametric version) +=head2 new (parametric version) -=over 4 +=over 4 =item $fn = new Fun ( x_rule_ref, y_rule_ref ); A parametric function object is created where the subroutines refered to by x_rule_ref and y_rule_ref define -the x and y outputs in terms of the input t. +the x and y outputs in terms of the input t. =item $fn = new Fun ( x_rule_ref, y_rule_ref, graph_ref ); @@ -89,10 +89,10 @@ of the function object is not adjusted. The domain's default value is (-1, 1). =head2 Properites - All of the properties are set using the construction $new_value = $fn->property($new_value) + All of the properties are set using the construction $new_value = $fn->property($new_value) and read using $current_value = $fn->property() -=over 4 +=over 4 =item tstart, tstop, steps @@ -101,7 +101,7 @@ used in graphing the function. =item color -The color used to draw the function is specified by a word such as 'orange' or 'yellow'. +The color used to draw the function is specified by a word such as 'orange' or 'yellow'. C<$fn->color('blue')> sets the drawing color to blue. The RGB values for the color are defined in the graph object in which the function is drawn. If the color, e.g. 'mauve', is not defined by the graph object then the function is drawn using the color 'default_color' which is always defined (and usually black). @@ -125,22 +125,22 @@ The width in pixels of the pen used to draw the graph. The pen is square. =over 4 -=item rule +=item rule -This defines a non-parametric function. +This defines a non-parametric function. + + $fn->rule(sub {my $x =shift; $x**2;} ) - $fn->rule(sub {my $x =shift; $x**2;} ) - is equivalent to - + $fn->x_rule(sub {my $x = shift; $x;}); $fn->y_rule(sub {my $x = shift; $x**2;); - + $fn->rule() returns the reference to the y_rule. =item domain -$array_ref = $fn->domain(-1,1) sets tstart to -1 and tstop to 1 and +$array_ref = $fn->domain(-1,1) sets tstart to -1 and tstop to 1 and returns a reference to an array containing this pair of numbers. @@ -154,13 +154,13 @@ The graph object must respond to the methods below. The draw call is mainly for internal use by the graph object. Most users will not call it directly. -=over 4 +=over 4 -=item $graph_ref->{colors} +=item $graph_ref->{colors} a hash containing the defined colors -=item $graph_ref ->im +=item $graph_ref ->im a GD image object @@ -177,18 +177,16 @@ draw line to the point (x,y) using the pattern set by SetBrushed (see GD documen set the current position to (x,y) -=back +=back =back =cut -BEGIN { - be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. -} - package Fun; +use strict; + #use "WWPlot.pm"; #Because of the way problem modules are loaded 'use' is disabled. diff --git a/lib/LaTeXImage.pm b/lib/LaTeXImage.pm index b3503c3cec..0117115ad1 100644 --- a/lib/LaTeXImage.pm +++ b/lib/LaTeXImage.pm @@ -1,7 +1,7 @@ #!/bin/perl ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -16,57 +16,56 @@ # This is a Perl module which simplifies and automates the process of generating # simple images using LaTeX, and converting them into a web-useable format. Its -# typical usage is via the macro PGtikz.pl and is documented there. +# typical usage is via the PGlateximage.pl or PGtikz.pl macros and is documented +# there. + +package LaTeXImage; use strict; use warnings; -use Carp; -use WeBWorK::PG::IO; -use WeBWorK::PG::ImageGenerator; -package LaTeXImage; +use File::Copy qw(move); + +require WeBWorK::PG::IO; +require WeBWorK::PG::ImageGenerator; # The constructor (it takes no parameters) sub new { my $class = shift; my $data = { - tex => '', - environment => '', - # if tikzOptions is nonempty, then environment - # will effectively be ['tikzpicture', tikzOptions] + tex => '', + # If tikzOptions is nonempty, then environment will effectively be [ 'tikzpicture', tikzOptions ]. + environment => '', tikzOptions => '', tikzLibraries => '', texPackages => [], addToPreamble => '', ext => 'svg', - svgMethod => 'pdf2svg', + svgMethod => 'dvisvgm', convertOptions => { input => {}, output => {} }, imageName => '' }; - my $self = sub { - my $field = shift; - if (@_) { + return bless sub { + my ($field, $value) = @_; + if (defined $value) { # The ext field is protected to ensure that unsafe commands can not # be passed to the command line in the system call it is used in. if ($field eq 'ext') { - my $ext = shift; - $data->{ext} = $ext - if ($ext && ($ext =~ /^(png|gif|svg|pdf|tgz)$/)); + $data->{ext} = $value if $value && ($value =~ /^(png|gif|svg|pdf|tgz)$/); } else { - $data->{$field} = shift; + $data->{$field} = $value; } } return $data->{$field}; - }; - return bless $self, $class; + }, $class; } # Accessors -# Set LaTeX image code as a single string parameter. Works best single quoted. +# Set LaTeX image code as a single string parameter. sub tex { - my $self = shift; - return &$self('tex', @_); + my ($self, $tex) = @_; + return &$self('tex', $tex); } # Set an environment to surround the tex(). This can be a string naming the environment. @@ -74,24 +73,24 @@ sub tex { # the environment. If there is a second element, it should be a string with options for # the environment. This could be extended to support environments with multiple option # fields that may use parentheses for delimiters. -# If tikzOptions is nonempty, the input is ignored and output is ['tikzpicture',tikzOptions]. +# If tikzOptions is nonempty, the input is ignored and output is [ 'tikzpicture', tikzOptions ]. sub environment { - my $self = shift; - return [ 'tikzpicture', $self->tikzOptions ] if ($self->tikzOptions ne ''); - return [ &$self('environment', @_), '' ] if (ref(&$self('environment', @_)) ne 'ARRAY'); - return &$self('environment', @_); + my ($self, $environment) = @_; + return [ 'tikzpicture', $self->tikzOptions ] if $self->tikzOptions ne ''; + return [ &$self('environment', $environment), '' ] if ref(&$self('environment', $environment)) ne 'ARRAY'; + return &$self('environment', $environment); } # Set TikZ picture options as a single string parameter. sub tikzOptions { - my $self = shift; - return &$self('tikzOptions', @_); + my ($self, $tikzOptions) = @_; + return &$self('tikzOptions', $tikzOptions); } # Set additional TikZ libraries to load as a single string parameter. sub tikzLibraries { - my $self = shift; - return &$self('tikzLibraries', @_); + my ($self, $tikzLibraries) = @_; + return &$self('tikzLibraries', $tikzLibraries); } # Set additional TeX packages to load. This accepts an array parameter. Note @@ -99,15 +98,15 @@ sub tikzLibraries { # or two elements (the first element the package name, and the optional second # element the package options). sub texPackages { - my $self = shift; - return &$self('texPackages', $_[0]) if ref($_[0]) eq "ARRAY"; + my ($self, $texPackages) = @_; + return &$self('texPackages', $texPackages) if ref($texPackages) eq 'ARRAY'; return &$self('texPackages'); } # Additional TeX commands to add to the TeX preamble sub addToPreamble { - my $self = shift; - return &$self('addToPreamble', @_); + my ($self, $additionalPreamble) = @_; + return &$self('addToPreamble', $additionalPreamble); } # Set the image type. The valid types are 'png', 'gif', 'svg', 'pdf', and 'tgz'. @@ -115,31 +114,31 @@ sub addToPreamble { # The 'tgz' option should be set when 'PTX' is the display mode. # It creates a .tgz file containing .tex, .pdf, .png, and .svg versions of the image sub ext { - my $self = shift; - return &$self('ext', @_); + my ($self, $ext) = @_; + return &$self('ext', $ext); } # Set the method to use to generate svg images. The valid methods are 'pdf2svg' and 'dvisvgm'. sub svgMethod { - my $self = shift; - return &$self('svgMethod', @_); + my ($self, $svgMethod) = @_; + return &$self('svgMethod', $svgMethod); } # Set the options to be used by ImageMagick convert. sub convertOptions { - my $self = shift; - return &$self('convertOptions', @_); + my ($self, $convertOptions) = @_; + return &$self('convertOptions', $convertOptions); } # Set the file name. sub imageName { - my $self = shift; - return &$self('imageName', @_); + my ($self, $imageName) = @_; + return &$self('imageName', $imageName); } sub header { - my $self = shift; - my @output = (); + my $self = shift; + my @output; push(@output, "\\documentclass{standalone}\n"); my @xcolorOpts = grep { ref $_ eq "ARRAY" && $_->[0] eq "xcolor" && defined $_->[1] } @{ $self->texPackages }; my $xcolorOpts = @xcolorOpts ? $xcolorOpts[0][1] : 'svgnames'; @@ -165,15 +164,15 @@ sub header { if (defined $self->environment->[1] && $self->environment->[1] ne ""); push(@output, "\n"); } - @output; + return @output; } sub footer { - my $self = shift; - my @output = (); + my $self = shift; + my @output; push(@output, "\\end{", $self->environment->[0] . "}\n") if $self->environment->[0]; push(@output, "\\end{document}\n"); - @output; + return @output; } # Generate the image file and return the stored location of the image. @@ -181,45 +180,47 @@ sub draw { my $self = shift; my $working_dir = WeBWorK::PG::ImageGenerator::makeTempDirectory(WeBWorK::PG::IO::pg_tmp_dir(), "latex"); - my $data; my $ext = $self->ext; my $svgMethod = $self->svgMethod; - my $fh; - # Create either one or two tex files with one small difference: # set pgfsysdriver to pgfsys-dvisvgm.def for a tex file that dvisvgm will use # Then make only the dvi, only the pdf, or both in case we are making tgz with svg via dvisvgm if (($ext eq 'svg' || $ext eq 'tgz') && $svgMethod eq 'dvisvgm') { - open($fh, ">", "$working_dir/image-dvisvgm.tex") - or warn "Can't open $working_dir/image-dvisvgm.tex for writing."; - my @header = $self->header; - splice @header, 1, 0, "\\def\\pgfsysdriver{pgfsys-dvisvgm.def}\n"; - chmod(0777, "$working_dir/image-dvisvgm.tex"); - print $fh @header; - print $fh $self->tex =~ s/\\\\/\\/gr . "\n"; - print $fh $self->footer; - close $fh; - system "cd $working_dir && " - . WeBWorK::PG::IO::externalCommand('latex') - . " --interaction=nonstopmode image-dvisvgm.tex > latex.stdout 2> /dev/null && " - . WeBWorK::PG::IO::externalCommand('mv') - . " image-dvisvgm.dvi image.dvi"; - chmod(0777, "$working_dir/image.dvi"); + if (open(my $fh, ">", "$working_dir/image-dvisvgm.tex")) { + my @header = $self->header; + splice @header, 1, 0, "\\def\\pgfsysdriver{pgfsys-dvisvgm.def}\n"; + chmod(0777, "$working_dir/image-dvisvgm.tex"); + print $fh @header; + print $fh $self->tex =~ s/\\\\/\\/gr . "\n"; + print $fh $self->footer; + close $fh; + system "cd $working_dir && " + . WeBWorK::PG::IO::externalCommand('latex') + . " --interaction=nonstopmode image-dvisvgm.tex > latex.stdout 2> /dev/null"; + move("$working_dir/image-dvisvgm.dvi", "$working_dir/image.dvi"); + chmod(0777, "$working_dir/image.dvi"); + } else { + warn "Can't open $working_dir/image-dvisvgm.tex for writing."; + return ''; + } } if ($ext ne 'svg' || ($ext eq 'svg' && $svgMethod ne 'dvisvgm')) { - open($fh, ">", "$working_dir/image.tex") - or warn "Can't open $working_dir/image.tex for writing."; - chmod(0777, "$working_dir/image.tex"); - print $fh $self->header; - print $fh $self->tex =~ s/\\\\/\\/gr . "\n"; - print $fh $self->footer; - close $fh; - system "cd $working_dir && " - . WeBWorK::PG::IO::externalCommand('pdflatex') - . " --interaction=nonstopmode image.tex > pdflatex.stdout 2> /dev/null"; - chmod(0777, "$working_dir/image.pdf"); + if (open(my $fh, ">", "$working_dir/image.tex")) { + chmod(0777, "$working_dir/image.tex"); + print $fh $self->header; + print $fh $self->tex =~ s/\\\\/\\/gr . "\n"; + print $fh $self->footer; + close $fh; + system "cd $working_dir && " + . WeBWorK::PG::IO::externalCommand('latex2pdf') + . " --interaction=nonstopmode image.tex > latex.stdout 2> /dev/null"; + chmod(0777, "$working_dir/image.pdf"); + } else { + warn "Can't open $working_dir/image.tex for writing."; + return ''; + } } # Make derivatives of the dvi @@ -250,7 +251,7 @@ sub draw { } } else { warn "The pdf file was not created."; - if (open(my $err_fh, "<", "$working_dir/pdflatex.stdout")) { + if (open(my $err_fh, "<", "$working_dir/latex.stdout")) { while (my $error = <$err_fh>) { warn $error; } @@ -267,6 +268,8 @@ sub draw { warn "Failed to generate tgz file." unless -r "$working_dir/image.tgz"; } + my $data; + # Read the generated image file into memory if (-r "$working_dir/image.$ext") { open(my $in_fh, "<", "$working_dir/image.$ext") @@ -279,16 +282,13 @@ sub draw { } # Delete the files used to generate the image. - if (-e $working_dir) { - system WeBWorK::PG::IO::externalCommand('rm') . " -rf $working_dir"; - } + WeBWorK::PG::IO::remove_tree($working_dir) if -e $working_dir; return $data; } sub use_svgMethod { - my $self = shift; - my $working_dir = shift; + my ($self, $working_dir) = @_; if ($self->svgMethod eq 'dvisvgm') { system WeBWorK::PG::IO::externalCommand('dvisvgm') . " $working_dir/image.dvi --no-fonts --output=$working_dir/image.svg > /dev/null 2>&1"; @@ -297,18 +297,20 @@ sub use_svgMethod { . " $working_dir/image.pdf $working_dir/image.svg > /dev/null 2>&1"; } warn "Failed to generate svg file." unless -r "$working_dir/image.svg"; + + return; } sub use_convert { - my $self = shift; - my $working_dir = shift; - my $ext = shift; + my ($self, $working_dir, $ext) = @_; system WeBWorK::PG::IO::externalCommand('convert') . join('', map { " -$_ " . $self->convertOptions->{input}->{$_} } (keys %{ $self->convertOptions->{input} })) . " $working_dir/image.pdf" . join('', map { " -$_ " . $self->convertOptions->{output}->{$_} } (keys %{ $self->convertOptions->{output} })) . " $working_dir/image.$ext > /dev/null 2>&1"; warn "Failed to generate $ext file." unless -r "$working_dir/image.$ext"; + + return; } 1; diff --git a/lib/Label.pm b/lib/Label.pm index c42cc1709f..52f9b77071 100644 --- a/lib/Label.pm +++ b/lib/Label.pm @@ -39,12 +39,10 @@ This module defines labels for the graph objects (WWPlot). =cut -BEGIN { - be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. -} - package Label; + use strict; + #use Exporter; #use DynaLoader; #use GD; # this is needed to be able to define GD::gdMediumBoldFont and other terms used by GD diff --git a/lib/List.pm b/lib/List.pm index ad9a8a1726..c182e07d71 100644 --- a/lib/List.pm +++ b/lib/List.pm @@ -126,13 +126,10 @@ format and (as with Match.pm), if necessary, can be appended in order at the end =cut -BEGIN { - be_strict(); -} -#use strict; - package List; +use strict; + @List::ISA = qw( Exporter ); my %fields = ( @@ -294,7 +291,7 @@ sub complement { =head3 qa Usage: $ml->qa( qw( question1 answer1 question2 answer2 ) ); - + =cut sub qa { @@ -340,11 +337,11 @@ sub ra_correct_ans { =head3 cmp Usage ANS($ml -> cmp); - + provides a MathObject like comparison method returns a string of comparison methods for checking the list object -=cut +=cut sub cmp { my $self = shift; diff --git a/lib/Match.pm b/lib/Match.pm index bcca81fe38..e79ece8f6c 100644 --- a/lib/Match.pm +++ b/lib/Match.pm @@ -179,12 +179,10 @@ ra_correct_ans variable. =cut -BEGIN { - be_strict(); -} - package Match; +use strict; + @Match::ISA = qw( ChoiceList ); # *** Subroutines which overload ChoiceList.pm *** diff --git a/lib/Matrix.pm b/lib/Matrix.pm index 676e6a0825..05b5afaa62 100644 --- a/lib/Matrix.pm +++ b/lib/Matrix.pm @@ -22,16 +22,10 @@ such as decompose_LR(). =cut -our $OPTION_ENTRY = $MatrixReal1::OPTION_ENTRY; -use strict; -# BEGIN { -# be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. -# -# } -use MatrixReal1; - package Matrix; -@Matrix::ISA = qw(MatrixReal1); +use parent MatrixReal1; + +use strict; use Carp; diff --git a/lib/Multiple.pm b/lib/Multiple.pm index 7e3686c4df..3ddd07798b 100644 --- a/lib/Multiple.pm +++ b/lib/Multiple.pm @@ -178,14 +178,10 @@ instead of radio_cmp(). =cut -BEGIN { - be_strict(); -} - -#use strict; package Multiple; -@Multiple::ISA = (); +use strict; + @Multiple::ISA = qw( Exporter ChoiceList ); # *** Subroutines which overload ChoiceList.pm *** diff --git a/lib/PGUtil.pm b/lib/PGUtil.pm index 8e47069ee6..bfc435f3a8 100644 --- a/lib/PGUtil.pm +++ b/lib/PGUtil.pm @@ -1,6 +1,6 @@ ############################################################################### # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -12,229 +12,203 @@ # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the # Artistic License for more details. ################################################################################ -package PGUtil; - -################################## -# Utility macro -################################## -=head2 Utility Macros +package PGUtil; +use parent 'Exporter'; +=head1 NAME -=head4 not_null - - not_null(item) returns 1 or 0 - - empty arrays, empty hashes, strings containing only whitespace are all NULL and return 0 - all undefined quantities are null and return 0 +PGUtil.pm - Utility Methods +=head1 METHODS =cut use strict; use warnings; -use Exporter 'import'; -our @EXPORT = qw( - not_null - pretty_print -); - -sub not_null { # empty arrays, empty hashes and strings containing only whitespace are all NULL - # in modern perl // would be a reasonable and more robust substitute - # a function, not a method + +our @EXPORT_OK = qw(not_null pretty_print); + +=head2 not_null + +Usage: + + not_null($item) + +Returns 1 if C<$item> is not null, and 0 otherwise. Undefined quantities, empty +arrays, empty hashes, and strings containing only whitespace are null and return +0. + +=cut + +sub not_null { my $item = shift; - return 0 unless defined($item); + return 0 unless defined $item; if (ref($item) =~ /ARRAY/) { - return scalar(@{$item}); # return the length + return scalar(@$item); # return the length } elsif (ref($item) =~ /HASH/) { - return scalar(keys %{$item}); + return scalar(keys %$item); } else { # string case return 1 if none empty return ($item =~ /\S/) ? 1 : 0; } } -=head4 pretty_print +=head2 pretty_print - Usage: warn pretty_print( $rh_hash_input, displayMode, level) - TEXT(pretty_print($ans_hash, displayMode, level)); - TEXT(pretty_print(~~%envir, displayMode, level )); +Usage: -This can be very useful for printing out HTML messages about objects while debugging + pretty_print($rh_hash_input, $displayMode, $level) -=cut +This method is useful for displaying the contents of objects while debugging. + +The C<$displayMode> parameter should be one of "TeX", "text", or "html" +The default is "html". -# ^function pretty_print -# ^uses lex_sort -# ^uses pretty_print +The C<$level> parameter is the cut off for the depth into objects to show. The +default is 5. + +=cut sub pretty_print { - my $r_input = shift; - my $displayMode = shift // 'html'; # default printing style is html - my $level = shift // 5; # default is 5 levels deep - my $out = ''; + my ($r_input, $displayMode, $level) = @_; + $displayMode //= 'html'; # default printing style is html + $level //= 5; # default is 5 levels deep if ($displayMode eq 'TeX') { - $out .= "{\\tiny"; - $out .= pretty_print_tex($r_input, $level); - $out .= "}"; + return '{\\tiny' . pretty_print_tex($r_input, $level) . '}'; } elsif ($displayMode eq 'text') { - $out = pretty_print_text($r_input, $level); + return pretty_print_text($r_input, $level); } else { - $out = pretty_print_html($r_input, $level); #default + return pretty_print_html($r_input, $level); #default } - $out; } -sub pretty_print_html { # provides html output -- NOT a method - my $r_input = shift; - return '' unless defined $r_input; - my $level = shift; - $level--; - return "PGalias has too much info. Try \$PG->{PG_alias}->{resource_list}" - if ref($r_input) eq 'PGalias'; # PGalias just has too much information - return 'too deep' unless $level > 0; # only print four levels of hashes (safety feature) - my $out = ''; - # protect against modules defined in Safe which can't find their stringify procedure. - my $dummy = eval {"$r_input"}; - if ($@) { - $out = "Unable to determine stringify for this item\n"; - $out .= $@ . "\n"; - return ($out); - } +# Note that the following methods use `eval { %$r_input || 1 }` to detect all objectes that can be accessed like a hash. +# `ref $r_input` will not see blessed objects that can be accessed like a hash. Previously `"$r_input" =~ /hash/i` was +# used. This will also detect strings containing the word hash, and will cause errors. - if (not ref($r_input)) { - $out = $r_input if defined $r_input; # not a reference - $out =~ s/"; - - foreach my $key (sort (keys %$r_input)) { - $out .= - "
                      "; - } - $out .= "
                      $key=> " - . pretty_print_html($r_input->{$key}, $level) - . "
                      "; - } elsif (ref($r_input) eq 'ARRAY') { - my @array = @$r_input; - $out .= "( "; - while (@array) { - $out .= pretty_print_html(shift @array, $level) . " , "; - } - $out .= " )"; - } elsif (ref($r_input) eq 'CODE') { - $out = "$r_input"; +sub pretty_print_html { # provides html output -- NOT a method + my ($r_input, $level) = @_; + return 'undef' unless defined $r_input; + + my $ref = ref $r_input; + + # Don't display PGalias. It has too much information. + return 'PGalias has too much info. Try $PG->{PG_alias}{resource_list}' if $ref eq 'PGalias'; + + --$level; + return 'too deep' unless $level > 0; + + # Protect against modules defined in Safe which can't find their stringify procedure. + return "Unable to determine stringify for this item.\n$@\n" if !eval { "$r_input" || 1 } || $@; + + if (!$ref) { + return $r_input =~ s/' + . ($ref eq 'HASH' + ? '' + : '
                      ' + . "$ref
                      ") + . '
                      ' + . join( + '', + map { + '
                      ' + . ($_ =~ s/' + . qq{
                      =>
                      } + . qq{
                      } + . pretty_print_html($r_input->{$_}, $level) + . '
                      ' + } + sort keys %$r_input + ) . '
                      '; + } elsif ($ref eq 'ARRAY') { + return '[ ' . join(', ', map { pretty_print_html($_, $level) } @$r_input) . ' ]'; + } elsif ($ref eq 'CODE') { + return 'CODE'; } else { - $out = $r_input; - $out =~ s/{PG\\_alias}->{resource\\_list}" - if ref($r_input) eq 'PGalias'; # PGalias just has too much information - return 'too deep' unless $level > 0; #only print four levels of hashes (safety feature) - - my $protect_tex = sub { my $str = shift; $str =~ s/_/\\\_/g; $str }; - - my $out = ''; - my $dummy = eval {"$r_input"}; - if ($@) { - $out = "Unable to determine stringify for this item\n"; - $out .= $@ . "\n"; - return ($out); - } - - if (not ref($r_input)) { - $out = $r_input if defined $r_input; - $out =~ s/_/\\\_/g; # protect tex - $out =~ s/&/\\\&/g; - $out =~ s/\$/\\\$/g; - #FIXME -- how should mathobjects be handled?? - } elsif ("$r_input" =~ /hash/i) - { # ref($r_input) or "$r_input" will pick up objects whose '$self' is hash and so works better than ref($r_iput). - local ($^W) = 0; - - $out .= "\\begin{tabular}{| l | l |}\\hline\n\\multicolumn{2}{|l|}{$r_input}\\\\ \\hline\n"; - - foreach my $key (sort (keys %$r_input)) { - $out .= &$protect_tex($key) . " & " . pretty_print_tex($r_input->{$key}, $level) . "\\\\ \\hline\n"; - } - $out .= "\\end{tabular}\n"; - } elsif (ref($r_input) eq 'ARRAY') { - my @array = @$r_input; - $out .= "( "; - while (@array) { - $out .= pretty_print_tex(shift @array, $level) . " , "; - } - $out .= " )"; - } elsif (ref($r_input) eq 'CODE') { - $out = "$r_input"; + my ($r_input, $level) = @_; + return 'undef' unless defined $r_input; + + my $ref = ref($r_input); + + # Don't display PGalias. It has too much information. + return 'PGalias has too much info. Try \\$PG->{PG\\_alias}->{resource\\_list}' if $ref eq 'PGalias'; + + --$level; + return 'too deep' unless $level > 0; + + # Protect against modules defined in Safe which can't find their stringify procedure. + return "Unable to determine stringify for this item.\n$@\n" if !eval { "$r_input" || 1 } || $@; + + my $protect_tex = sub { my $str = shift; return (($str =~ s/_/\\\_/gr) =~ s/&/\\\&/gr) =~ s/\$/\\\$/gr; }; + + # Note: Do not add newlines to this. If this is in a PGML section + # those will cause errors due to PGML's catcode hackery. + if (!$ref) { + return $protect_tex->($r_input); + } elsif (eval { %$r_input || 1 }) { + return + "\\begin{tabular}{|l|l|}\\hline " + . ($ref eq 'HASH' ? '' : "\\multicolumn{2}{|l|}{" . $protect_tex->($ref) . "}\\\\ \\hline ") + . join('', + map { $protect_tex->($_) . " & " . pretty_print_tex($r_input->{$_}, $level) . "\\\\ \\hline "; } + sort (keys %$r_input)) + . "\\end{tabular}"; + } elsif ($ref eq 'ARRAY') { + return '[ ' . join(', ', map { pretty_print_tex($_, $level) } @$r_input) . ' ]'; + } elsif ($ref eq 'CODE') { + return 'CODE'; } else { - $out = $r_input if defined $r_input; - $out =~ s/_/\\\_/g; # protect tex - $out =~ s/&/\\\&/g; + return $protect_tex->($r_input); } - $out; } sub pretty_print_text { - my $r_input = shift; - my $level = shift; - return '' unless defined $r_input; - $level--; - return "PGalias has too much info. Try \\\$PG->{PG\\_alias}->{resource\\_list}" - if ref($r_input) eq 'PGalias'; # PGalias just has too much information - return 'too deep' unless $level > 0; #only print four levels of hashes (safety feature) - - my $out = ""; - my $dummy = eval {"$r_input"}; - if ($@) { - $out = "Unable to determine stringify for this item\n"; - $out .= $@ . "\n"; - return ($out); - } - - my $type = ref($r_input); - - if (defined($type) and $type) { - $out .= " type = $type; "; - } elsif (!defined($r_input)) { - $out .= " type = UNDEFINED; "; - } - return $out . " " unless defined($r_input); - - if (ref($r_input) =~ /HASH/ or "$r_input" =~ /HASH/) { - $out .= "{\n"; - $level++; - foreach my $key (sort keys %{$r_input}) { - $out .= " " x $level . "$key => " . pretty_print_text($r_input->{$key}, $level) . "\n"; - } - $level--; - $out .= "\n" . " " x $level . "}\n"; - - } elsif (ref($r_input) =~ /ARRAY/ or "$r_input" =~ /ARRAY/) { - $out .= " ( "; - foreach my $elem (@{$r_input}) { - $out .= pretty_print_text($elem, $level); - - } - $out .= " ) \n"; - } elsif (ref($r_input) =~ /SCALAR/) { - $out .= "scalar reference " . ${$r_input}; - } elsif (ref($r_input) =~ /Base64/) { - $out .= "base64 reference " . $$r_input; + my ($r_input, $level, $print_level) = @_; + return 'undef' unless defined $r_input; + + my $ref = ref($r_input); + + # Don't display PGalias. It has too much information. + return 'PGalias has too much info. Try $PG->{PG_alias}->{resource_list}' if $ref eq 'PGalias'; + + --$level; + return 'too deep' unless $level > 0; + + # Protect against modules defined in Safe which can't find their stringify procedure. + return "Unable to determine stringify for this item.\n$@\n" if !eval { "$r_input" || 1 } || $@; + + $print_level //= 1; + + if (!$ref) { + return $r_input; + } elsif (eval { %$r_input || 1 }) { + return + ($ref eq 'HASH' ? '' : "$ref ") . "{\n" + . join(",\n", + map { (' ' x $print_level) . "$_ => " . pretty_print_text($r_input->{$_}, $level, $print_level + 1) } + sort keys %$r_input) + . "\n" + . (' ' x ($print_level - 1)) . "}"; + } elsif ($ref eq 'ARRAY') { + return + "[\n" + . join(",\n", map { (' ' x $print_level) . pretty_print_text($_, $level, $print_level + 1) } @$r_input) + . "\n" + . (' ' x ($print_level - 1)) . "]"; + } elsif ($ref eq 'CODE') { + return 'CODE'; } else { - $out .= $r_input; + return $r_input; } - - return $out . " "; } 1; diff --git a/lib/PGalias.pm b/lib/PGalias.pm index ec07bed843..26593aec8a 100644 --- a/lib/PGalias.pm +++ b/lib/PGalias.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -14,682 +14,378 @@ ################################################################################ package PGalias; +use parent PGcore; # This is so that a PGalias object can call the PGcore warning_message and debug_message methods. + use strict; -use Exporter; +use warnings; + use UUID::Tiny ':std'; use PGcore; use PGresource; -our @ISA = qw ( PGcore ); # look up features in PGcore -- in this case we want the environment. - -=head2 new - - Create one alias object per question (and per PGcore object since there is a unique PGcore per question.) - Check that information is intact - Construct unique id stub seeds -- the id stub seed is for this PGalias object which is - attached to all the resource files (except equations) for this question. - Maintain list of links to external resources - -=cut - sub new { - my $class = shift; - my $envir = shift; #pointer to environment hash - my %options = @_; - warn "PGlias must be called with an environment" unless ref($envir) =~ /HASH/; - my $self = { - envir => $envir, - search_list => [ { url => 'foo', dir => '.' } ], # for subclasses -> list of url/directories to search - resource_list => {}, - %options, - - }; - bless $self, $class; - $self->initialize; - $self->check_parameters; - return $self; -} - -sub add_resource { - my $self = shift; - my ($aux_file_id, $resource) = @_; - if (ref($resource) =~ /PGresource/) { - $self->{resource_list}->{$aux_file_id} = $resource; - #$self->debug_message("$aux_file_id resource added"); - } else { - $self->warning_message("$aux_file_id does not refer to a a valid resource $resource"); - } -} + my ($class, $envir, %options) = @_; + warn 'PGlias must be called with an environment' unless ref($envir) =~ /HASH/; + my $self = bless { envir => $envir, resource_list => {}, %options }, $class; -sub get_resource { - my $self = shift; - my $aux_file_id = shift; - $self->{resource_list}->{$aux_file_id}; -} - -# methods -# make_alias -- outputs url and does what needs to be done -# normalize paths (remove extra precursors to the path) -# search directories for item -# make_links -- in those cases where links need to be made -# create_files -- e.g. when printing hardcopy -# dispatcher -- decides what needs to be done based on displayMode and file type -# alias_for_html -# alias_for_image_in_html image includes gif, png, jpg, swf, svg, flv?? ogg??, js -# alias_for_image_in_tex - -sub initialize { - my $self = shift; - my $envir = $self->{envir}; - - $self->{pgFileName} = $envir->{probFileName} // ''; + $self->{probFileName} = $envir->{probFileName} // ''; $self->{htmlDirectory} = $envir->{htmlDirectory}; $self->{htmlURL} = $envir->{htmlURL}; $self->{tempDirectory} = $envir->{tempDirectory}; $self->{templateDirectory} = $envir->{templateDirectory}; $self->{tempURL} = $envir->{tempURL}; - $self->{psvn} = $envir->{psvn}; $self->{displayMode} = $envir->{displayMode}; - $self->{problemSeed} = $envir->{problemSeed}; - $self->{problemUUID} = $envir->{problemUUID} // 0; - # Find auxiliary files even when the main file is in templates/tmpEdit - # FIXME: This shouldn't be done here. Instead the front end should pass in the problem source with the file name. + # Find auxiliary files even when the main file is in templates/tmpEdit. + # FIXME: This shouldn't be done here. Instead the front end should pass in the problem source with the file name. # The other instance of this in PGloadfiles.pm needs to be removed. - $self->{pgFileName} =~ s!(^|/)tmpEdit/!$1!; - - $self->{ext} = ''; + $self->{probFileName} =~ s!(^|/)tmpEdit/!$1!; - # Create an ID which is unique for the given psvn, problemSeed, and problemUUID. - # It is the responsibility of the caller to pass in a problemUUID that will provide the required uniqueness. - # That could include a course name, a student login name, etc. + # Create an ID which is unique for the given psvn, problemSeed, and problemUUID. It is the responsibility of the + # caller to pass in a problemUUID that will provide the required uniqueness. That could include a course name, a + # student login name, etc. $self->{unique_id_stub} = create_uuid_as_string(UUID_V3, UUID_NS_URL, - join('-', $self->{psvn}, $self->{problemSeed}, $self->{problemUUID})); -} + join('-', $envir->{psvn} // (), $envir->{problemSeed}, $envir->{problemUUID} // ())); + + # Check the parameters. + $self->warning_message('The displayMode is not defined') unless $self->{displayMode}; + $self->warning_message('The htmlDirectory is not defined.') unless $self->{htmlDirectory}; + $self->warning_message('The htmlURL is not defined.') unless $self->{htmlURL}; + $self->warning_message('The tempURL is not defined.') unless $self->{tempURL}; -sub check_parameters { - my $self = shift; + return $self; +} - # Problem specific data - $self->warning_message('The current problem set version number (psvn) is not defined') - unless defined $self->{psvn}; - $self->warning_message('The displayMode is not defined') unless $self->{displayMode}; +# This cache's auxiliary files within a single PG problem. +sub add_resource { + my ($self, $aux_file_id, $resource) = @_; + if (ref($resource) =~ /PGresource/) { + $self->{resource_list}{$aux_file_id} = $resource; + } else { + $self->warning_message(qq{"$aux_file_id" does not refer to a valid resource.}); + } + return; +} - # required directory addresses (and URL address) - warn 'htmlDirectory is not defined.' unless $self->{htmlDirectory}; - warn 'htmlURL is not defined.' unless $self->{htmlURL}; - warn 'tempURL is not defined.' unless $self->{tempURL}; +sub get_resource { + my ($self, $aux_file_id) = @_; + return $self->{resource_list}{$aux_file_id}; } sub make_resource_object { - my $self = shift; - my $aux_file_id = shift; - my $ext = shift; - my $resource = PGresource->new( - $self, #parent alias of resource + my ($self, $aux_file_id, $ext) = @_; + return PGresource->new( + $self, # parent alias of resource $aux_file_id, # resource file name $ext, # resource type - WARNING_messages => $self->{WARNING_messages}, #connect warning message channels + WARNING_messages => $self->{WARNING_messages}, # connect warning message channels DEBUG_messages => $self->{DEBUG_messages}, ); - return $resource; } -=head2 make_alias - -This is the workhorse of the PGalias module. It's front end is alias() in PG.pl. - -make_alias magically takes a name of an external resource ( html file, png file, etc.) -and creates full directory addresses and uri's appropriate to the current displayMode. -It also does any necessary conversions behind the scenes. - -Returns the uri of the resource. - -=cut - sub make_alias { - my $self = shift; - my $aux_file_id = shift; - #$self->debug_message("make alias for file $aux_file_id"); - $self->warning_message("Empty string used as input into the function alias") unless $aux_file_id; - - my $displayMode = $self->{displayMode}; - - # $adr_output is a url in HTML mode - # and a complete directory path in TEX mode. - my $adr_output; - my $ext = ''; - -####################################################################### - # determine file type - # determine display mode - # dispatch -####################################################################### - # determine extension, if there is one - # if extension exists use the value for $ext - # files without extensions are flagged with errors. - # The extension is retained as part of aux_file_id - - #$self->debug_message("This auxiliary file id is $aux_file_id" ); + my ($self, $aux_file_id) = @_; + $self->warning_message('Empty string used as input into the function alias') unless $aux_file_id; + + # Determine the file extension, if there is one. Files without extensions are flagged with errors. + my $ext; if ($aux_file_id =~ m/\.([^\.]+)$/) { $ext = $1; } else { - $self->warning_message("The file name $aux_file_id did not have an extension.
                      " - . "Every file name used as an argument to alias must have an extension.
                      " - . "The permissable extensions are .jpg, .pdf, .gif, .png, .mpg, .mp4, .ogg, .webm and .html .
                      "); - $ext = undef; - return undef; #quit; + $self->warning_message(qq{The file name "$aux_file_id" does not have an extension. } + . 'Every file name used as an argument to alias must have an extension. The permissable extensions are ' + . '.gif, .jpg, .png, .svg, .pdf, .mp4, .mpg, .ogg, .webm, .css, .js, .nb, .csv, .tgz, and .html.'); + return; } - # $self->debug_message("This auxiliary file id is $aux_file_id of type $ext" ); - -################################################################### - # Create resource object -################################################################### - #$self->debug_message("creating resource with id $aux_file_id"); - - ################################################################### - # This section checks to see if a resource exists (in this question) - # for this particular aux_file_id. - # If so, we simply return the appropriate uri for the file. - # The displayMode will be the same throughout the processing of the .pg file - # This effectively cache's auxiliary files within a single PG question. - ################################################################### - unless (defined $self->get_resource($aux_file_id)) { - $self->add_resource( - $aux_file_id, - $self->make_resource_object( - $aux_file_id, # resource file name - $ext # resource type - ) - - ); + # Checks to see if a resource exists for this particular aux_file_id. + # If not, then create one. Otherwise, return the URI for the existing resource. + unless (defined $self->get_resource($aux_file_id)) { + $self->add_resource($aux_file_id, $self->make_resource_object($aux_file_id, $ext)); } else { - #$self->debug_message( "found existing resource_object $aux_file_id"); - return $self->get_resource($aux_file_id)->uri(); + return $self->get_resource($aux_file_id)->uri; } - ################################################################### + + # $output_location is a URL in HTML mode and a complete directory path in TeX mode. + my $output_location; if ($ext eq 'html') { - $adr_output = $self->alias_for_html($aux_file_id, $ext); - } elsif ($ext =~ /^(gif|jpg|png|svg|pdf|mp4|mpg|ogg|webm|css|js|nb|tgz)$/) { - if ($displayMode =~ /^HTML/ or $displayMode eq 'PTX') { - $adr_output = $self->alias_for_html($aux_file_id, $ext); - } elsif ($displayMode eq 'TeX') { - ################################################################################ - # .gif FILES in TeX mode - ################################################################################ - $adr_output = $self->alias_for_tex($aux_file_id, $ext); + $output_location = $self->alias_for_html($aux_file_id, $ext); + } elsif ($ext =~ /^(gif|jpg|png|svg|pdf|mp4|mpg|ogg|webm|css|js|nb|csv|tgz)$/) { + if ($self->{displayMode} =~ /^HTML/ or $self->{displayMode} eq 'PTX') { + $output_location = $self->alias_for_html($aux_file_id, $ext); + } elsif ($self->{displayMode} eq 'TeX') { + $output_location = $self->alias_for_tex($aux_file_id, $ext); } else { - die "Error in alias: PGalias.pm: unrecognizable displayMode = $displayMode"; + $self->warning_message("Error creating resource alias. Unrecognizable displayMode: $self->{displayMode}"); } - # } elsif ($ext eq 'svg') { - # if ($displayMode =~/HTML/) { - # $self->warning_message("The image $aux_file_id of type $ext cannot yet be displayed in HTML mode"); - # # svg images need an embed tag not an image tag -- need to modify image for this also - # # an alternative (not desirable) is to convert svg to png - # } elsif ($displayMode eq 'TeX') { - # $self->warning_message("The image $aux_file_id of type $ext cannot yet be displayed in TeX mode"); - # } else { - # die "Error in alias: PGalias.pm: unrecognizable displayMode = $displayMode"; - # } - - } else { # $ext is not recognized - ################################################################################ - # FILES with unrecognized file extensions in any display modes - ################################################################################ - - warn "Error in the macro alias. Alias does not understand how to process files with extension $ext. - (Path to problem file is " . $self->{pgFileName} . ") "; + } else { + # $ext is not recognized + $self->warning_message(qq{Error creating resource alias. Files with extension "$ext" are not allowed.\n} + . qq{(Path to problem file is "$self->{probFileName}".)}); } - $self->warning_message( - "The macro alias was unable to form a URL for the auxiliary file |$aux_file_id| used in this problem.") - unless $adr_output; + $self->warning_message(qq{Unable to form a URL for the auxiliary file "$aux_file_id" used in this problem.}) + unless $output_location; - # $adr_output is a url in HTML modes - # and a complete path in TEX mode. - my $resource_object = $self->get_resource($aux_file_id); - # TEXT(alias() ) is expecting only a single item not an array - # so the code immediately below for adding extra information to alias is a bad idea. - #return (wantarray) ? ($adr_output, $resource_object): $adr_output; - # Instead we'll implement a get_resource() command in PGcore and PG - return ($adr_output); + return $output_location; } sub alias_for_html { - my $self = shift; #handed alias object - my $aux_file_id = shift; #handed the name of the resource object - # case 1: aux_file_id is complete or relative path to file - # case 2: aux_file_id is file name alone relative to the templates directory. - my $ext = shift; - #$self->debug_message("handling $aux_file_id of type $ext"); -####################### - # gather needed data and declare it locally -####################### - my $htmlURL = $self->{htmlURL}; - my $htmlDirectory = $self->{htmlDirectory}; - my $pgFileName = $self->{pgFileName}; - my $tempURL = $self->{tempURL}; - my $tempDirectory = $self->{tempDirectory}; - my $templateDirectory = $self->{templateDirectory}; - -####################### - # retrieve PGresponse resource object -####################### - my ($resource_uri); - my $resource_object = $self->get_resource($aux_file_id); - # $self->debug_message( "\nresource for $aux_file_id is ", ref($resource_object), $resource_object ); + my ($self, $aux_file_id, $ext) = @_; -############################################## - # Find complete path to the original files -############################################## + my $resource_object = $self->get_resource($aux_file_id); - # get the directories that might contain html files - my $dirPath = ''; - if ($ext eq 'html') { - $dirPath = 'htmlPath'; - } else { - $dirPath = 'imagesPath'; + if ($aux_file_id =~ /https?:/) { + # External URL. + $resource_object->uri($aux_file_id); + return $resource_object->uri; # External URLs need no further processing. } - my @aux_files_directories = @{ $self->{envir}->{$dirPath} }; - if ($pgFileName) { + # Get the directories that might contain auxiliary files. + my @aux_files_directories = @{ $self->{envir}{ $ext eq 'html' ? 'htmlPath' : 'imagesPath' } }; + if ($self->{probFileName}) { # Replace "." with the current pg problem file directory. - my $current_pg_directory = $self->directoryFromPath($pgFileName); - $current_pg_directory = $self->{templateDirectory} . $current_pg_directory; - @aux_files_directories = map { $_ eq '.' ? $current_pg_directory : $_ } @aux_files_directories; + @aux_files_directories = + map { $_ eq '.' ? $self->{templateDirectory} . $self->directoryFromPath($self->{probFileName}) : $_ } + @aux_files_directories; } else { @aux_files_directories = grep { $_ ne '.' } @aux_files_directories; } - # Find complete path to the original file - my $file_path; - if ($aux_file_id =~ /https?:/) { #external link_file - $resource_object->uri($aux_file_id); #no unique id is needed -- external link doc - $resource_object->{copy_link}->{type} = 'external'; - $resource_object->{uri}{is_accessible} = 1; # Assume a url is accessible. - return $resource_object->uri; # external links need no further processing - } elsif ($aux_file_id =~ m|^/|) { - $file_path = $aux_file_id; - } else { - $file_path = $self->find_file_in_directories($aux_file_id, \@aux_files_directories); + # Find the complete path to the original file. + my $file_path = + $aux_file_id =~ m|^/| ? $aux_file_id : $self->find_file_in_directories($aux_file_id, \@aux_files_directories); + + unless ($file_path) { + $self->warning_message(qq{Unable to find file "$aux_file_id".}); + return; } - # $self->debug_message("file path is $file_path"); - -##################### Case1: we've got a full pathname to a file in either the temp directory or the htmlDirectory -##################### Case2: we assume the file is in the same directory as the problem source file -##################### Case3: the file could have an external url - -############################################## - # store the complete path to the original file - # calculate the uri (which is a url suitable for the browser relative to the current site) - # store the uri. - # record status of the resource -############################################## - if ($file_path =~ m|^$tempDirectory|) { #case: file is stored in the course temporary directory - $resource_uri = $file_path; - $resource_uri =~ s|$tempDirectory|$tempURL|; - $resource_object->uri($resource_uri); #no unique id is needed -- public doc + + # Store the complete path to the original file, and calculate and store the URI (which is a URL suitable for the + # browser relative to the current site). + if ($file_path =~ m|^$self->{tempDirectory}|) { + # File is in the course temporary directory. + $resource_object->uri($file_path =~ s|$self->{tempDirectory}|$self->{tempURL}|r); $resource_object->path($file_path); - $resource_object->{copy_link}->{type} = 'orig'; - $resource_object->{path}->{is_complete} = (-r $resource_object->path); - } elsif ($file_path =~ m|^$htmlDirectory|) { #case: file is under the course html directory - $resource_uri = $file_path; - $resource_uri =~ s|$htmlDirectory|$htmlURL|; - $resource_object->uri($resource_uri); + } elsif ($file_path =~ m|^$self->{htmlDirectory}|) { + # File is in the course html directory. + $resource_object->uri($file_path =~ s|$self->{htmlDirectory}|$self->{htmlURL}|r); $resource_object->path($file_path); - $resource_object->{copy_link}->{type} = 'orig'; - $resource_object->{path}->{is_complete} = (-r $resource_object->path); - #################################################### - # one can add more public locations such as the site htdocs directory here in the elsif chain - #################################################### - } else { #case: resource is in a directory which is not public - # most often this is the directory containing the .pg file - # these files require a link to the temp Directory - # $self->debug_message("source file path ", $sourceFilePath); + } else { + # Resource is in a directory which is not public. + # Most often this is the directory containing the .pg file. + # These files need to be linked to from the public html temporary directory. $resource_object->path($file_path); - $resource_object->{copy_link}->{type} = 'link'; - $resource_object->{path}->{is_complete} = 0; - $resource_object->{uri}->{is_complete} = 0; - warn "$ext not defined" unless $ext; + $self->warning_message("File extension for resource $file_path is not defined") unless $ext; $resource_object->create_unique_id($ext); - # notice the resource uri is not yet defined -- we have to make the link first - } -############################################## - # Create links for objects of "link" type. - # between private directories such as myCourse/template - # and public directories (such as wwtmp/courseName or myCourse/html - # The location of the links depends on the type and location of the file -############################################## - # create_link_to_tmp_file() - #input: resource object, ext, (html) (tempURL), - #return: uri - if ($resource_object->{copy_link}->{type} eq 'link') { - # this creates a link from the original file to an alias in the tmp/html directory - # and places information about the path and the uri in the PGresponse object $resource_object - my $subdir = ''; - if ($ext eq 'html') { - $subdir = 'html'; - } else { - $subdir = 'images'; - } - $self->create_link_to_tmp_file(resource => $resource_object, subdir => $subdir); + # Create a link from the original file to an alias in the temporary public html directory. + $self->create_link_to_tmp_file($resource_object, $ext eq 'html' ? 'html' : 'images'); } -################################################################################ - # Return full url to image file (resource_id) -################################################################################ - $resource_object->uri(); # return the uri of the resource -- in this case the URL for the file in the temp directory + # Return the URI of the resource. + return $resource_object->uri; } -################################################################################ -# alias for image in tex mode -################################################################################ - sub alias_for_tex { - my $self = shift; #handed alias object - my $aux_file_id = shift; #handed the name of the resource object - # case 1: aux_file_id is complete or relative path to file - # case 2: aux_file_id is file name alone relative to the templates directory. - my $ext = shift; - - my $from_file_type = $ext; - my $to_file_type = "png"; # needed for conversion cases - - my $convert_fileQ = ( - $ext eq 'gif' # gif files need to be converted - # or $ext eq 'pdf' # other image types for tex - # or $ext eq 'jpg' - # or $ext eq 'svg' - # or $ext eq 'html' - ) ? 1 : 0; # does this file need conversion - - my $link_fileQ = 0; # does this file need to be linked? - my $targetDirectory = ($ext eq 'html') ? 'html' : 'images'; # subdirectory of tmp directory - -####################### - # gather needed data and declare it locally -####################### - my $htmlURL = $self->{htmlURL}; - my $htmlDirectory = $self->{htmlDirectory}; - my $pgFileName = $self->{pgFileName}; - my $tempURL = $self->{tempURL}; - my $tempDirectory = $self->{tempDirectory}; - my $templateDirectory = $self->{templateDirectory}; - -####################### - # retrieve PGresponse resource object -####################### - my ($resource_uri); - my $resource_object = $self->get_resource($aux_file_id); - #warn ( "\nresource for $aux_file_id is ", ref($resource_object), $resource_object ); + my ($self, $aux_file_id, $ext) = @_; -############################################## - # Find complete path to the original files -############################################## + my $resource_object = $self->get_resource($aux_file_id); - # get the directories that might contain html files - my $dirPath = ''; - if ($ext eq 'html') { - $dirPath = 'htmlPath'; - } else { - $dirPath = 'imagesPath'; + if ($aux_file_id =~ /https?:/) { + # External URL. + $resource_object->uri($aux_file_id); + return $resource_object->uri; # External URLs need no further processing. } - my @aux_files_directories = @{ $self->{envir}->{$dirPath} }; - if ($pgFileName) { + # Get the directories that might contain auxiliary files. + my @aux_files_directories = @{ $self->{envir}{ $ext eq 'html' ? 'htmlPath' : 'imagesPath' } }; + if ($self->{probFileName}) { # Replace "." with the current pg problem file directory. - my $current_pg_directory = $self->directoryFromPath($pgFileName); + my $current_pg_directory = $self->directoryFromPath($self->{probFileName}); $current_pg_directory = $self->{templateDirectory} . $current_pg_directory; @aux_files_directories = map { $_ eq '.' ? $current_pg_directory : $_ } @aux_files_directories; } else { @aux_files_directories = grep { $_ ne '.' } @aux_files_directories; } - # Find complete path to the original file - my $file_path; - if ($aux_file_id =~ /https?:/) { # external link_file - $resource_object->uri($aux_file_id); #no unique id is needed -- external link doc - $resource_object->{copy_link}->{type} = 'external'; - $resource_object->{uri}{is_accessible} = 1; # Assume a url is accessible. - return $resource_object->uri; # external links need no further processing - } elsif ($aux_file_id =~ m|^/|) { - $file_path = $aux_file_id; - } else { - $file_path = $self->find_file_in_directories($aux_file_id, \@aux_files_directories); + # Find complete path to the original file. + my $file_path = + $aux_file_id =~ m|^/| ? $aux_file_id : $self->find_file_in_directories($aux_file_id, \@aux_files_directories); + + unless ($file_path) { + $self->warning_message(qq{Unable to find "$aux_file_id" in any of the allowed auxiliary file directories.}); + return; } - #warn ("file path is $file_path"); - -##################### Case1: we've got a full pathname to a file in either the temp directory or the htmlDirectory -##################### Case2: we assume the file is in the same directory as the problem source file -##################### Case3: the file could have an external url - -############################################## - # store the complete path to the original file - # calculate the uri (which is a url suitable for the browser relative to the current site) - # store the uri. - # record status of the resource -############################################## - - if ($file_path =~ m|^$tempDirectory|) { #case: file is stored in the course temporary directory - - my $sourceFilePath = $file_path; - $resource_object->path($sourceFilePath); - $resource_object->{path}->{is_complete} = 1; - #warn("tempDir filePath ",$resource_object->path, "\n"); - # Gif files always need to be converted to png files for inclusion in pdflatex documents. - - $resource_object->{convert}->{needed} = $convert_fileQ; - $resource_object->{convert}->{from_path} = $sourceFilePath; - $resource_object->{convert}->{from_type} = $from_file_type; - $resource_object->{convert}->{to_path} = ''; #define later - $resource_object->{convert}->{to_type} = $to_file_type; - } elsif ($file_path =~ m|^$htmlDirectory|) { #case: file is under the course html directory - - my $sourceFilePath = $aux_file_id; - $resource_object->path($sourceFilePath); - $resource_object->{path}->{is_complete} = 1; - - $resource_object->{convert}->{needed} = $convert_fileQ; - $resource_object->{convert}->{from_path} = $sourceFilePath; - $resource_object->{convert}->{from_type} = $from_file_type; - $resource_object->{convert}->{to_path} = ''; #define later - $resource_object->{convert}->{to_type} = $to_file_type; - #warn ("htmlDir filePath ",$resource_object->path, "\n"); + # Store the complete path to the original file. + if ($file_path =~ m|^$self->{tempDirectory}|) { + # File is in the course temporary directory. + $resource_object->path($file_path); + } elsif ($file_path =~ m|^$self->{htmlDirectory}|) { + # File is in the course html directory. + $resource_object->path($aux_file_id); } else { - $resource_object->path($file_path); - $resource_object->{path}->{is_complete} = (-r $resource_object->path); - - $resource_object->{convert}->{needed} = $convert_fileQ; - $resource_object->{convert}->{from_path} = $resource_object->path(); - $resource_object->{convert}->{from_type} = $from_file_type; - $resource_object->{convert}->{to_path} = ''; #define later - $resource_object->{convert}->{to_type} = $to_file_type; - #warn ("templateDir filePath ",$resource_object->path, "\n"); - # notice the resource uri is not yet defined -- we have to make the link first } -################################################################################ - # Convert images to .png files if needed -################################################################################ - if ($resource_object->{convert}->{needed}) { #convert .gif to .png - - $self->convert_file_to_png_for_tex( - resource => $resource_object, - targetDirectory => $targetDirectory - ); - } else { # no conversion needed - $resource_object->uri($resource_object->path()); #path and uri are the same in this case. - $resource_object->{uri}->{is_complete} = 1; - $resource_object->{uri}->{is_accessible} = (-r $resource_object->uri()); + if ($ext eq 'gif' || $ext eq 'svg') { + # Convert gif and svg files to png files. + $self->convert_file_to_png_for_tex($resource_object, $ext eq 'html' ? 'html' : 'images'); + } else { + # Path and URI are the same in this case. + $resource_object->uri($resource_object->path); } -################################################################################ - # Don't need to create aliases in this case because nothing is being served over the web -################################################################################ - # Return full path to image file (resource_id) -################################################################################ - #warn ("final filePath ", $resource_object->uri(), "\n"); - #warn "file is a accessible ", $resource_object->{uri}->{is_accessible},"\n"; - # returns a file path - ($resource_object->{uri}->{is_accessible} == 1) ? $resource_object->uri() : ""; + # An alias is not needed in this case because nothing is being served over the web. + # Return the full path to the image file. + return $resource_object->uri && -r $resource_object->uri ? $resource_object->uri : ''; } -############################################################################ -# Utility for creating link from original file to alias in publically accessible temp directory -############################################################################ sub create_link_to_tmp_file { - my $self = shift; - my %args = @_; - my $resource_object = $args{resource}; - # warn "resource_object =", ref($resource_object); - my $unique_id = $resource_object->{unique_id}; - my $ext = $resource_object->{type}; - my $subdir = $args{subdir}; - my $link = "$subdir/$unique_id"; - ################# - # construct resource uri - ################# - my $resource_uri = $self->{tempURL}; - $resource_uri =~ s|/$||; #remove trailing slash, if any - $resource_uri = "$resource_uri/$link"; - ################# - # insure that linkPath exists and all intermediate directories have been created - ################# + my ($self, $resource_object, $subdir) = @_; + + my $ext = $resource_object->{type}; + my $link = "$subdir/$resource_object->{unique_id}"; + + # Insure that link path exists and all intermediate directories have been created. my $linkPath = $self->surePathToTmpFile($link); - if (-e $resource_object->path()) { - # if resource file exists - ################# - # destroy the old link. - ################# + if (-e $resource_object->path) { if (-e $linkPath) { - unlink($linkPath) || $self->warning_message("Unable to unlink alias file at |$linkPath|"); + # Destroy the old link. + unlink($linkPath) or $self->warning_message(qq{Unable to unlink alias file at "$linkPath".}); } - ################# - # create new link. - # create uri to this link - ################# - if (symlink($resource_object->path(), $linkPath)) { #create the symlink - $resource_object->{path}->{is_accessible} = 1; - $resource_object->{copy_link}->{link_to_path} = $linkPath; - $resource_object->{path}->{is_accessible} = (-r $linkPath); - - $resource_object->uri($resource_uri); - $resource_object->{uri}{is_accessible} = 1; # Assume a url is accessible. - $resource_object->{path}->{is_complete} = 1; - $resource_object->{uri}->{is_complete} = 1; + + # Create a new link, and the URI to this link. + if (symlink($resource_object->path, $linkPath)) { + $resource_object->uri(($self->{tempURL} =~ s|/$||r) . "/$link"); } else { $self->warning_message( - "The macro alias cannot create a link from |$linkPath| to |" . $resource_object->path() . "|
                      "); + qq{The macro alias cannot create a link from "$linkPath" to "} . $resource_object->path . '"'); } } else { - # if the resource file doesn't exist - my $message = ($resource_object->path()) ? " at |" . $resource_object->path() . "|" : " anywhere"; - $self->warning_message( - "The macro alias cannot find the file: |" . ($resource_object->fileName) . '|' . $message); - $resource_object->{path}->{is_accessible} = 0; - $resource_object->{uri}->{is_accessible} = 0; - # we should delete the resource object in this case? + $self->warning_message('Cannot find the file: "' + . $resource_object->fileName . '" ' + . ($resource_object->path ? ' at "' . $resource_object->path . '"' : ' anywhere')); } + return; } -############################################################################ -# Utility for converting .gif files to .png for tex -############################################################################ - sub convert_file_to_png_for_tex { - my $self = shift; - my %args = @_; - my $resource_object = $args{resource}; - my $targetDirectory = $args{targetDirectory}; - my $conversion_command = WeBWorK::PG::IO::externalCommand('gif2png'); - ################################################################################ - # Create path to new .png file - # Create new .png file - # We may not have permission to do this in the template directory - # so we create the file in the course temp directory. - ################################################################################ - my $ext = $resource_object->{type}; - $resource_object->create_unique_id($ext); - my $unique_id = $resource_object->{unique_id}; - $unique_id =~ s|\.[^/\.]*$|.png|; - my $link = "$targetDirectory/$unique_id"; - my $targetFilePath = $self->surePathToTmpFile($link); - $resource_object->{convert}->{to_path} = $targetFilePath; - $self->debug_message("target filePath ", $targetFilePath, "\n"); - my $sourceFilePath = $resource_object->{convert}->{from_path}; - $self->debug_message("convert filePath ", $sourceFilePath, "\n"); - # conversion_command is imported into this subroutine from the config files. - #$self->debug_message("cat $sourceFilePath | $conversion_command > $targetFilePath"); - my $returnCode = system "cat $sourceFilePath | $conversion_command > $targetFilePath"; - #$resource_object->debug_message( "FILE path $targetFilePath created =", -e $targetFilePath ); - #$resource_object->debug_message( "return Code $returnCode from cat $sourceFilePath | $command > $targetFilePath"); - if ($returnCode or not -e $targetFilePath) { + my ($self, $resource_object, $target_directory) = @_; + + $resource_object->create_unique_id($resource_object->{type}); + my $targetFilePath = + $self->surePathToTmpFile("$target_directory/" . ($resource_object->{unique_id} =~ s|\.[^/\.]*$|.png|r)); + $self->debug_message('target filePath ', $targetFilePath, "\n"); + my $sourceFilePath = $resource_object->path; + $self->debug_message('convert filePath ', $sourceFilePath, "\n"); + + my $conversion_command = WeBWorK::PG::IO::externalCommand('convert'); + my $returnCode = system "$conversion_command '${sourceFilePath}[0]' $targetFilePath"; + if ($returnCode || !-e $targetFilePath) { $resource_object->warning_message( - "returnCode $returnCode: failed to convert $sourceFilePath to $targetFilePath using gif->png with $conversion_command: $!" - ); + qq{Failed to convert "$sourceFilePath" to "$targetFilePath" using "$conversion_command": $!}); } - $resource_object->uri($resource_object->{convert}->{to_path}); - $resource_object->{uri}->{is_complete} = 1; - $resource_object->{uri}->{is_accessible} = (-r $resource_object->uri()); -} -################################################ - -# More resource search macros - -################################################ + $resource_object->uri($targetFilePath); -# -# Look for a macro file in the directories specified in the macros path -# - -# ^variable my $macrosPath -our ( - $macrosPath, - # ^variable my $pwd - $pwd, -); - -# ^function findMacroFile -# ^uses $macrosPath -# ^uses $pwd -sub findMacroFile { - my $self = shift; - my $fileName = shift; - my $filePath; - foreach my $dir (@{$macrosPath}) { - $filePath = "$dir/$fileName"; - $filePath =~ s!^\.\.?/!$pwd/!; # defined for PGloadFiles but not here - #FIXME? where is $pwd defined? why did it want to replace ../ with current directory - return $filePath if (-r $filePath); - } - return; # no file found + return; } sub find_file_in_directories { - my $self = shift; - my $file_name = shift; - my $directories = shift; - my $file_path; - foreach my $dir (@$directories) { - $dir =~ s|/$||; # remove final / if present - $file_path = "$dir/$file_name"; - return $file_path if (-r $file_path); + my ($self, $file_name, $directories) = @_; + for my $dir (@$directories) { + $dir =~ s|/$||; # Remove final / if present. + my $file_path = "$dir/$file_name"; + return $file_path if -r $file_path; } - return; # no file found -} - -# This is a stub for deprecated problems that call this method. Some of the Geogebra problems that do so actually work -# even though this method fails. -sub findAppletCodebase { - return ''; + return; # No file found. } 1; + +=head1 NAME + +PGalias - Create aliases for auxiliary resources. + +=head2 new + +Usage: C<< PGalias->new($envir, %options) >> + +The C constructor. The C<$envir> hash containing the problem +environment is required. The C<%options> can contain C and +C which should be array references. These are passed on to all +C objects constructed for each problem resource and are used by both +modules to store warning and debug messages. + +One C object is created for each C object (which is unique for +each problem). This object is used to construct unique ids for problem +resources and maintain a list of the resources used by a problem. A +unique_id_stub is generated for this C object which is the basis for +the unique ids generated for resource files (except equation images for the +"images" display mode) used by the problem. + +=head2 get_resource + +Usage: C<< $pgAlias->get_resource($aux_file_id) >> + +Returns the C object corresponding to C<$aux_file_id>. + +=head2 make_alias + +Usage: C<< $pgAlias->make_alias($aux_file_id) >> + +This is the workhorse of the C module. Its front end is C in +L. + +C takes the name of an auxiliary resource (html file, png file, +etc.) and creates a file name or URL appropriate to the current display mode. +It also does any necessary conversions behind the scenes. + +It returns the URL of the resource if the display mode is HTML or PTX, and the +full file path if the display mode is TeX. + +=head2 alias_for_html + +Usage: C<< $pgAlias->alias_for_html($aux_file_id, $ext) >> + +Returns the URL alias for the resource identified by C<$aux_file_id> with the +file name extension C<$ext>. + +=head2 alias_for_tex + +Usage: C<< $pgAlias->alias_for_tex($aux_file_id, $ext) >> + +Returns the full file path alias for the resource identified by C<$aux_file_id> +with the file name extension C<$ext>. + +=head2 create_link_to_tmp_file + +Usage: C<< $pgAlias->create_link_to_tmp_file($resource_object, $subdir) >> + +Creates a symbolic link in the subdirectory C<$subdir> of the publicly +accessible temporary directory to the file (usually in a course's templates +directory) represented by the C referenced by C<$resource_object>. +The link name is the file unique id alias. + +=head2 convert_file_to_png_for_tex + +Usage: C<< $pgAlias->convert_file_to_png_for_tex($resource_object, $target_directory) >> + +Converts a "gif" or "svg" file to a "png" file. The "png" file is saved in +C<$target_directory> and the file name is the unique id alias for the +C referenced by C<$resource_object>. + +=head2 find_file_in_directories + +Usage: C<< $pgAlias->find_file_in_directories($file_name, $directories) >> + +Finds the first directory in the array of directory names referenced to by +C<$directories> that contains a readable file named C<$file_name>, and returns +the full path of that file. + +=cut diff --git a/lib/PGanswergroup.pm b/lib/PGanswergroup.pm index b47abc3bd4..db14608aae 100644 --- a/lib/PGanswergroup.pm +++ b/lib/PGanswergroup.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/PGcore.pm b/lib/PGcore.pm index b9f7e809bd..ade9a9fb8c 100755 --- a/lib/PGcore.pm +++ b/lib/PGcore.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -32,7 +32,7 @@ use PGrandom; use PGalias; use PGloadfiles; use AnswerHash; -use WeBWorK::PG::IO(); # don't important any command directly +require WeBWorK::PG::IO; use Tie::IxHash; use MIME::Base64(); use PGUtil(); @@ -71,37 +71,35 @@ sub new { OUTPUT_ARRAY => [], # holds output body text HEADER_ARRAY => [], # holds output for the header text POST_HEADER_ARRAY => [], - # PG_ANSWERS => [], # holds answers with labels # deprecated - # PG_UNLABELED_ANSWERS => [], # holds unlabeled ans. #deprecated -replaced by PG_ANSWERS_HASH - PG_ANSWERS_HASH => {}, # holds label=>answer pairs + PG_ANSWERS_HASH => {}, # holds label=>answer pairs # Holds other data, besides answers, which persists during a session and beyond. - PERSISTENCE_HASH => $envir->{PERSISTENCE_HASH} // {}, # Main data, received from DB - PERSISTENCE_HASH_UPDATED => {}, # Keys whose updated values should be saved by the DB - - answer_eval_count => 0, - answer_blank_count => 0, - unlabeled_answer_blank_count => 0, - unlabeled_answer_eval_count => 0, - KEPT_EXTRA_ANSWERS => [], - ANSWER_PREFIX => 'AnSwEr', - ARRAY_PREFIX => 'ArRaY', - vec_num => 0, # for distinguishing matrices - QUIZ_PREFIX => $envir->{QUIZ_PREFIX}, - PG_VERSION => $ENV{PG_VERSION}, - PG_ACTIVE => 1, # toggle to zero to stop processing - submittedAnswers => 0, # have any answers been submitted? is this the first time this session? - PG_session_persistence_hash => {}, # stores data from one invoction of the session to the next. - PG_original_problem_seed => 0, - PG_random_generator => undef, - PG_alias => undef, - PG_problem_grader => undef, - displayMode => undef, - envir => $envir, - WARNING_messages => [], - DEBUG_messages => [], - names_created => 0, - external_refs => {}, # record of external references + PERSISTENCE_HASH => $envir->{PERSISTENCE_HASH} // {}, # Main data, received from DB + PERSISTENCE_HASH_UPDATED => {}, # Keys whose updated values should be saved by the DB + answer_name_count => 0, + implicit_named_answer_stack => [], + implicit_answer_eval_stack => [], + explicit_answer_name_evals => {}, + KEPT_EXTRA_ANSWERS => [], + ANSWER_PREFIX => 'AnSwEr', + ARRAY_PREFIX => 'ArRaY', + vec_num => 0, # for distinguishing matrices + QUIZ_PREFIX => $envir->{QUIZ_PREFIX}, + PG_VERSION => $ENV{PG_VERSION}, + PG_ACTIVE => 1, # toggle to zero to stop processing + submittedAnswers => 0, # have any answers been submitted? is this the first time this session? + PG_session_persistence_hash => {}, # stores data from one invoction of the session to the next. + PG_original_problem_seed => 0, + PG_random_generator => undef, + PG_alias => undef, + PG_problem_grader => undef, + displayMode => undef, + content_post_processors => [], + envir => $envir, + WARNING_messages => [], + DEBUG_messages => [], + names_created => 0, + external_refs => {}, # record of external references %options, # allows overrides and initialization }; bless $self, $class; @@ -125,9 +123,7 @@ sub initialize { WARNING_messages => $self->{WARNING_messages}, DEBUG_messages => $self->{DEBUG_messages}, ); - #$self->{maketext} = WeBWorK::Localize::getLoc($self->{envir}->{language}); - $self->{maketext} = $self->{envir}->{language_subroutine}; - #$self->debug_message("PG alias created", $self->{PG_alias} ); + $self->{maketext} = $self->{envir}{language_subroutine}; $self->{PG_loadMacros} = new PGloadfiles($self->{envir}); $self->{flags} = { showPartialCorrectAnswers => 1, @@ -161,16 +157,16 @@ resulting HTML page. See HEADER_TEXT() below. =item * -Implicitly-labeled answers: Answers that have not been explicitly +Implicitly-named answers: Answers that have not been explicitly assigned names, and are associated with their answer blanks by the order in which they appear in the problem. These types of answers are designated using -the ANS() macro. +the C method. =item * -Explicitly-labeled answers: Answers that have been explicitly assigned -names with the LABELED_ANS() macro, or a macro that uses it. An explicitly- -labeled answer is associated with its answer blank by name. +Explicitly-named answers: Answers that have been explicitly assigned +names with the C method, or a macro that uses it. An explicitly- +named answer is associated with its answer blank by name. =item * @@ -194,9 +190,9 @@ up the results of problem processing for delivery back to WeBWorK. The HEADER_TEXT(), TEXT(), and ANS() macros add to the header text string, body text string, and answer evaluator queue, respectively. -=over +=head1 METHODS -=item HEADER_TEXT() +=head2 HEADER_TEXT HEADER_TEXT("string1", "string2", "string3"); @@ -220,7 +216,7 @@ sub HEADER_TEXT { $self->{HEADER_ARRAY}; } -=item POST_HEADER_TEXT() +=head2 POST_HEADER_TEXT POST_HEADER_TEXT("string1", "string2", "string3"); @@ -245,7 +241,7 @@ sub POST_HEADER_TEXT { $self->{POST_HEADER_ARRAY}; } -=item TEXT() +=head2 TEXT TEXT("string1", "string2", "string3"); @@ -269,7 +265,6 @@ content being appended. =cut # ^function TEXT -# ^uses $PG_STOP_FLAG # ^uses $STRINGforOUTPUT sub TEXT { @@ -295,49 +290,36 @@ sub envir { } -=item LABELED_ANS() - - TEXT(labeled_ans_rule("name1"), labeled_ans_rule("name2")); - LABELED_ANS(name1 => answer_evaluator1, name2 => answer_evaluator2); +=head2 NAMED_ANS -Adds the answer evaluators listed to the list of labeled answer evaluators. -They will be paired with labeled answer rules (a.k.a. answer blanks) in the -order entered. This allows pairing of answer evaluators and answer rules that -may not have been entered in the same order. - -=cut +Associates answer names with answer evaluators. If the given answer name has a +response group in the PG_ANSWERS_HASH, then the evaluator is added to that +response group. Otherwise the name and evaluator are added to the hash of +explicitly named answer evaluators. They will be paired with explicitly named +answer rules by name. This allows pairing of answer evaluators and answer rules +that may not have been entered in the same order. -# ^function NAMED_ANS -# ^uses &LABELED_ANS -sub NAMED_ANS { - &LABELED_ANS; -} +An example of the usage is: -=item NAMED_ANS() + TEXT(NAMED_ANS_RULE("name1"), NAMED_ANS_RULE("name2")); + NAMED_ANS(name1 => answer_evaluator1, name2 => answer_evaluator2); -Old name for LABELED_ANS(). DEPRECATED. +Note that internally implicitly named evaluators are also associated with +their names via this method. =cut -# ^function NAMED_ANS -# ^uses $PG_STOP_FLAG -sub LABELED_ANS { - my $self = shift; - my @in = @_; +sub NAMED_ANS { + my ($self, @in) = @_; + while (@in) { - my $label = shift @in; - my $ans_eval = shift @in; + my ($label, $ans_eval) = (shift @in, shift @in); $self->warning_message( - "
                      Error in LABELED_ANS:|$label| - -- inputs must be references to AnswerEvaluator objects or subroutines
                      " - ) - unless ref($ans_eval) =~ /CODE/ - or ref($ans_eval) =~ /AnswerEvaluator/; + "Error in NAMED_ANS: |$label| -- inputs must be references to AnswerEvaluator objects or subroutines.") + unless ref($ans_eval) =~ /CODE/ || ref($ans_eval) =~ /AnswerEvaluator/; if (ref($ans_eval) =~ /CODE/) { - # - # Create an AnswerEvaluator that calls the given CODE reference and use that for $ans_eval. - # So we always have an AnswerEvaluator from here on. - # + # Create an AnswerEvaluator that calls the given CODE reference and use that for $ans_eval. + # So we always have an AnswerEvaluator from here on. my $cmp = new AnswerEvaluator; $cmp->install_evaluator( sub { @@ -345,60 +327,59 @@ sub LABELED_ANS { my $checker = shift; my @args = ($ans->{student_ans}); push(@args, ans_label => $ans->{ans_label}) if defined($ans->{ans_label}); - $checker->(@args) - ; # Call the original checker with the arguments that PG::Translator would have used + # Call the original checker with the arguments that PG::Translator would have used + $checker->(@args); }, $ans_eval ); $ans_eval = $cmp; } - if (defined($self->{PG_ANSWERS_HASH}->{$label})) { - $self->{PG_ANSWERS_HASH}->{$label} + if (ref($self->{PG_ANSWERS_HASH}{$label}) eq 'PGanswergroup') { + $self->{PG_ANSWERS_HASH}{$label} ->insert(ans_label => $label, ans_eval => $ans_eval, active => $self->{PG_ACTIVE}); } else { - $self->{PG_ANSWERS_HASH}->{$label} = - PGanswergroup->new($label, ans_eval => $ans_eval, active => $self->{PG_ACTIVE}); + $self->{explicit_answer_name_evals}{$label} = $ans_eval; } - $self->{answer_eval_count}++; } - $self->{PG_ANSWERS_HASH}; + + return; } -=item ANS() +=head2 ANS - TEXT(ans_rule(), ans_rule(), ans_rule()); - ANS($answer_evaluator1, $answer_evaluator2, $answer_evaluator3); +Registers answer evaluators to be implicitly associated with answer names. If +there is an answer name in the implicit answer name stack, then a given answer +evaluator will be paired with the first name in the stack. Otherwise the +evaluator will be pushed onto the implicit answer evaluator stack. This is the +standard method for entering answers. -Adds the answer evaluators listed to the list of unlabeled answer evaluators. -They will be paired with unlabeled answer rules (a.k.a. answer blanks) in the -order entered. This is the standard method for entering answers. + TEXT(ans_rule(), ans_rule(), ans_rule()); + ANS($answer_evaluator1, $answer_evaluator2, $answer_evaluator3); -In the above example, answer_evaluator1 will be associated with the first -answer rule, answer_evaluator2 with the second, and answer_evaluator3 with the -third. In practice, the arguments to ANS() will usually be calls to an answer -evaluator generator such as the cmp() method of MathObjects or the num_cmp() -macro in L. +In the above example, C<$answer_evaluator1> will be associated with the first +answer rule, C<$answer_evaluator2> with the second, and C<$answer_evaluator3> +with the third. In practice, the arguments to C will usually be calls to +an answer evaluator generator such as the C method of MathObjects or the +C macro in L. Note that if the C call is made +before the C calls, the same pairing would occur. =cut -# ^function ANS -# ^uses $PG_STOP_FLAG -# ^uses @PG_ANSWERS - sub ANS { - my $self = shift; - my @in = @_; + my ($self, @in) = @_; while (@in) { - # create new label - $self->{unlabeled_answer_eval_count}++; - my $label = $self->new_label($self->{unlabeled_answer_eval_count}); - my $evaluator = shift @in; - $self->LABELED_ANS($label, $evaluator); + if (my $label = shift @{ $self->{implicit_named_answer_stack} }) { + $self->NAMED_ANS($label, shift @in); + } else { + # In this case ANS is called before the answer rule method for this answer. + # Defer calling NAMED_ANS until the answer rule method is called and the answer is recorded. + push(@{ $self->{implicit_answer_eval_stack} }, shift @in); + } } - $self->{PG_ANSWERS_HASH}; + return; } -=item STOP_RENDERING() +=head2 STOP_RENDERING STOP_RENDERING() unless all_answers_are_correct(); @@ -408,14 +389,13 @@ and answer evaluators until RESUME_RENDERING() is called. =cut # ^function STOP_RENDERING -# ^uses $PG_STOP_FLAG sub STOP_RENDERING { my $self = shift; $self->{PG_ACTIVE} = 0; ""; } -=item RESUME_RENDERING() +=head2 RESUME_RENDERING RESUME_RENDERING(); @@ -425,88 +405,65 @@ evaluators. Reverses the effect of STOP_RENDERING(). =cut # ^function RESUME_RENDERING -# ^uses $PG_STOP_FLAG sub RESUME_RENDERING { my $self = shift; $self->{PG_ACTIVE} = 1; ""; } -######## + # Internal methods -######### -sub new_label { #creates a new label for unlabeled submissions ASNWER_PREFIX.$number - my $self = shift; - my $number = shift; - $self->{QUIZ_PREFIX} . $self->{ANSWER_PREFIX} . sprintf("%04u", $number); -} -sub new_array_label { #creates a new label for unlabeled submissions ASNWER_PREFIX.$number - my $self = shift; - my $number = shift; - $self->{QUIZ_PREFIX} . $self->{ARRAY_PREFIX} . sprintf("%04u", $number); +# Creates a new name for an answer rule. +sub new_label { + my ($self, $number) = @_; + return $self->{QUIZ_PREFIX} . $self->{ANSWER_PREFIX} . sprintf("%04u", $number); } -sub new_array_element_label { #creates a new label for unlabeled submissions ARRAY_PREFIX.$number - my $self = shift; - my $ans_label = shift; # name of the PGanswer group holding this array - my $row_num = shift; - my $col_num = shift; - my %options = @_; - my $vec_num = (defined $options{vec_num}) ? $options{vec_num} : 0; - $self->{QUIZ_PREFIX} . $ans_label . '__' . $vec_num . '-' . $row_num . '-' . $col_num . '__'; +# Creates a new name for an element in an array answer rule group. +# $ans_label is the name of the PGanswer group holding this array. +sub new_array_element_label { + my ($self, $ans_label, $row_num, $col_num, %options) = @_; + my $vec_num = $options{vec_num} // 0; + return $self->{QUIZ_PREFIX} . $ans_label . '__' . $vec_num . '-' . $row_num . '-' . $col_num . '__'; } -sub new_answer_name { # bit of a legacy item - &new_label; +sub new_ans_name { + my $self = shift; + return $self->new_label(++$self->{answer_name_count}); } -sub record_ans_name { # the labels in the PGanswer group and response group should match in this case - my $self = shift; - my $label = shift; - my $value = shift; - #$self->internal_debug_message("PGcore::record_ans_name: $label $value"); - my $response_group = new PGresponsegroup($label, $label, $value); - #$self->debug_message("adding a response group $response_group"); - if (ref($self->{PG_ANSWERS_HASH}->{$label}) =~ /PGanswergroup/) { - $self->{PG_ANSWERS_HASH}->{$label}->replace( - ans_label => $label, - response => $response_group, - active => $self->{PG_ACTIVE} - ); - } else { - $self->{PG_ANSWERS_HASH}->{$label} = PGanswergroup->new( - $label, - response => $response_group, - active => $self->{PG_ACTIVE} - ); - } - $self->{answer_blank_count}++; - $label; -} +sub record_ans_name { + my ($self, $label, $value) = @_; -sub record_array_name { # currently the same as record ans name - my $self = shift; - my $label = shift; - my $value = shift; my $response_group = new PGresponsegroup($label, $label, $value); - #$self->debug_message("adding a response group $response_group"); - if (ref($self->{PG_ANSWERS_HASH}->{$label}) =~ /PGanswergroup/) { - $self->{PG_ANSWERS_HASH}->{$label}->replace( - ans_label => $label, - response => $response_group, - active => $self->{PG_ACTIVE} - ); + + if (ref($self->{PG_ANSWERS_HASH}{$label}) eq 'PGanswergroup') { + # This should really never happen. Should this warn if it does? + $self->{PG_ANSWERS_HASH}{$label} + ->replace(ans_label => $label, response => $response_group, active => $self->{PG_ACTIVE}); + } elsif ($self->{explicit_answer_name_evals}{$label}) { + $self->{PG_ANSWERS_HASH}{$label} = + PGanswergroup->new($label, response => $response_group, active => $self->{PG_ACTIVE}); + $self->NAMED_ANS($label, delete $self->{explicit_answer_name_evals}{$label}); + } elsif (my $evaluator = shift @{ $self->{implicit_answer_eval_stack} }) { + $self->{PG_ANSWERS_HASH}{$label} = + PGanswergroup->new($label, response => $response_group, active => $self->{PG_ACTIVE}); + $self->NAMED_ANS($label, $evaluator); } else { - $self->{PG_ANSWERS_HASH}->{$label} = PGanswergroup->new( - $label, - response => $response_group, - active => $self->{PG_ACTIVE} - ); + $self->{PG_ANSWERS_HASH}{$label} = + PGanswergroup->new($label, response => $response_group, active => $self->{PG_ACTIVE}); } - $self->{answer_blank_count}++; - #$self->{PG_ANSWERS_HASH}->{$label}->{response}->clear; #why is this ? - $label; + return $label; +} + +sub record_implicit_ans_name { + my ($self, $label) = @_; + # Do not add to the name stack if there is something in the evaluator stack. Note that if there is something in the + # evaluator stack then it will be removed when record_ans_name is called which is done when the named answer rule + # method is called. + push(@{ $self->{implicit_named_answer_stack} }, $label) unless @{ $self->{implicit_answer_eval_stack} }; + return $label; } sub extend_ans_group { # modifies the group type @@ -514,31 +471,10 @@ sub extend_ans_group { # modifies the group type my $label = shift; my @response_list = @_; my $answer_group = $self->{PG_ANSWERS_HASH}->{$label}; - if (ref($answer_group) =~ /PGanswergroup/) { + if (ref($answer_group) eq 'PGanswergroup') { $answer_group->append_responses(@response_list); - } else { - #$self->warning_message("The answer |$label| has not yet been defined, you cannot extend it.",caller() ); - # this error message is correct but misleading for the original way - # in which matrix blanks and their response evaluators are matched up - # we should restore the warning message once the new matrix evaluation method is in place - } - $label; -} - -sub record_unlabeled_ans_name { - my $self = shift; - $self->{unlabeled_answer_blank_count}++; - my $label = $self->new_label($self->{unlabeled_answer_blank_count}); - $self->record_ans_name($label); - $label; -} - -sub record_unlabeled_array_name { - my $self = shift; - $self->{unlabeled_answer_blank_count}++; - my $ans_label = $self->new_array_label($self->{unlabeled_answer_blank_count}); - $self->record_array_name($ans_label); + return $label; } sub store_persistent_data { # will store strings only (so far) @@ -563,14 +499,20 @@ sub get_persistent_data { return $self->{PERSISTENCE_HASH}{$label}; } +sub add_content_post_processor { + my ($self, $handler) = @_; + push(@{ $self->{content_post_processors} }, $handler) if ref($handler) eq 'CODE'; + return; +} + sub check_answer_hash { my $self = shift; foreach my $key (keys %{ $self->{PG_ANSWERS_HASH} }) { my $ans_eval = $self->{PG_ANSWERS_HASH}->{$key}->{ans_eval}; - unless (ref($ans_eval) =~ /CODE/ or ref($ans_eval) =~ /AnswerEvaluator/) { + unless (ref($ans_eval) eq 'CODE' or ref($ans_eval) eq 'AnswerEvaluator') { warn "The answer group labeled $key is missing an answer evaluator"; } - unless (ref($self->{PG_ANSWERS_HASH}->{$key}->{response}) =~ /PGresponsegroup/) { + unless (ref($self->{PG_ANSWERS_HASH}->{$key}->{response}) eq 'PGresponsegroup') { warn "The answer group labeled $key is missing answer blanks "; } } @@ -581,7 +523,7 @@ sub PG_restricted_eval { WeBWorK::PG::Translator::PG_restricted_eval(@_); } -=item base64 encoding and decoding +=head2 base64 encoding and decoding $str = decode_base64($coded_str); $coded_str = encode_base64($str); @@ -614,74 +556,6 @@ sub encode_pg_and_html { return $input; } -=back - -=head2 Message channels - -There are three message channels - $PG->debug_message() or in PG: DEBUG_MESSAGE() - $PG->warning_message() or in PG: WARN_MESSAGE() - -They behave the same way, it is simply convention as to how they are used. - -To report the messages use: - - $PG->get_debug_messages - $PG->get_warning_messages - -These are used in Problem.pm for example to report any errors. - -There is also - - $PG->internal_debug_message() - $PG->get_internal_debug_message - $PG->clear_internal_debug_messages(); - -There were times when things were buggy enough that only the internal_debug_message which are not saved -inside the PGcore object would report. - -=cut - -sub debug_message { - my ($self, @str) = @_; - push @{ $self->{DEBUG_messages} }, @str; -} - -sub get_debug_messages { - my $self = shift; - $self->{DEBUG_messages}; -} - -sub warning_message { - my ($self, @str) = @_; - # Mark the start of each message. - push @{ $self->{WARNING_messages} }, '------', @str; -} - -sub get_warning_messages { - my $self = shift; - $self->{WARNING_messages}; -} - -sub internal_debug_message { - my ($self, @str) = @_; - push @$internal_debug_messages, @str; -} - -sub get_internal_debug_messages { - my $self = shift; - $internal_debug_messages; -} - -sub clear_internal_debug_messages { - my $self = shift; - $internal_debug_messages = []; -} - -sub DESTROY { - # doing nothing about destruction, hope that isn't dangerous -} - =head2 insertGraph # returns a path to the file containing the graph image. @@ -711,7 +585,7 @@ sub insertGraph { my ($self, $graph) = @_; my $fileName = $graph->imageName . '.' . $graph->ext; - my $filePath = $self->surePathToTmpFile($self->convertPath("images/$fileName")); + my $filePath = $self->surePathToTmpFile("images/$fileName"); # Check to see if we already have this graph, or if we have to make it. if (!-e $filePath @@ -757,14 +631,75 @@ sub getUniqueName { return $resource->create_unique_id; } +=head2 surePathToTmpFile + + $path = surePathToTmpFile($path); + +Creates all of the intermediate directories between the tempDirectory + +If $path begins with the tempDirectory path, then the +path is treated as absolute. Otherwise, the path is treated as relative the the +course temp directory. + +=cut + +# A very useful macro for making sure that all of the directories to a file have been constructed. + +# ^function surePathToTmpFile +# ^uses getCourseTempDirectory + +sub surePathToTmpFile { + # constructs intermediate directories if needed beginning at ${Global::htmlDirectory}tmp/ + # the input path must be either the full path, or the path relative to this tmp sub directory + + my $self = shift; + my $path = shift; + my $delim = "/"; + my $tmpDirectory = $self->tempDirectory(); + unless (-e $tmpDirectory) { # if by some unlucky chance the tmpDirectory hasn't been created, create it. + my $parentDirectory = $tmpDirectory; + $parentDirectory =~ s|/$||; # remove a trailing / + $parentDirectory = $self->directoryFromPath($parentDirectory); + my ($perms, $groupID) = (stat $parentDirectory)[ 2, 5 ]; + #warn "Creating tmp directory at $tmpDirectory, perms $perms groupID $groupID"; + WeBWorK::PG::IO::createDirectory($tmpDirectory, $perms, $groupID) + or warn "Failed to create parent tmp directory at $path"; + + } + # use the permissions/group on the temp directory itself as a template + my ($perms, $groupID) = (stat $tmpDirectory)[ 2, 5 ]; + #warn "surePathToTmpFile: directory=$tmpDirectory, perms=$perms, groupID=$groupID\n"; + + # if the path starts with $tmpDirectory (which is permitted but optional) remove this initial segment + $path =~ s|^$tmpDirectory|| if $path =~ m|^$tmpDirectory|; + + # find the nodes on the given path + my @nodes = split("$delim", $path); + + # create new path + $path = $tmpDirectory; + + while (@nodes > 1) { + $path = $path . shift(@nodes) . "/"; + + unless (-e $path) { + WeBWorK::PG::IO::createDirectory($path, $perms, $groupID) + or $self->warning_message( + "Failed to create directory at $path with permissions $perms and groupID $groupID"); + } + + } + + $path = $path . shift(@nodes); + return $path; +} + =head1 Macros from IO.pm includePGtext read_whole_problem_file - convertPath fileFromPath directoryFromPath - createDirectory =cut @@ -786,11 +721,6 @@ sub read_whole_problem_file { WeBWorK::PG::IO::read_whole_problem_file(@_); } -sub convertPath { - my $self = shift; - WeBWorK::PG::IO::convertPath(@_); -} - sub fileFromPath { my $self = shift; WeBWorK::PG::IO::fileFromPath(@_); @@ -801,16 +731,11 @@ sub directoryFromPath { WeBWorK::PG::IO::directoryFromPath(@_); } -sub createDirectory { - my $self = shift; - WeBWorK::PG::IO::createDirectory(@_); -} - sub AskSage { my $self = shift; my $python = shift; my $options = shift; - $options->{curlCommand} = WeBWorK::PG::IO::curlCommand(); + $options->{curlCommand} = WeBWorK::PG::IO::externalCommand('curl'); WeBWorK::PG::IO::AskSage($python, $options); } @@ -819,68 +744,69 @@ sub tempDirectory { return $self->{tempDirectory}; } -=head2 surePathToTmpFile +=head1 Message channels - $path = surePathToTmpFile($path); +There are two message channels + $PG->debug_message() or in PG: DEBUG_MESSAGE() + $PG->warning_message() or in PG: WARN_MESSAGE() -Creates all of the intermediate directories between the tempDirectory +They behave the same way, it is simply convention as to how they are used. -If $path begins with the tempDirectory path, then the -path is treated as absolute. Otherwise, the path is treated as relative the the -course temp directory. +To report the messages use: -=cut + $PG->get_debug_messages + $PG->get_warning_messages -# A very useful macro for making sure that all of the directories to a file have been constructed. +These are used in Problem.pm for example to report any errors. -# ^function surePathToTmpFile -# ^uses getCourseTempDirectory -# ^uses createDirectory +There is also -sub surePathToTmpFile { - # constructs intermediate directories if needed beginning at ${Global::htmlDirectory}tmp/ - # the input path must be either the full path, or the path relative to this tmp sub directory + $PG->internal_debug_message() + $PG->get_internal_debug_message + $PG->clear_internal_debug_messages(); - my $self = shift; - my $path = shift; - my $delim = "/"; - my $tmpDirectory = $self->tempDirectory(); - unless (-e $tmpDirectory) { # if by some unlucky chance the tmpDirectory hasn't been created, create it. - my $parentDirectory = $tmpDirectory; - $parentDirectory =~ s|/$||; # remove a trailing / - $parentDirectory = $self->directoryFromPath($parentDirectory); - my ($perms, $groupID) = (stat $parentDirectory)[ 2, 5 ]; - #warn "Creating tmp directory at $tmpDirectory, perms $perms groupID $groupID"; - $self->createDirectory($tmpDirectory, $perms, $groupID) - or warn "Failed to create parent tmp directory at $path"; +There were times when things were buggy enough that only the internal_debug_message which are not saved +inside the PGcore object would report. - } - # use the permissions/group on the temp directory itself as a template - my ($perms, $groupID) = (stat $tmpDirectory)[ 2, 5 ]; - #warn "surePathToTmpFile: directory=$tmpDirectory, perms=$perms, groupID=$groupID\n"; +=cut - # if the path starts with $tmpDirectory (which is permitted but optional) remove this initial segment - $path =~ s|^$tmpDirectory|| if $path =~ m|^$tmpDirectory|; +sub debug_message { + my ($self, @str) = @_; + push @{ $self->{DEBUG_messages} }, @str; +} - # find the nodes on the given path - my @nodes = split("$delim", $path); +sub get_debug_messages { + my $self = shift; + $self->{DEBUG_messages}; +} - # create new path - $path = $tmpDirectory; +sub warning_message { + my ($self, @str) = @_; + push @{ $self->{WARNING_messages} }, @str; +} - while (@nodes > 1) { - $path = $path . shift(@nodes) . "/"; #convertPath($path . shift (@nodes) . "/"); +sub get_warning_messages { + my $self = shift; + $self->{WARNING_messages}; +} - unless (-e $path) { - $self->createDirectory($path, $perms, $groupID) - or $self->warning_message( - "Failed to create directory at $path with permissions $perms and groupID $groupID"); - } +sub internal_debug_message { + my ($self, @str) = @_; + push @$internal_debug_messages, @str; +} - } +sub get_internal_debug_messages { + my $self = shift; + $internal_debug_messages; +} - $path = $path . shift(@nodes); #convertPath($path . shift(@nodes)); - return $path; +sub clear_internal_debug_messages { + my $self = shift; + $internal_debug_messages = []; +} + +sub DESTROY { + # doing nothing about destruction, hope that isn't dangerous } 1; diff --git a/lib/PGloadfiles.pm b/lib/PGloadfiles.pm index a9c34217a7..9cf76e944e 100644 --- a/lib/PGloadfiles.pm +++ b/lib/PGloadfiles.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -84,7 +84,6 @@ sub new { my $self = { envir => $envir, macroFileList => {}, # records macros used in compilation - pgFileName => '', # current pg file being processed macrosPath => '', pwd => '', # current directory -- defined in initialize }; diff --git a/lib/PGresource.pm b/lib/PGresource.pm index 827da51c40..bdac8a51ef 100644 --- a/lib/PGresource.pm +++ b/lib/PGresource.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -14,110 +14,137 @@ ################################################################################ package PGresource; +use parent PGcore; # This is so that a PGresource object can call the PGcore warning_message and debug_message methods. + use strict; -use Exporter; +use warnings; + use Scalar::Util; use UUID::Tiny ':std'; -use PGcore; -our @ISA = qw( PGcore ); sub new { - my $class = shift; - my $parent_alias = shift; - my $id = shift; - my $type = shift; - my %options = @_; - $type =~ s/^\.//; # remove initial period if included in type. - my $self = { - - id => $id, #auxiliary file name - parent_alias => $parent_alias, - type => $type, # gif eps pdf html pg (macro: pl) (applets: java js fla geogebra (ggb) swf ) - parent_file_id => $parent_alias->{pgFileName}, # file id for the file requesting the resource - - path => { - content => undef, # complete file path to resource - is_complete => 0, - is_accessible => 0, - }, - uri => { - content => undef, # usually url path to resource - is_complete => 0, - is_accessible => 0, - }, - return_uri => '', - recorded_uri => '', - convert => { - needed => 0, - from_type => undef, - from_path => undef, - to_type => undef, - to_path => undef, - }, - copy_link => { - type => undef, # copy or link or orig (original file, no copy or link needed) - link_to_path => undef, # the path of the alias - copy_to_path => undef, # the path of the duplicate file - }, - cache_info => {}, - unique_id => undef, - %options, - }; - bless $self, $class; + my ($class, $parent_alias, $id, $type, %options) = @_; + warn "PGresource must be called with a PGalias parent object." + unless ref($parent_alias) =~ /PGalias/; + + my $self = bless { + id => $id, # auxiliary file name + parent_alias => $parent_alias, + type => $type =~ s/^\.//r, # file extension + probFileName => $parent_alias->{probFileName}, + unique_id => undef, + path => undef, # complete file path to resource + uri => undef, # URL path (or complete file path for TeX) to resource + %options + }, $class; + Scalar::Util::weaken($self->{parent_alias}); - $self->warning_message("PGresource must be called with an alias object") unless ref($parent_alias) =~ /PGalias/; - $self->warning_message("PGresource must be called with a name") unless $id; - $self->warning_message("PGresource must be called with a type") unless $type; - # $self->warning_message( "Test warning message from resource object"); - # Use this to check if the warning and debug channels have been hooked up to PGcore and PGalias correctly. - return $self; -} -sub uri { - my $self = shift; - my $uri = shift; - $self->{uri}->{content} = $uri if $uri; - $self->{uri}->{content}; -} + $self->warning_message("PGresource must be called with a name.") unless $id; + $self->warning_message("PGresource must be called with a type.") unless $type; -sub path { - my $self = shift; - my $url = shift; - $self->{path}->{content} = $url if $url; - $self->{path}->{content}; + # Use this to check if the warning and debug channels have been hooked up to PGcore and PGalias correctly. + #$self->warning_message("Test warning message from resource object"); + #$self->debug_message("Test debug message from resource object"); + + return $self; } sub create_unique_id { - my $self = shift; - my $ext = shift; + my ($self, $ext) = @_; my $fileName = $self->fileName; + if ($self->{unique_id}) { $self->warning_message("unique id already exists for $fileName."); return $self->{unique_id}; } - $self->warning_message("auxiliary file $fileName missing resource path ") unless $self->path; - $self->warning_message("auxiliary file $fileName missing problem psvn") unless $self->{parent_alias}->{psvn}; - $self->warning_message("auxiliary file $fileName missing unique_id_stub") - unless $self->{parent_alias}->{unique_id_stub}; - my $unique_id_seed = $self->path() . $self->{parent_file_id} . $self->{id}; + + $self->warning_message(qq{Auxiliary file "$fileName" missing resource path.}) unless $self->path; + $self->warning_message(qq{Auxiliary file "$fileName" missing unique_id_stub.}) + unless $self->{parent_alias}{unique_id_stub}; + + my $unique_id_seed = $self->path . $self->{probFileName} . $self->{id}; $self->{unique_id} = - $self->{parent_alias}->{unique_id_stub} . '___' . create_uuid_as_string(UUID_V3, UUID_NS_URL, $unique_id_seed); + $self->{parent_alias}{unique_id_stub} . '___' . create_uuid_as_string(UUID_V3, UUID_NS_URL, $unique_id_seed); $self->{unique_id} .= ".$ext" if $ext; - $self->{unique_id}; + return $self->{unique_id}; +} + +sub uri { + my ($self, $uri) = @_; + $self->{uri} = $uri if $uri; + return $self->{uri}; +} + +sub path { + my ($self, $path) = @_; + $self->{path} = $path if $path; + return $self->{path}; } sub unique_id { - my $self = shift; - my $unique_id = shift; - $self->{unique_id} = $unique_id if $unique_id; - $self->{unique_id}; + my $self = shift; + return $self->{unique_id}; } sub fileName { - my $self = shift; - my $fileName = shift; + my ($self, $fileName) = @_; $self->{id} = $fileName if $fileName; - $self->{id}; + return $self->{id}; } + 1; + +=head1 NAME + +PGresource - Store information for an auxiliary resource. + +=head2 new + +Usage: C<< PGresource->new($parent_alias, $id, $type, %options) >> + +The C constructor. The C<$parent_alias>, C<$id>, and C<$type> +parameters are required. The C<$parent_alias> must be the parent C +object that calls this constructor (and this is the only situation where this +object should be constructed). The C<$id> should be the file name (or external +URL) of the auxiliary resource to be represented by this C object. +The C<$type> should be the file extension. The C<%options> should contain +C and C which should be array references. +These are used to store warning and debug messages. + +=head2 create_unique_id + +Usage: C<< $pgResource->create_unique_id($ext) >> + +This is the primary method of the C module. This generates a unique +id for the auxiliary resource that it represents. That id takes into account the +unique id stub of the parent C object, the full path to the resource, +the problem file name, and the resource file name. + +=head2 uri + +Usage: C<< $pgResource->uri($uri) >> + +Get or set the URI of the resource. + +=head2 path + +Usage: C<< $pgResource->path($path) >> + +Get or set the path of the resource. + +=head2 unique_id + +Usage: C<< $pgResource->unique_id >> + +Get the unique id of the resource. Note that the unique id is set by calling +C. + +=head2 fileName + +Usage: C<< $pgResource->fileName($fileName) >> + +Get or set the file name (or id) of the resource. + +=cut diff --git a/lib/PGresponsegroup.pm b/lib/PGresponsegroup.pm index 09ea0e0b7f..23267d22e5 100644 --- a/lib/PGresponsegroup.pm +++ b/lib/PGresponsegroup.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/Parser/BOP.pm b/lib/Parser/BOP.pm index 441c405572..e9f692088d 100644 --- a/lib/Parser/BOP.pm +++ b/lib/Parser/BOP.pm @@ -302,6 +302,32 @@ sub swapOps { return $self; } +# +# Change an association "(a bop b) bop c" to "a bop (b bop c)" or vice versa +# argument $dir should be 'left' or 'right' +# 'left' for "a bop (b bop c)" to become "(a bop b) bop c" +# 'right' for "(a bop b) bop c" to become "a bop (b bop c)" +# Assumes the calling entity verified it is appropriate to associate +# +sub associateOps { + my $self = shift; + my $dir = shift; + if ($dir eq 'left') { + return $self->Item('BOP')->new( + $self->{equation}, $self->{bop}, + $self->Item('BOP')->new($self->{equation}, $self->{bop}, $self->{lop}, $self->{rop}{lop})->reduce, + $self->{rop}{rop} + )->reduce; + } else { + return $self->Item('BOP')->new( + $self->{equation}, + $self->{lop}{bop}, + $self->{lop}{lop}, + $self->Item('BOP')->new($self->{equation}, $self->{bop}, $self->{lop}{rop}, $self->{rop}) + )->reduce; + } +} + # # Get the variables from the two operands # diff --git a/lib/Parser/BOP/multiply.pm b/lib/Parser/BOP/multiply.pm index cb28381bf4..f7e8418833 100644 --- a/lib/Parser/BOP/multiply.pm +++ b/lib/Parser/BOP/multiply.pm @@ -68,6 +68,11 @@ sub _reduce { $self->swapOps if (($self->{rop}->class eq 'Number' && $self->{lop}->class ne 'Number' && $reduce->{'x*n'}) || ($self->{lop}->class eq 'Function' && $self->{rop}->class ne 'Function' && $reduce->{'fn*x'})); + $self = $self->associateOps('left') + if ($reduce->{'m*(n*x)'} + && $self->{lop}->class eq 'Number' + && $self->{rop}->isa('Parser::BOP::multiply') + && $self->{rop}{lop}->class eq 'Number'); return $self; } @@ -78,14 +83,15 @@ sub makeNeg { return $self; } -$Parser::reduce->{'1*x'} = 1; -$Parser::reduce->{'x*1'} = 1; -$Parser::reduce->{'0*x'} = 1; -$Parser::reduce->{'x*0'} = 1; -$Parser::reduce->{'(-x)*y'} = 1; -$Parser::reduce->{'x*(-y)'} = 1; -$Parser::reduce->{'x*n'} = 1; -$Parser::reduce->{'fn*x'} = 1; +$Parser::reduce->{'1*x'} = 1; +$Parser::reduce->{'x*1'} = 1; +$Parser::reduce->{'0*x'} = 1; +$Parser::reduce->{'x*0'} = 1; +$Parser::reduce->{'(-x)*y'} = 1; +$Parser::reduce->{'x*(-y)'} = 1; +$Parser::reduce->{'x*n'} = 1; +$Parser::reduce->{'fn*x'} = 1; +$Parser::reduce->{'m*(n*x)'} = 1; sub string { my ($self, $precedence, $showparens, $position, $outerRight) = @_; diff --git a/lib/Parser/Legacy/PGcomplexmacros.pl b/lib/Parser/Legacy/PGcomplexmacros.pl index 56e3d06444..696880e563 100644 --- a/lib/Parser/Legacy/PGcomplexmacros.pl +++ b/lib/Parser/Legacy/PGcomplexmacros.pl @@ -19,8 +19,7 @@ =head1 DESCRIPTION =cut BEGIN { - be_strict(); - + strict->import; } sub _PGcomplexmacros_init { @@ -59,7 +58,7 @@ sub _PGcomplexmacros_init { my $number = '([+-]?)(?=\d|\.\d)\d*(\.\d*)?(E([+-]?\d+))?'; =head3 cplx_cmp - + # This subroutine compares complex numbers. # Available prefilters include: # each of these are called by cplx_cmp( answer, mode => '(prefilter name)' ) @@ -412,21 +411,21 @@ sub compare_cplx { =head3 multi_cmp - # + # # Checks a comma separated string of items against an array of evaluators. # For example this is useful for checking all of the complex roots of an equation. # Each student answer must be evaluated as correct by a DISTINCT answer evalutor. - # + # # This answer checker will only work reliably if each answer checker corresponds # to a distinct correct answer. For example if one answer checker requires # any positive number, and the second requires the answer 1, then 1,2 might # be judged incorrect since 1, satisifes the first answer checker, but 2 doesn't # satisfy the second. 2,1 would work however. Avoid this type of use!! - # + # # Including backtracking to fit the answers as best possible to each answer evaluator # in the best possible way, is beyond the ambitions of this evaluator. -=cut +=cut sub multi_cmp { my $ra_answer_evaluators = shift; # array of evaluators @@ -512,7 +511,7 @@ sub cplx_constants { } } -=head2 Utility functions +=head2 Utility functions # for checking the form of a number or of the C field in an answer hash @@ -698,7 +697,7 @@ =head4 single_term() # Of course, the unary operator "-" must be handled... if it is a unary operator, and not a regular - # the only place it could occur unambiguously without being surrounded by parenthesis, is the very # first position. So that case is checked before the loop begins. - + =cut sub single_term { diff --git a/lib/Parser/List/Matrix.pm b/lib/Parser/List/Matrix.pm index 73071afbb3..6bdfc26b6a 100644 --- a/lib/Parser/List/Matrix.pm +++ b/lib/Parser/List/Matrix.pm @@ -24,6 +24,8 @@ sub _check { $self->{equation}->Error("Entries in a Matrix must be Numbers or Lists of Numbers") unless ($x->type =~ m/Number|Matrix/); } + $self->{equation}->Error("Entries of a Matrix must be constant") + if ($self->context->flag("requireConstantMatrices") && !($self->{isConstant})); } # diff --git a/lib/Parser/List/Point.pm b/lib/Parser/List/Point.pm index 93e515b3f4..0b8fee4ff1 100644 --- a/lib/Parser/List/Point.pm +++ b/lib/Parser/List/Point.pm @@ -20,6 +20,8 @@ sub _check { $self->{equation}->Error([ "Coordinates of Points must be Numbers, not %s", $type ]); } } + $self->{equation}->Error("Coordinates of a Point must be constant") + if ($self->context->flag("requireConstantPoints") && !($self->{isConstant})); } ######################################################################### diff --git a/lib/Parser/List/Vector.pm b/lib/Parser/List/Vector.pm index 21145d0026..476f793d51 100644 --- a/lib/Parser/List/Vector.pm +++ b/lib/Parser/List/Vector.pm @@ -23,6 +23,8 @@ sub _check { $self->{equation}->Error([ "Coordinates of Vectors must be Numbers, not %s", $type ]); } } + $self->{equation}->Error("Coordinates of a Vector must be constant") + if ($self->context->flag("requireConstantVectors") && !($self->{isConstant})); } sub ijk { diff --git a/lib/SampleProblemParser.pm b/lib/SampleProblemParser.pm index 6cce81e38f..2123ac5093 100644 --- a/lib/SampleProblemParser.pm +++ b/lib/SampleProblemParser.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/Select.pm b/lib/Select.pm index 55057c898f..9049de7888 100644 --- a/lib/Select.pm +++ b/lib/Select.pm @@ -186,12 +186,10 @@ as a regular select list problem as described above. =cut -BEGIN { - be_strict(); -} -#' package Select; +use strict; + @Select::ISA = qw( Exporter ChoiceList ); # *** Subroutines which overload ChoiceList.pm *** diff --git a/lib/Statistics.pm b/lib/Statistics.pm index 4b24189cfd..4cf8b72b66 100644 --- a/lib/Statistics.pm +++ b/lib/Statistics.pm @@ -1,7 +1,7 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -57,7 +57,6 @@ sub make_csv_alias { # Define the file name, clean it up and convert to a url. my $filePath = "data/$studentLogin-$problemSeed-set" . $setName . "prob$prob.html"; - $filePath = $self->{PG}->convertPath($filePath); $filePath = $self->{PG}->surePathToTmpFile($filePath); my $url = $self->{PG}->{PG_alias}->make_alias($filePath); diff --git a/lib/Value.pm b/lib/Value.pm index 1b3897e446..4c7b799fc7 100644 --- a/lib/Value.pm +++ b/lib/Value.pm @@ -267,13 +267,12 @@ sub inContext { my $self = shift; $self->context(@_); $self } ############################################################# -# # # The address of a Value object (actually ANY perl value). # Use this to compare two objects to see of they are # the same object (avoids automatic stringification). # -sub address { oct(sprintf("0x%p", shift)) } +sub address { Scalar::Util::refaddr(shift) } sub isBlessed { (Scalar::Util::blessed(shift) // '') ne "" } sub blessedClass { Scalar::Util::blessed(shift) } diff --git a/lib/Value/AnswerChecker.pm b/lib/Value/AnswerChecker.pm index 2ae6c1f6f9..85ed004fb8 100644 --- a/lib/Value/AnswerChecker.pm +++ b/lib/Value/AnswerChecker.pm @@ -182,6 +182,7 @@ sub cmp_parse { $self->cmp_diagnostics($ans); } } else { + $ans->{student_ans} = protectHTML($ans->{student_ans}); $self->cmp_collect($ans); $self->cmp_error($ans); } @@ -446,9 +447,8 @@ sub ans_matrix { my $named_extension = pgRef('NAMED_ANS_ARRAY_EXTENSION'); my $named_ans_rule = pgRef('NAMED_ANS_RULE'); my $HTML = ""; - my $ename = $name; - $name = pgCall('NEW_ANS_NAME') if ($name eq ''); - $ename = "${answerPrefix}_${name}"; + pgCall('RECORD_IMPLICIT_ANS_NAME', $name = pgCall('NEW_ANS_NAME')) unless $name; + my $ename = "${answerPrefix}_${name}"; $self->{ans_name} = $ename; $self->{ans_rows} = $rows; $self->{ans_cols} = $cols; @@ -490,7 +490,14 @@ sub ans_matrix { } push(@array, [@row]); } - $self->format_matrix([@array], open => $open, close => $close, sep => $sep, top_labels => $toplabels); + $self->format_matrix( + [@array], + open => $open, + close => $close, + sep => $sep, + top_labels => $toplabels, + ans_last_name => ANS_NAME($ename, $rows - 1, $cols - 1) + ); } sub ANS_NAME { @@ -599,7 +606,13 @@ sub format_matrix_HTML { . $close . ''; } - return '' . $HTML . ''; + return '' + . $HTML + . ''; } sub EVALUATE { diff --git a/lib/Value/Matrix.pm b/lib/Value/Matrix.pm index f4ccc87453..1499656c68 100644 --- a/lib/Value/Matrix.pm +++ b/lib/Value/Matrix.pm @@ -291,6 +291,119 @@ sub isZero { return 1; } +# +# See if the matrix is triangular, diagonal, symmetric, orthogonal +# + +sub isUpperTriangular { + my $self = shift; + my @d = $self->dimensions; + return 1 if scalar(@d) == 1; + return 0 if scalar(@d) > 2; + for my $i (2 .. $d[0]) { + for my $j (1 .. ($i - 1 < $d[1] ? $i - 1 : $d[1])) { + return 0 unless $self->element($i, $j) == 0; + } + } + return 1; +} + +sub isLowerTriangular { + my $self = shift; + my @d = $self->dimensions; + if (scalar(@d) == 1) { + for ((@{ $self->{data} })[ 1 .. $#{ $self->{data} } ]) { + return 0 unless $_ == 0; + } + } + return 0 if scalar(@d) > 2; + for my $i (1 .. $d[0] - 1) { + for my $j ($i + 1 .. $d[1]) { + return 0 unless $self->element($i, $j) == 0; + } + } + return 1; +} + +sub isDiagonal { + my $self = shift; + return $self->isSquare && $self->isUpperTriangular && $self->isLowerTriangular; +} + +sub isSymmetric { + my $self = shift; + return 0 unless $self->isSquare; + my $d = ($self->dimensions)[0]; + return 1 if $d == 1; + for my $i (1 .. $d - 1) { + for my $j ($i + 1 .. $d) { + return 0 unless $self->element($i, $j) == $self->element($j, $i); + } + } + return 1; +} + +sub isOrthogonal { + my $self = shift; + return 0 unless $self->isSquare; + my @d = $self->dimensions; + if (scalar(@d) == 1) { + return 0 unless ($self->{data}->[0] == 1 || $self->{data}->[0] == -1); + } + my $M = $self * $self->transpose; + return $M->isOne; +} + +# +# See if the matrix is in (reduced) row echelon form +# + +sub isREF { + my $self = shift; + my @d = $self->dimensions; + return 1 if scalar(@d) == 1; + return 0 if scalar(@d) > 2; + my $k = 0; + for my $i (1 .. $d[0]) { + for my $j (1 .. $d[1]) { + if ($j <= $k) { + return 0 unless $self->element($i, $j) == 0; + } elsif ($self->element($i, $j) != 0) { + $k = $j; + last; + } elsif ($j == $d[1]) { + $k = $d[1] + 1; + } + } + } + return 1; +} + +sub isRREF { + my $self = shift; + my @d = $self->dimensions; + return 1 if scalar(@d) == 1; + return 0 if scalar(@d) > 2; + my $k = 0; + for my $i (1 .. $d[0]) { + for my $j (1 .. $d[1]) { + if ($j <= $k) { + return 0 unless $self->element($i, $j) == 0; + } elsif ($self->element($i, $j) != 0) { + return 0 unless $self->element($i, $j) == 1; + for my $m (1 .. $i - 1) { + return 0 unless $self->element($m, $j) == 0; + } + $k = $j; + last; + } elsif ($j == $d[1]) { + $k = $d[1] + 1; + } + } + } + return 1; +} + sub _isNumber { my $n = shift; return Value::isNumber($n) || Value::classMatch($n, 'Fraction'); @@ -473,26 +586,140 @@ sub transpose { # # Get an identity matrix of the requested size +# Value::Matrix->I(n) +# $A->I # n is the number of rows of $A # sub I { my $self = shift; my $d = shift; my $context = shift || $self->context; - $d = ($self->dimensions)[0] if !defined $d && ref($self) && $self->isSquare; + $d = ($self->dimensions)[0] if !defined $d && ref($self); Value::Error("You must provide a dimension for the Identity matrix") unless defined $d; Value::Error("Dimension must be a positive integer") unless $d =~ m/^[1-9]\d*$/; my @M = (); - my @Z = split('', 0 x $d); my $REAL = $context->Package('Real'); - foreach my $i (0 .. $d - 1) { - my @row = @Z; + for my $i (0 .. $d - 1) { + push(@M, $self->make($context, map { $REAL->new(($_ == $i) ? 1 : 0) } 0 .. $d - 1)); + } + return $self->make($context, @M); +} + +# +# Get an elementary matrix of the requested size and type +# Value::Matrix->E(n,[i,j]) nxn, swap rows i and j +# Value::Matrix->E(n,[i,j],k) nxn, replace row i with row i added to k times row j +# Value::Matrix->E(n,[i],k) nxn, scale row i by k +# $A->E([i,j]) # n is the number of rows of $A +# $A->E([i,j],k) # n is the number of rows of $A +# $A->E([i],k) # n is the number of rows of $A +# +sub E { + my ($self, $d, $rows, $k, $context) = @_; + if (ref $d eq 'ARRAY') { + ($rows, $k, $context) = ($d, $rows, $k); + $d = ($self->dimensions)[0] if ref($self); + } + $context = $self->context unless $context; + Value::Error("You must provide a dimension for an Elementary matrix") unless defined $d; + Value::Error("Dimension must be a positive integer") unless $d =~ m/^[1-9]\d*$/; + my @ij = @{$rows}; + Value::Error("Either one or two rows must be specified for an Elementary matrix") unless (@ij == 1 || @ij == 2); + Value::Error( + "If only one row is specified for an Elementary matrix, then a number to scale by must also be specified") + if (@ij == 1 && !defined $k); + for (@ij) { + Value::Error("Row indices must be integers between 1 and $d") + unless ($_ =~ m/^[1-9]\d*$/ && $_ >= 1 && $_ <= $d); + } + @ij = map { $_ - 1 } (@ij); + + my @M = (); + my $REAL = $context->Package('Real'); + + for my $i (0 .. $d - 1) { + my @row = (0) x $d; $row[$i] = 1; + if (@ij == 1) { + $row[$i] = $k if ($i == $ij[0]); + } elsif (defined $k) { + $row[ $ij[1] ] = $k if ($i == $ij[0]); + } else { + ($row[ $ij[0] ], $row[ $ij[1] ]) = ($row[ $ij[1] ], $row[ $ij[0] ]) if ($i == $ij[0] || $i == $ij[1]); + } push(@M, $self->make($context, map { $REAL->new($_) } @row)); } return $self->make($context, @M); } +# +# Get a permutation matrix of the requested size +# E.g. P(3,[1,2,3]) corresponds to cycle (123) applied to rows of I_3i, +# and P(6,[1,4],[2,4,6]) corresponds to cycle product (14)(246) applied to rows of I_6 +# Value::Matrix->P(n,(cycles)) +# $A->P((cycles)) # n is the number of rows of $A +# +sub P { + my ($self, $d, @cycles) = @_; + if (ref $d eq 'ARRAY') { + unshift(@cycles, $d); + $d = ($self->dimensions)[0] if ref($self); + } + my $context = $self->context; + $d = ($self->dimensions)[0] if !defined $d && ref($self) && $self->isSquare; + Value::Error("You must provide a dimension for a Permutation matrix") unless defined $d; + Value::Error("Dimension must be a positive integer") unless $d =~ m/^[1-9]\d*$/; + for my $c (@cycles) { + Value::Error("Permutation cycles should be array references") unless (ref($c) eq 'ARRAY'); + for (@$c) { + Value::Error("Permutation cycle indices must be integers between 1 and $d") + unless ($_ =~ m/^[1-9]\d*$/ && $_ >= 1 && $_ <= $d); + } + my %cycle_hash = map { $_ => '' } (@$c); + Value::Error("A permutation cycle should not repeat an index") unless (@$c == keys %cycle_hash); + } + my @M = (); + my $REAL = $context->Package('Real'); + + # Make an identity matrix + for my $i (0 .. $d - 1) { + push(@M, $self->make($context, map { $REAL->new(($_ == $i) ? 1 : 0) } 0 .. $d - 1)); + } + + # Then apply the permutation cycles to it + for my $c (@cycles) { + my $swap; + for my $i (0 .. $#$c, 0) { + ($swap, $M[ $c->[$i] - 1 ]) = ($M[ $c->[$i] - 1 ], $swap); + } + } + + return $self->make($context, @M); +} + +# +# Get an all zero matrix of the requested size +# Value::Matrix->Zero(m,n) +# Value::Matrix->Zero(n) +# $A->Zero # n is the number of rows of $A +# +sub Zero { + my ($self, $m, $n, $context) = @_; + $context = $self->context unless $context; + $n = $m if !defined $n && defined $m; + $m = ($self->dimensions)[0] if !defined $m && ref($self); + $n = ($self->dimensions)[1] if !defined $n && ref($self); + Value::Error("You must provide dimensions for the Zero matrix") unless defined $m && defined $n; + Value::Error("Dimension must be a positive integer") unless $m =~ m/^[1-9]\d*$/ && $n =~ m/^[1-9]\d*$/; + my @M = (); + my $REAL = $context->Package('Real'); + + for my $i (0 .. $m - 1) { + push(@M, $self->make($context, map { $REAL->new(0) } 0 .. $n - 1)); + } + return $self->make($context, @M); +} + # # Extract a given row from the matrix # diff --git a/lib/Value/Real.pm b/lib/Value/Real.pm index f637a85d0b..ced0b4072a 100644 --- a/lib/Value/Real.pm +++ b/lib/Value/Real.pm @@ -147,22 +147,23 @@ sub compare { my $digits = (1 > int($tolerance) ? 1 : int($tolerance + 0.5)) - 1; my $extraDigits = int($self->getFlag('tolExtraDigits')); my $tdigits = (0 < $extraDigits ? $extraDigits + $digits : $digits); - my $exp = substr(sprintf("%E", $b), -3) - 15; # Adjust $a by an amount in the round-off error - $a += ($a <=> 0) * "1E$exp"; # range so that it rounds better in sprintf - $b += ($b <=> 0) * "1E$exp"; # Same for $b - my $bd = sprintf("%.${tdigits}E", $b); # Round $b to the number of tdigits - $bd =~ s/^.*\.(.*?)0*E.*$/$1/; # Get the decimal part without trailing zeros - my $bn = CORE::length($bd); # Number of those decimal digits - $bn = $digits if ($bn < $digits); # (with a minimum of $digits); - my $aE = sprintf("%.${bn}E", $a); # Round $a to $bn digits - my $bE = sprintf("%.${bn}E", $b); # Round $b to $bn digits - return 0 if $aE eq $bE; # Return equal if they are - - if ($self->getFlag('tolTruncation')) { # If truncation is allowed - $aE = sprintf("%.15E", $a); # Get $a to full resolution + # Adjust $a by an amount in the round-off error range so that it rounds better in sprintf + my $exp = substr(sprintf("%E", $b), -3) + substr(sprintf("%E", $zeroLevel), -3); + $a += ($a <=> 0) * "1E$exp"; + $b += ($b <=> 0) * "1E$exp"; # Same for $b + my $bd = sprintf("%.${tdigits}E", $b); # Round $b to the number of tdigits + $bd =~ s/^.*\.(.*?)0*E.*$/$1/; # Get the decimal part without trailing zeros + my $bn = CORE::length($bd); # Number of those decimal digits + $bn = $digits if ($bn < $digits); # (with a minimum of $digits); + my $aE = sprintf("%.${bn}E", $a); # Round $a to $bn digits + my $bE = sprintf("%.${bn}E", $b); # Round $b to $bn digits + return 0 if $aE eq $bE; # Return equal if they are + + if ($self->getFlag('tolTruncation')) { # If truncation is allowed + $aE = sprintf("%.15E", $a); # Get $a to full resolution $aE =~ s/\.(\d{$bn}).*E/.$1E/; - $aE =~ s/\.E/E/; # Truncate it to the required number of digits - return 0 if $aE eq $bE; # Return equal if they are + $aE =~ s/\.E/E/; # Truncate it to the required number of digits + return 0 if $aE eq $bE; # Return equal if they are } # return $a <=> $b; # Otherwise compare numbers as perl reals } diff --git a/lib/Value/String.pm b/lib/Value/String.pm index fc7af6087b..829aaa6b4b 100644 --- a/lib/Value/String.pm +++ b/lib/Value/String.pm @@ -127,6 +127,20 @@ sub quoteHTML { return '' . $s . ''; } +# +# Quote XML special characters +# +sub quoteXML { + shift; + my $s = shift; + return unless defined $s; + return $s if eval('$main::displayMode') eq 'TeX'; + $s =~ s/&/\&/g; + $s =~ s//\>/g; + return $s; +} + # # Render the value verbatim # diff --git a/lib/VectorField.pm b/lib/VectorField.pm index 2bcd1259ad..369fd313c0 100644 --- a/lib/VectorField.pm +++ b/lib/VectorField.pm @@ -48,7 +48,7 @@ the x and y components of the vector field at (x,y). Both subroutines must be f =item $vf = new VectorField ( x_rule_ref, y_rule_ref, graph_ref ); This variant inserts the vector field object into the graph object referred to by graph_ref. The domain -of the vector field object is set to the domain of the graph. The graph_ref must come last. +of the vector field object is set to the domain of the graph. The graph_ref must come last. =back @@ -141,12 +141,10 @@ set the current position to (x,y) =cut -BEGIN { - be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. -} - package VectorField; +use strict; + #use "WWPlot.pm"; #Because of the way problem modules are loaded 'use' is disabled. diff --git a/lib/WWPlot.pm b/lib/WWPlot.pm index 6a731206a5..1b73e350e4 100644 --- a/lib/WWPlot.pm +++ b/lib/WWPlot.pm @@ -166,13 +166,10 @@ These functions translate from real world to pixel coordinates. =cut -BEGIN { - be_strict(); # an alias for use strict. This means that all global variable must contain main:: as a prefix. - -} - package WWPlot; +use strict; + #use Exporter; #use DynaLoader; #use GD; diff --git a/lib/WeBWorK/PG.pm b/lib/WeBWorK/PG.pm index 475f8d1fb3..2aedd86d77 100644 --- a/lib/WeBWorK/PG.pm +++ b/lib/WeBWorK/PG.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -24,6 +24,7 @@ use WeBWorK::PG::Environment; use WeBWorK::PG::Translator; use WeBWorK::PG::RestrictedClosureClass; use WeBWorK::PG::Constants; +use WeBWorK::PG::Localize; use constant DISPLAY_MODES => { # display name # mode name @@ -139,7 +140,7 @@ sub new_helper ($invocant, %options) { } } - $translator->translate(); + $translator->translate; # IMPORTANT: The translator environment should not be trusted after the problem code runs. @@ -172,15 +173,25 @@ sub new_helper ($invocant, %options) { ); } - # HTML_dpng uses an ImageGenerator. We have to render the queued equations. + # HTML_dpng uses an ImageGenerator. We have to render the queued equations. This must be done before the post + # processing, since the image tags output by the image generator initially include markers which are invalid html. + # Mojo::DOM will change these markers into attributes with values and this will fail. if ($image_generator) { - my $sourceFile = "$options{templateDirectory}$options{sourceFilePath}"; $image_generator->render( refresh => $options{refreshMath2img} // 0, body_text => $translator->r_text, ); } + $translator->post_process_content if ref($translator->{rh_pgcore}) eq 'PGcore'; + $translator->stringify_answers; + + # Add the result summary set in post processing into the result. + $result->{summary} = $translator->{rh_pgcore}{result_summary} + if ref($translator->{rh_pgcore}) eq 'PGcore' + && $translator->{rh_pgcore}{result_summary} + && (!defined $result->{summary} || $result->{summary} !~ /\S/); + return bless { translator => $translator, head_text => ${ $translator->r_header }, @@ -244,6 +255,15 @@ sub defineProblemEnvironment ($pg_envir, $options = {}, $image_generator = undef isInstructor => $options->{isInstructor} // 0, PERSISTENCE_HASH => $options->{PERSISTENCE_HASH} // {}, + # Attempt Results + showFeedback => $options->{showFeedback} // 0, + showAttemptAnswers => $options->{showAttemptAnswers} // 1, + showAttemptPreviews => $options->{showAttemptPreviews} // 1, + forceShowAttemptResults => $options->{forceShowAttemptResults} // 0, + showAttemptResults => $options->{showAttemptResults} // 0, + showMessages => $options->{showMessages} // 1, + showCorrectAnswers => $options->{showCorrectAnswers} // 0, + # The next has marks what data was updated and needs to be saved # by the front end. PERSISTENCE_HASH_UPDATED => {}, @@ -260,8 +280,8 @@ sub defineProblemEnvironment ($pg_envir, $options = {}, $image_generator = undef mathViewLocale => $options->{mathViewLocale} // $pg_envir->{options}{mathViewLocale}, # Internationalization - language => $options->{language} // 'en', - language_subroutine => $options->{language_subroutine} // sub (@args) { return $args[0]; }, + language => $options->{language} // 'en', + language_subroutine => WeBWorK::PG::Localize::getLoc($options->{language} // 'en'), # Directories and URLs pgMacrosDir => "$pg_envir->{directories}{root}/macros", @@ -459,6 +479,55 @@ This may contain the following keys (example values are shown) useBaseTenLog: 0 defaultDisplayMatrixStyle: '[s]' # left delimiter, middle line delimiters, right delimiter +=item showFeedback (boolean, default: 0) + +Determines if feedback will be shown for answers in the problem. Note that +feedback will be shown if forceShowAttemptResults is true regardless of +the value of this option. + +=item showAttemptAnswers (boolean, default: 1) + +Determines if the student's evaluated (i.e. "Entered") answers will be shown in +feedback. + +=item showAttemptPreviews (boolean, default: 1) + +Determines if the student's answer previews will be shown in feedback. + +=item showAttemptResults (boolean, default: 0) + +Determines if attempt results will be revealed in feedback. In other words, +if the student's answers are correct, incorrect, or partially correct. This +honors the value of the PG C flag. If that flag is +false, then attempt results will still not be shown. + +If this is true, then a summary of results will also be generated. The +summary will be returned in the C key of the C hash. + +=item forceShowAttemptResults (boolean, default: 0) + +If this is true then feedback will be shown with attempt results. This ignores +the PG C flag and shows attempt results in any case. +The summary will also be generated if this is true. + +=item showMessages (boolean, default: 1) + +Determines if any messages generated in answer evaluation will be shown. + +=item showCorrectAnswers (numeric, default: 0) + +Determines if correct answers will be shown. If 0, then correct answers are not +shown. If set to 1, then correct answers are shown but hidden, and a "Reveal" +button is shown at first. If that button is clicked, then the answer is shown. +If set to 2, then correct answers are shown immediately. + +There is one special case that needs extra explanation. If this is true +(greater than zero), C is true, C +is true, and C, C, and C +are all false, then correct answers will be shown with no other content in the +feedback popover except a close button, and the popover will open automatically +on page load. + =item answerPrefix (string, default: '') A prefix to prepend to all answer labels. Note that other prefixes may be diff --git a/lib/WeBWorK/PG/Constants.pm b/lib/WeBWorK/PG/Constants.pm index acd9fd4a0f..566b419038 100644 --- a/lib/WeBWorK/PG/Constants.pm +++ b/lib/WeBWorK/PG/Constants.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/WeBWorK/PG/ConvertToPGML.pm b/lib/WeBWorK/PG/ConvertToPGML.pm new file mode 100644 index 0000000000..0dd38b4d36 --- /dev/null +++ b/lib/WeBWorK/PG/ConvertToPGML.pm @@ -0,0 +1,297 @@ +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2024 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 + +WeBWorK::PG::ConvertToPGML + +=head1 DESCRIPTION + +Converts a pg file to PGML format. + +This script does a number of conversions: + +=over +=item Update the loadMacros call to include PGML.pl, eliminate MathObject.pl (since it is loaded by PGML.pl) +and adds PGcourse.pl to the end of the list. +=item Coverts BEGIN_TEXT/END_TEXT (and older versions of this), BEGIN_SOLUTION/END_SOLUTION, BEGIN_HINT/END_HINT +to their newer BEGIN_PGML blocks. +=item Convert math mode in these blocks to PGML style math mode. +=item Convert other styling (bold, italics) to PGML style. +=item Convert variables to the interpolated [$var] PGML style. +=item Convert some of the answer rules to newer PGML style. +=item Remove some outdated code. +=item A few other minor things. +=back + +=head1 OPTIONS + +=cut + +package WeBWorK::PG::ConvertToPGML; +use parent qw(Exporter); + +use strict; +use warnings; + +our @EXPORT = qw(convertToPGML); + +# This subroutine converts the file that is passed in as a multi-line string and +# assumed to be an older-style PG file with BEGIN_TEXT/END_TEXT, BEGIN_SOLUTION/END_SOLUTION, +# and BEGIN_HINT/END_HINT blocks. + +# * parses the loadMacros line(s) to include PGML.pl (and eliminate MathObjects.pl, which) +# is imported by PGML.pl. This also adds PGcourse.pl to the end of the list. + +# input is a string containing the source of the pg file to be converted. +# returns a string that is the converted input string. + +# This stores the answers inside of ANS and related functions. +my @ans_list; + +sub convertToPGML { + my ($pg_source) = @_; + + # First get a list of all of the ANS, LABELED_ANS, etc. in the problem. + @ans_list = getANS($pg_source); + + my @pgml_block; + my $in_pgml_block = 0; + my @all_lines; + + my @rows = split(/\n/, $pg_source); + + while (@rows) { + my $row = shift @rows; + if ($row =~ /BEGIN_(TEXT|HINT|SOLUTION)/ + || $row =~ /SOLUTION\(EV3\(<<\'END_SOLUTION\'\)\);/ + || $row =~ /TEXT\(EV2\(<= 1; $n--) { + if ($empty_lines[$n] == $empty_lines[ $n - 1 ] + 1) { + splice(@all_lines, $empty_lines[$n], 1); + } + } + return join "\n", @all_lines; +} + +# This subroutine converts a block (passed in as an array ref of strings) to +# PGML format. This includes: +# * converting BEGIN_TEXT/END_TEXT to BEGIN_PGML/END_PGML +# * converting BEGIN_HINT/END_HINT to BEGIN_PGML_HINT/END_PGML_HINT +# * converting BEGIN_SOLUTION/END_SOLUTION to BEGIN_PGML_SOLUTION/END_PGML_SOLUTION +# * converting begin end math with PGML versions +# * adding an extra space before or after a $PAR depending on where it is. +# * adding two spaces at the end of a line for a $BR at the end of a line +# * converting $HR to --- +# * convert center, bold and italics to PGML forms. +# * converting other variables from $var to [$var] +# * converting ans_rule to [_]{} format +# * converting \{ \} to [@ @] without altering code within the \{ \}. + +sub convertPGMLBlock { + my ($block) = @_; + my @new_rows; + while (@$block) { + my $row = shift @$block; + my $add_blank_line_before = ($row =~ /^\s*\$PAR/); + my $add_blank_line_after = ($row =~ /\$PAR\s*$/); + + # match all forms of ans_rule + $row = convertANSrule($row); + + # Capture any perl blocks inside \{ \} + my @perl_block; + + if ($row =~ /^(.*)\\\{(.*)\\\}(.*)/) { + push(@perl_block, $2); + $row = "$1 PERL_BLOCK $3"; + } elsif ($row =~ /^(.*)\\\{(.*)$/) { # This is a multi-line perl block + my $tmp = $1; + push(@perl_block, $2); + do { + $row = shift @$block; + push(@perl_block, $row) unless $row =~ /^(.*)\\\}(.*)$/; + } until $row =~ /^(.*)\\\}(.*)$/; + push(@perl_block, $1); + $row = "$tmp PERL_BLOCK $2"; + } + + $row =~ s/(BEGIN|END)_TEXT/$1_PGML/; + $row =~ s/TEXT\(EV2\(<>/g; + $row =~ s/\$\{?ECENTER\}?/<)?\{.*?\})?)/[$1]/; + } + } + + # Do some converting inside a perl block: + for (0 .. $#perl_block) { + $perl_block[$_] =~ s/AnswerFormatHelp\(["']([\w\s]+)["']\)/helpLink('$1')/g; + } + + if ($add_blank_line_before) { + push @new_rows, '', $row; + } elsif ($add_blank_line_after) { + push @new_rows, $row, ''; + } elsif ($row =~ /^(.*)?\sPERL_BLOCK\s(.*)?$/) { + # remove any empty lines in the block + @perl_block = grep { $_ !~ /^\s*$/ } @perl_block; + # Wrap the perl block in [@ @] + if ($#perl_block == 0) { + push(@new_rows, ($1 // '') . ' [@ ' . $perl_block[0] . ' @]*' . ($2 // '')); + } else { + push(@new_rows, ($1 // '') . ' [@ ' . shift(@perl_block), @perl_block, ' @]*' . ($2 // '')); + } + } else { + push @new_rows, $row; + } + + } + return \@new_rows; +} + +# Convert many ans_rule constructs to the PGML answer blank form [_]{$var}. +# This is called recursively to handle multiple ans_rule on a single line. + +sub convertANSrule { + my ($str) = @_; + if ($str =~ /(.*)\\\{\s*((\$\w+)->)?ans_rule(\((\d*)\))?\s*\\\}(.*)$/) { + my $ans = shift(@ans_list); + my $var = $3 // $ans->{arg} // ''; + my $size = $5 ? "{$5}" : ''; + return convertANSrule($1 // '') . '[_]' . "{$var}$size" . convertANSrule($6 // ''); + } else { + return $str; + } +} + +# remove some unnecessary code including: +# * removing TEXT(beginproblem()) +# * removing Context()->texStrings; +# * removing Context()->normalStrings; +# * commenting out ANS, WEIGHTED_ANS, NAMED_ANS or LABELED_ANS +# * removing any line that only comment symbols. + +sub cleanUpCode { + my ($row) = @_; + $row =~ s/^\s*#+\s*$//; + $row =~ s/Context\(\)->normalStrings;//; + $row =~ s/Context\(\)->texStrings;//; + $row =~ s/TEXT\(\s*&?beginproblem(\(\))?\s*\);//; + $row =~ s/^(LABELED_|NAMED_|WEIGHTED_|)ANS(.*)/# $1ANS$2/; + return $row; +} + +# Loads the entire file searching for instances of ANS, WEIGHTED_ANS, NAMED_ANS or LABELED_ANS +# and returns an arrayref with an ordered list of them. +sub getANS { + my ($pg_source) = @_; + my @ans_list; + for my $row (split(/\n/, $pg_source)) { + if ($row !~ /^\s*#/ && $row =~ /(LABELED_|NAMED_|WEIGHTED_|)ANS/) { + # For style like ANS($ans->cmp()); + if ($row =~ /((LABELED_|NAMED_|WEIGHTED_|)ANS)\(\s*([\$\w]+)->(\w+)(\(\))?\s*\)/) { + push(@ans_list, { type => $1, arg => $3 }); + # for style like ANS(num_cmp($ans)) + } elsif ($row =~ /((LABELED_|NAMED_|WEIGHTED_|)ANS)\(\s*(([\w\_]+)\((\$[\w\_]+)\))\)/) { + my $type = $1; + my $arg = $3 =~ s/(std_)?num_cmp/Real/r; + $arg =~ s/str_cmp|std_num_cmp/String/; + $arg =~ s/interval_cmp/Interval/; + $arg =~ s/fun_cmp/Formula/; + $arg =~ s/radio_cmp|checkbox_cmp//; + push(@ans_list, { type => $type, arg => $arg }); + } + } + } + return @ans_list; +} + +1; diff --git a/lib/WeBWorK/PG/Environment.pm b/lib/WeBWorK/PG/Environment.pm index 8f12e43fce..3a882893f1 100644 --- a/lib/WeBWorK/PG/Environment.pm +++ b/lib/WeBWorK/PG/Environment.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/WeBWorK/PG/EquationCache.pm b/lib/WeBWorK/PG/EquationCache.pm index 6ea35d91d3..e822f00278 100644 --- a/lib/WeBWorK/PG/EquationCache.pm +++ b/lib/WeBWorK/PG/EquationCache.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/WeBWorK/PG/IO.pm b/lib/WeBWorK/PG/IO.pm index 9329cf8da0..6b3e7d1eaa 100644 --- a/lib/WeBWorK/PG/IO.pm +++ b/lib/WeBWorK/PG/IO.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -16,86 +16,85 @@ package WeBWorK::PG::IO; use parent qw(Exporter); +=head1 NAME + +WeBWorK::PG::IO - Functions used by C for file IO. + +=head1 DESCRIPTION + +This module defines several functions to be shared with a safe compartment by +the PG translator. All exported methods are shared. + +=cut + use strict; use warnings; +use utf8; -use Encode qw( encode decode); +use Encode qw(encode decode); use JSON qw(decode_json); use File::Spec::Functions qw(canonpath); +use File::Find qw(finddepth); + use PGUtil qw(not_null); use WeBWorK::PG::Environment; -use utf8; -#binmode(STDOUT,":encoding(UTF-8)"); -#binmode(STDIN,":encoding(UTF-8)"); -#binmode(INPUT,":encoding(UTF-8)"); - -my $pg_envir = WeBWorK::PG::Environment->new; - -=head1 NAME - -WeBWorK::PG::IO - Functions used by WeBWorK::PG::Translator for file IO. -=cut - -our @EXPORT = qw( +our @EXPORT_OK = qw( includePGtext read_whole_problem_file read_whole_file - convertPath fileFromPath directoryFromPath - createDirectory ); -=head1 SYNOPSIS - - use WeBWorK::PG::IO; - my %functions_to_share = %WeBWorK::PG::IO::SHARE; - -=head1 DESCRIPTION - -This module defines several functions to be shared with a safe compartment by -the PG translator. All exported methods are shared. +my $pg_envir = WeBWorK::PG::Environment->new; =head1 FUNCTIONS -=over +=head2 includePGtext -=item includePGtext($string_ref) +This is used in processing some of the sample CAPA files and in creating aliases +to redirect calls to duplicate problems so that they go to the original problem +instead. It is called by includePGproblem. -This is used in processing some of the sample CAPA files and -in creating aliases to redirect calls to duplicate problems so that -they go to the original problem instead. It is called by includePGproblem. +Usage: C -It reads and evaluates the string in the same way that the Translator evaluates the string in a PG file. +Note that the C<$str> parameter may be a string or a reference to a string. + +It reads and evaluates the string in the same way that the Translator evaluates +the string in a PG file. =cut sub includePGtext { my $evalString = shift; - if (ref($evalString) eq 'SCALAR') { - $evalString = $$evalString; - } + $evalString = $$evalString if ref($evalString) eq 'SCALAR'; no strict; - $evalString = eval(q! &{$main::PREPROCESS_CODE}($evalString) !); - $evalString = $evalString || ''; - # current preprocessing code passed from Translator (see Translator::initialization) + + # Preprocess code (this method is shared to the WWSafe compartment in Translator::initialize) + $evalString = eval(q!&{$main::PREPROCESS_CODE}($evalString)!) || ''; + my $errors = $@; eval("package main; $evalString"); $errors .= $@; - die eval(q! "ERROR in included file:\n$main::envir{probFileName}\n $errors\n$evalString"!) if $errors; + die eval(q!"ERROR in included file:\n$main::envir{probFileName}\n$errors\n$evalString"!) if $errors; + use strict; - return ""; + + return ''; } -=item read_whole_problem_file($filePath) +=head2 read_whole_problem_file + +Read the contents of a pg file. + +Usage: C Don't use for huge files. The file name will have .pg appended to it if it -doesn't already end in .pg. Files may become double spaced.? Check the join -below. This is used in importing additional .pg files as is done in the sample -problems translated from CAPA. Returns a reference to a string containing the -contents of the file. +doesn't already end in .pg. This is used in importing additional .pg files as +is done in the sample problems translated from CAPA. Returns a reference to a +string containing the contents of the file. =cut @@ -103,73 +102,77 @@ sub read_whole_problem_file { my $filePath = shift; $filePath =~ s/^\s*|\s$//g; # get rid of leading and trailing spaces $filePath = "$filePath.pg" unless $filePath =~ /\.pg$/; - read_whole_file($filePath); + return read_whole_file($filePath); } +=head2 read_whole_file + +Read the contents of a file. Don't use for huge files. + +Usage: C + +=cut + sub read_whole_file { my $filePath = shift; - warn "Can't read file $filePath
                      " unless -r $filePath; - return "" unless -r $filePath; - die "File path $filePath is unsafe." - unless path_is_readable_subdir($filePath); - - local (*INPUT); - open(INPUT, "<:raw", $filePath) || die "$0: read_whole_file subroutine:
                      Can't read file $filePath"; - local ($/) = undef; - my $string = ; + + unless (-r $filePath) { + warn "Can't read file $filePath."; + return ''; + } + die "File path $filePath is unsafe." unless path_is_readable_subdir($filePath); + + open(my $INPUT, "<:raw", $filePath) or die "$0: read_whole_file subroutine: Can't read file $filePath"; + local $/ = undef; + my $string = <$INPUT>; + close($INPUT); + my $backup_string = $string; - # can't append spaces because this causes trouble with <<'EOF' \nEOF construction - my $success = utf8::decode($string); - unless ($success) { + unless (utf8::decode($string)) { warn "There was an error decoding $filePath as UTF-8, will try to upgrade"; - utf8: upgrade($backup_string); - $string = $backup_string; + $string = utf8::upgrade($backup_string); } - close(INPUT); - \$string; + + return \$string; } # <:utf8 is more relaxed on input, <:encoding(UTF-8) would be better, but # perhaps it's not so horrible to have lax input. encoding(UTF-8) tries to use require # to import Encode, Encode::Alias::find_encoding and Safe raises an exception. # haven't figured a way around this yet. -=item convertPath($path) - -Currently a no-op. Returns $path unmodified. - -=cut - -sub convertPath { - return wantarray ? @_ : shift; -} +=head2 fileFromPath -=item fileFromPath($path) +Usage: C -Returns the last segment of the path (i.e. the text after the last forward slash). +Returns the last segment of the path (i.e. the text after the last forward +slash). =cut sub fileFromPath { my $path = shift; - $path = convertPath($path); $path =~ m|([^/]+)$|; - $1; + return $1; } -=item directoryFromPath($path) +=head2 directoryFromPath -Returns the initial segments of the of the path (i.e. the text up to the last forward slash). +Usage: C + +Returns the initial segments of the of the path (i.e. the text up to the last +forward slash). =cut sub directoryFromPath { my $path = shift; - $path = convertPath($path); $path =~ s|[^/]*$||; - $path; + return $path; } -=item createFile($fileName, $permission, $numgid) +=head2 createFile + +Usage: C Creates a file with the given name, permission bits, and group ID. @@ -180,22 +183,25 @@ sub createFile { die 'Path is unsafe' unless path_is_readable_subdir($fileName); - open(TEMPCREATEFILE, ">:encoding(UTF-8)", $fileName) - or die "Can't open $fileName: $!"; - my @stat = stat TEMPCREATEFILE; - close(TEMPCREATEFILE); + open(my $TEMPCREATEFILE, ">:encoding(UTF-8)", $fileName) or die "Can't open $fileName: $!"; + my @stat = stat $TEMPCREATEFILE; + close($TEMPCREATEFILE); - # if the owner of the file is running this script (e.g. when the file is - # first created) set the permissions and group correctly + # If the owner of the file is running this script (e.g. when the file is + # first created) set the permissions and group correctly. if ($< == $stat[4]) { my $tmp = chmod($permission, $fileName) or warn "Can't do chmod($permission, $fileName): $!"; chown(-1, $numgid, $fileName) or warn "Can't do chown($numgid, $fileName): $!"; } + + return; } -=item createDirectory($dirName, $permission, $numgid) +=head2 createDirectory + +Usage: C Creates a directory with the given name, permission bits, and group ID. @@ -204,8 +210,8 @@ Creates a directory with the given name, permission bits, and group ID. sub createDirectory { my ($dirName, $permission, $numgid) = @_; - $permission = (defined($permission)) ? $permission : '0770'; - # FIXME -- find out where the permission is supposed to be defined + $permission //= oct(770); + my $errors = ''; mkdir($dirName, $permission) or $errors .= "Can't do mkdir($dirName, $permission): $!\n" . caller(3); @@ -215,6 +221,7 @@ sub createDirectory { chown(-1, $numgid, $dirName) or $errors .= "Can't do chown(-1,$numgid,$dirName): $!\n" . caller(3); } + if ($errors) { warn $errors; return 0; @@ -223,6 +230,26 @@ sub createDirectory { } } +=head2 remove_tree + +Usage: C + +Remove a directory and its contents. + +=cut + +sub remove_tree { + my $dir = shift; + + finddepth sub { + if (!-l && -d _) { + rmdir($File::Find::name) or warn "Unable to remove directory $File::Find::name: $!"; + } else { + unlink($File::Find::name) or warn "Unable to delete file $File::Find::name: $!"; + } + }, $dir; +} + # This is needed for the subroutine below. It is copied from WeBWorK::Utils. # Note: if a place for common code is ever created this should go there. @@ -248,9 +275,12 @@ sub path_is_subdir { return 1; } -=item path_is_readable_subdir($path) +=head2 path_is_readable_subdir + +Usage: C -Checks to see if the given path is a sub directory of the directory the caller says we are allowed to read from. +Checks to see if the given path is a sub directory of the directory the caller +says we are allowed to read from. =cut @@ -258,49 +288,38 @@ sub path_is_readable_subdir { return path_is_subdir(shift, $pg_envir->{directories}{permitted_read_dir}, 1); } -sub pg_tmp_dir { - return $pg_envir->{directories}{tmp}; -} - -=item curlCommand +=head2 pg_tmp_dir - curl -- path to curl defined in site.conf +Returns the temporary directory set in the WeBWorK::PG::Environment. =cut -sub curlCommand { - return $pg_envir->{externalPrograms}{curl}; +sub pg_tmp_dir { + return $pg_envir->{directories}{tmp}; } -=item copyCommand - - copyCommand -- path to cp defined in site.conf - -=cut - -sub copyCommand { - return $pg_envir->{externalPrograms}{cp}; -} +=head2 externalCommand -=item externalCommand +Usage: C - returns the path to a requested external command that is defined in site.conf +Returns the path to a requested external command that is defined in the +C. =cut sub externalCommand { - return $pg_envir->{externalPrograms}{ $_[0] }; + my $cmd = shift; + return $pg_envir->{externalPrograms}{$cmd}; } -# -# isolate the call to the sage server in case we have to jazz it up -# +# Isolate the call to the sage server in case we have to jazz it up. sub query_sage_server { my ($python, $url, $accepted_tos, $setSeed, $webworkfunc, $debug, $curlCommand) = @_; -# my $sagecall = qq{$curlCommand -i -k -sS -L --http1.1 --data-urlencode "accepted_tos=${accepted_tos}"}. -# qq{ --data-urlencode 'user_expressions={"WEBWORK":"_webwork_safe_json(WEBWORK)"}' --data-urlencode "code=${setSeed}${webworkfunc}$python" $url}; - my $sagecall = qq{$curlCommand -i -k -sS -L --data-urlencode "accepted_tos=${accepted_tos}"} - . qq{ --data-urlencode 'user_expressions={"WEBWORK":"_webwork_safe_json(WEBWORK)"}' --data-urlencode "code=${setSeed}${webworkfunc}$python" $url}; + my $sagecall = + qq{$curlCommand -i -k -sS -L } + . qq{--data-urlencode "accepted_tos=${accepted_tos}" } + . qq{--data-urlencode 'user_expressions={"WEBWORK":"_webwork_safe_json(WEBWORK)"}' } + . qq{--data-urlencode "code=${setSeed}${webworkfunc}$python" $url}; my $output = `$sagecall`; if ($debug) { @@ -309,7 +328,8 @@ sub query_sage_server { warn "\n\nRETURN from sage call \n", $output, "\n\n"; warn "\n\n END SAGE CALL"; } - # has something been returned? + + # Has something been returned? # $continue: HTTP/1.1 100 (Continue) # $header: HTTP/1.1 200 OK # Content-Length: 1625 @@ -337,47 +357,54 @@ sub query_sage_server { # content my ($continue, $header, @content) = split("\r\n\r\n", $output); - #my $content = join("\r\n\r\n",@content); # handle case where there were blank lines in the content my @lines = split("\r\n\r\n", $output); $continue = 0; my $header_ok = 0; while (@lines) { my $header_block = shift(@lines); warn "checking for header: $header_block" if $debug; - next unless $header_block =~ /\S/; #skip empty lines; + next unless $header_block =~ /\S/; # skip empty lines; next if ($header_block =~ m!HTTP[ 12/.]+100!); # skip continue line if ($header_block =~ m!HTTP[ 12/.]+200!) { # 200 return is ok $header_ok = 1; last; } } - my $content = join("|||\n|||", @lines); #headers have been removed. - #warn "output list is ", $content; # join("|||\n|||",($continue, $header, $content)); - #warn "header_ok is $header_ok"; + my $content = join("|||\n|||", @lines); # headers have been removed. my $result; - if ($header_ok) { #success put any extraneous splits back together + if ($header_ok) { + # Success! Put any extraneous splits back together. $result = join("\r\n\r\n", @lines); } else { - warn "ERROR in contacting sage server. Did you accept the terms of service by - setting {accepted_tos=>'true'} in the askSage options?\n $content\n"; + warn "ERROR in contacting sage server. Did you accept the terms of service by " + . "setting { accepted_tos => 'true' } in the askSage options?\n$content\n"; $result = undef; } - $result; + + return $result; } +=head2 AskSage + +Usage: C + +Executes a sage cell server query via curl and returns the result. + +=cut + sub AskSage { - # - # to send values back in a hash, add them to the python WEBWORK dictionary - # - chomp(my $python = shift); - my $args = shift @_; + my ($python, $args) = @_; + chomp($python); + + # To send values back in a hash, add them to the python WEBWORK dictionary. my $url = $args->{url} || 'https://sagecell.sagemath.org/service'; my $seed = $args->{seed}; - my $accepted_tos = $args->{accepted_tos} || 'false'; # force author to accept terms of service explicitly :-) + my $accepted_tos = $args->{accepted_tos} || 'false'; # Force author to accept terms of service explicitly. my $debug = $args->{debug} || 0; my $setSeed = $seed ? "set_random_seed($seed)\n" : ''; my $curlCommand = $args->{curlCommand}; - my $webworkfunc = < 0 }; # we want to export more than one piece of information + my $ret = { success => 0 }; # We want to export more than one piece of information. eval { my $output = query_sage_server($python, $url, $accepted_tos, $setSeed, $webworkfunc, $debug, $curlCommand); @@ -421,16 +448,15 @@ END my $warning_string = "decoded contents\n "; foreach my $key (keys %$decoded) { $warning_string .= "$key=" . $decoded->{$key} . ", "; } $warning_string .= ' end decoded contents'; - #warn "\n$warning_string" if $debug; warn " decoded contents \n", PGUtil::pretty_print($decoded, 'text'), "end decoded contents" if $debug; } - # was there a Sage/python syntax Error - # is the returned something text from stdout (deprecated) - # have objects been returned in a WEBWORK variable? + # Was there a Sage/python syntax error? + # Is the returned something text from stdout? (deprecated) + # Have objects been returned in a WEBWORK variable? my $success = 0; $success = $decoded->{success} if defined $decoded and $decoded->{success}; warn "success is $success" if $debug; - # the decoding process seems to change the string "true" to "1" sometimes -- we could enforce this + # The decoding process seems to change the string "true" to "1" sometimes -- we could enforce this $success = 1 if defined $success and $success eq 'true'; $success = 1 if $decoded->{execute_reply}->{status} eq 'ok'; warn "now success is $success because status was ok" if $debug; @@ -439,37 +465,39 @@ END my $sage_WEBWORK_data = $decoded->{execute_reply}{user_expressions}{WEBWORK}{data}{'text/plain'}; warn "sage_WEBWORK_data $sage_WEBWORK_data" if $debug; if (not_null($sage_WEBWORK_data)) { - $WEBWORK_variable_non_empty = #another hack because '{}' is sometimes returned + $WEBWORK_variable_non_empty = # another hack because '{}' is sometimes returned ($sage_WEBWORK_data ne "{}" and $sage_WEBWORK_data ne "'{}'") ? 1 : 0; } # {} indicates that WEBWORK was not used to pass or return a variable from sage. warn "WEBWORK variable has content" if $debug and $WEBWORK_variable_non_empty; - $sage_WEBWORK_data =~ s/^'//; #FIXME -- for now strip off the surrounding single quotes '. + $sage_WEBWORK_data =~ s/^'//; # FIXME: For now strip off the surrounding single quotes. $sage_WEBWORK_data =~ s/'$//; warn "sage_WEBWORK_data: ", PGUtil::pretty_print($sage_WEBWORK_data) if $debug and $WEBWORK_variable_non_empty; if ($WEBWORK_variable_non_empty) { - # have specific WEBWORK variables been defined? + # Have specific WEBWORK variables been defined? $ret->{webwork} = decode_json($sage_WEBWORK_data); $ret->{success} = 1; $ret->{stdout} = $decoded->{stdout}; - } elsif (not_null($decoded->{stdout})) { # no WEBWORK content, but stdout exists - # old style text output via stdout (deprecated) - $ret = $decoded->{stdout}; # only standard out is returned + } elsif (not_null($decoded->{stdout})) { + # No WEBWORK content, but stdout exists. + # Old style text output via stdout (deprecated) + $ret = $decoded->{stdout}; # only standard out is returned warn "no content in WEBWORK variable. Returning stdout", $ret if $debug; } else { die "Error receiving JSON output from sage: \n$output\n "; } - } elsif ($success == 0) { # this might be a syntax error - $ret->{error_message} = - $decoded->{execute_reply}; # this is a hash. # need a better pretty print method + } elsif ($success == 0) { + # This might be a syntax error. + $ret->{error_message} = $decoded->{execute_reply}; # This is a hash. Need a better pretty print method. warn("IO.pm: Perhaps there was syntax error.", join(" ", %{ $decoded->{execute_reply} })); } else { die "IO.pm: Unknown error in asking Sage to do something: success = $success output = \n$output\n"; } }; # end eval{} for trapping errors in sage call + if ($@) { warn "IO.pm: ERROR trapped during JSON call to sage:\n $@ "; if (ref($ret) =~ /HASH/) { @@ -478,11 +506,8 @@ END $ret = undef; } } + return $ret; } -=back - -=cut - 1; diff --git a/lib/WeBWorK/PG/ImageGenerator.pm b/lib/WeBWorK/PG/ImageGenerator.pm index 1bb134e62c..de9ac15b8c 100644 --- a/lib/WeBWorK/PG/ImageGenerator.pm +++ b/lib/WeBWorK/PG/ImageGenerator.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -477,7 +477,7 @@ sub fix_markers ($self) { my %depths = %{ $self->{depths} }; for my $depthkey (keys %depths) { if ($depths{$depthkey} eq 'none') { - ${ $self->{body_text} } =~ s/MaRkEr$depthkey/style="vertical-align:"$self->{dvipng_align}"/g; + ${ $self->{body_text} } =~ s/MaRkEr$depthkey/style="vertical-align:$self->{dvipng_align}"/g; } else { my $ndepth = 0 - $depths{$depthkey}; ${ $self->{body_text} } =~ s/MaRkEr$depthkey/style="vertical-align:${ndepth}px"/g; diff --git a/lib/WeBWorK/PG/Localize.pm b/lib/WeBWorK/PG/Localize.pm new file mode 100644 index 0000000000..a56788f4df --- /dev/null +++ b/lib/WeBWorK/PG/Localize.pm @@ -0,0 +1,284 @@ +package WeBWorK::PG::Localize; +use parent 'Locale::Maketext'; + +use strict; +use warnings; + +use File::Spec; +use Locale::Maketext::Lexicon; + +Locale::Maketext::Lexicon->import({ + 'i-default' => ['Auto'], + '*' => [ Gettext => File::Spec->catfile("$ENV{PG_ROOT}/lib/WeBWorK/PG/Localize", '*.[pm]o') ], + _decode => 1, + _encoding => undef, +}); +*tense = sub { \$_[1] . ((\$_[2] eq 'present') ? 'ing' : 'ed') }; + +# This subroutine is used to pass a language handle into the safe +# compartment so that maketext can be used in problems and macros. +sub getLoc { + my $lang = shift; + my $lh = WeBWorK::PG::Localize->get_handle($lang); + return sub { $lh->maketext(@_) }; +} + +sub getLangHandle { + my $lang = shift; + return WeBWorK::PG::Localize->get_handle($lang); +} + +# This is like [quant] but it doesn't write the number. +# usage: [quant,_1,,,] +sub plural { + my ($handle, $num, @forms) = @_; + + return '' if @forms == 0; + return $forms[2] if @forms > 2 && $num == 0; + + # Normal case: + return $handle->numerate($num, @forms); +} + +# This is like [quant] but it also has a negative case. (The one usage in the code interprets this as unlimited.) +# usage: [negquant,_1,,,,] +sub negquant { + my ($handle, $num, @forms) = @_; + + return $num if @forms == 0; + + my $negcase = shift @forms; + return $negcase if $num < 0; + + return $forms[2] if @forms > 2 && $num == 0; + return $handle->numf($num) . ' ' . $handle->numerate($num, @forms); +} + +our %Lexicon = ('_AUTO' => 1); + +# Override the Locale::Maketext::_compile method. The only real difference is that this override +# method does not call "use strict" in the code eval. Thus it can be used in the safe zone. +sub _compile { + my ($handle, $string_to_compile) = @_; + + # The while regex is more expensive than this check on strings that don't need a compile. + # This op causes a ~2% speed hit for strings that need compile and a 250% speed improvement + # on strings that don't need compiling. + return \"$string_to_compile" if $string_to_compile !~ m/[\[~\]]/ms; + + my @code; + my (@c) = (''); # "chunks" -- scratch. + my $call_count = 0; + my $big_pile = ''; + { + my $in_group = 0; # start out outside a group + my ($m, @params); # scratch + + while ( + $string_to_compile =~ # Iterate over chunks. + m/( + [^\~\[\]]+ # non-~[] stuff (Capture everything else here) + | + ~. # ~[, ~], ~~, ~other + | + \[ # [ presumably opening a group + | + \] # ] presumably closing a group + | + ~ # terminal ~ ? + | + $ + )/xgs + ) + { + if ($1 eq '[' || $1 eq '') { + # Whether this is "[" or end, force processing of any preceding literal. + if ($in_group) { + if ($1 eq '') { + $handle->_die_pointing($string_to_compile, 'Unterminated bracket group'); + } else { + $handle->_die_pointing($string_to_compile, 'You can\'t nest bracket groups'); + } + } else { + $in_group = 1 if ($1 ne ''); + + die "How come \@c is empty?? in <$string_to_compile>" unless @c; # sanity + if (length $c[-1]) { + # Now actually processing the preceding literal + $big_pile .= $c[-1]; + if ( + $Locale::Maketext::USE_LITERALS && ( + (ord('A') == 65) + ? $c[-1] !~ m/[^\x20-\x7E]/s + # ASCII very safe chars + : $c[-1] !~ m/[^ !"\#\$%&'()*+,\-.\/0-9:;<=>?\@A-Z[\\\]^_`a-z{|}~\x07]/s + # EBCDIC very safe chars + ) + ) + { + # Normal case -- all very safe chars + $c[-1] =~ s/'/\\'/g; + push @code, q{ '} . $c[-1] . "',\n"; + $c[-1] = ''; # reuse this slot + } else { + $c[-1] =~ s/\\\\/\\/g; + push @code, ' $c[' . $#c . "],\n"; + push @c, ''; # new chunk + } + } + # else just ignore the empty string. + } + + } elsif ($1 eq ']') { + # Close group -- go back in-band + if ($in_group) { + $in_group = 0; + + # And now process the group... + + if (!length($c[-1]) || $c[-1] =~ m/^\s+$/s) { + $c[-1] = ''; # Reset out chunk + next; + } + + ($m, @params) = split(/,/, $c[-1], -1); + + # A bit of a hack -- we've turned "~,"'s into DELs, so turn them into real commas here. + if (ord('A') == 65) { + # ASCII, etc + for ($m, @params) {tr/\x7F/,/} + } else { + # EBCDIC (1047, 0037, POSIX-BC) + for ($m, @params) {tr/\x07/,/} + } + + # Special-case handling of some method names: + if ($m eq '_*' || $m =~ m/^_(-?\d+)$/s) { + # Treat [_1,...] as [,_1,...], etc. + unshift @params, $m; + $m = ''; + } elsif ($m eq '*') { + $m = 'quant'; # "*" for "times": "4 cars" is 4 times "cars" + } elsif ($m eq '#') { + $m = 'numf'; # "#" for "number": [#,_1] for "the number _1" + } + + # Most common case: a simple, legal-looking method name. + if ($m eq '') { + # 0-length method name means to just interpolate: + push @code, ' ('; + } elsif ($m =~ /^\w+$/s + && !$handle->{'blacklist'}{$m} + && (!defined $handle->{'whitelist'} || $handle->{'whitelist'}{$m})) + { + # Exclude anything fancy and restrict to the whitelist/blacklist. + push @code, ' $_[0]->' . $m . '('; + } else { + # TODO: Implement something? Or just too icky to consider? + $handle->_die_pointing( + $string_to_compile, + "Can't use \"$m\" as a method name in bracket group", + 2 + length($c[-1]) + ); + } + + pop @c; # we don't need that chunk anymore + ++$call_count; + + for my $p (@params) { + if ($p eq '_*') { + # Meaning: all parameters except $_[0] + $code[-1] .= ' @_[1 .. $#_], '; + # and yes, that does the right thing for all @_ < 3 + } elsif ($p =~ m/^_(-?\d+)$/s) { + # _3 meaning $_[3] + $code[-1] .= '$_[' . (0 + $1) . '], '; + } elsif ( + $Locale::Maketext::USE_LITERALS && ( + (ord('A') == 65) + ? $p !~ m/[^\x20-\x7E]/s + # ASCII very safe chars + : $p !~ m/[^ !"\#\$%&'()*+,\-.\/0-9:;<=>?\@A-Z[\\\]^_`a-z{|}~\x07]/s + # EBCDIC very safe chars + ) + ) + { + # Normal case: a literal containing only safe characters + $p =~ s/'/\\'/g; + $code[-1] .= q{'} . $p . q{', }; + } else { + # Stow it on the chunk-stack, and just refer to that. + push @c, $p; + push @code, ' $c[' . $#c . '], '; + } + } + $code[-1] .= "),\n"; + + push @c, ''; + } else { + $handle->_die_pointing($string_to_compile, q{Unbalanced ']'}); + } + + } elsif (substr($1, 0, 1) ne '~') { + # It's stuff not containing "~" or "[" or "]", i.e., a literal blob. + my $text = $1; + $text =~ s/\\/\\\\/g; + $c[-1] .= $text; + } elsif ($1 eq '~~') { + $c[-1] .= '~'; + } elsif ($1 eq '~[') { + $c[-1] .= '['; + } elsif ($1 eq '~]') { + $c[-1] .= ']'; + } elsif ($1 eq '~,') { + if ($in_group) { + # This is a hack, based on the assumption that no one will actually + # want a DEL inside a bracket group. Let's hope that's it's true. + if (ord('A') == 65) { + # ASCII etc + $c[-1] .= "\x7F"; + } else { + # EBCDIC (cp 1047, 0037, POSIX-BC) + $c[-1] .= "\x07"; + } + } else { + $c[-1] .= '~,'; + } + } elsif ($1 eq '~') { + # This is possible only at string end, it seems. + $c[-1] .= '~'; + } else { + # It's a "~X" where X is not a special character. + # Consider it a literal ~ and X. + my $text = $1; + $text =~ s/\\/\\\\/g; + $c[-1] .= $text; + } + } + } + + if ($call_count) { + undef $big_pile; # Well, nevermind that. + } else { + # It's all literals! So don't bother with the eval. Return a SCALAR reference. + return \$big_pile; + } + + die q{Last chunk isn't null??} if @c && length $c[-1]; # sanity + if (@code == 0) { + # Not possible? + return \''; + } elsif (@code > 1) { + # Most cases, presumably! + unshift @code, "join '',\n"; + } + unshift @code, "sub {\n"; + push @code, "}\n"; + + my $sub = eval(join '', @code); + die "$@ while eval-ling " . join('', @code) if $@; # Should be impossible. + + return $sub; +} + +1; diff --git a/lib/WeBWorK/PG/Localize/cs-CZ.po b/lib/WeBWorK/PG/Localize/cs-CZ.po new file mode 100644 index 0000000000..6cebc8f7f9 --- /dev/null +++ b/lib/WeBWorK/PG/Localize/cs-CZ.po @@ -0,0 +1,252 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Robert Mařík , 2022-2023 +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:42-0500\n" +"Last-Translator: Robert Mařík , 2022-2023\n" +"Language-Team: Czech (Czech Republic) (http://app.transifex.com/webwork/" +"webwork2/language/cs_CZ/)\n" +"Language: cs_CZ\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n " +"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "%1% správně" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "Nebylo odpovězeno na %quant(%1,otázku,otázky)." + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "Správná odpověď" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "Nápověda:" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "Zpráva" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "Poznámka:" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "Řešení:" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "Zatím nehodnoceno" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "Za částečné zodpovězení této úlohy můžete obdržet částečné hodnocení." + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "" diff --git a/lib/WeBWorK/PG/Localize/de.po b/lib/WeBWorK/PG/Localize/de.po new file mode 100644 index 0000000000..50191e5f9f --- /dev/null +++ b/lib/WeBWorK/PG/Localize/de.po @@ -0,0 +1,256 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Armin Reiser, 2022 +# Armin Reiser, 2022 +# Fabian Gabel, 2022 +# Fabian Gabel, 2022 +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:42-0500\n" +"Last-Translator: Fabian Gabel, 2022\n" +"Language-Team: German (http://app.transifex.com/webwork/webwork2/language/" +"de/)\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "%1% richtig" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "%quant(%1,Frage wurde,Fragen wurden) nicht beantwortet." + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "Richtig" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "Lösung" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "Neue Version dieser Aufgabe anfordern" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "Hinweis: " + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "Hinweis: " + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "Falsch" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "Fehlermeldung" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "Note:" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "Lösung:" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "Lösung: " + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "Dies is eine neu randomisierte Version der Aufgabe." + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "Diese Aufgabe besteht aus mehreren Teilen." + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "Für diese Aufgabe können Sie Teilpunkte erhalten." + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" +"Nach Ablauf der Bearbeitungszeit erhalten Sie eine neue Version dieser " +"Aufgabe" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "Antworten können im nächsten Teil nicht mehr geändert werden!" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "Antwort" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "Spalte" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "Aufgabe" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "" diff --git a/lib/WeBWorK/PG/Localize/el.po b/lib/WeBWorK/PG/Localize/el.po new file mode 100644 index 0000000000..fae1eead4c --- /dev/null +++ b/lib/WeBWorK/PG/Localize/el.po @@ -0,0 +1,255 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +# Translators: +# Glenn Rice, 2023 +# Ioannis Souldatos, 2023 +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-11-19 04:33+0000\n" +"Last-Translator: Ioannis Souldatos, 2023\n" +"Language-Team: Greek (https://app.transifex.com/webwork/teams/16644/el/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: el\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "%1 από τις απαντήσεις ΔΕΝ είναι %plural(%1,σωστή, σωστές)." + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "%1 από τις απαντήσεις θα βαθμολογηθούν αργότερα. " + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "%1% σωστό" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "" +"%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "" +"%quant(%1,των ερωτήσεων απομένει, των ερωτήσεων απομένουν) αναπάντητες." + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "Όλες οι απαντήσεις είναι σωστές." + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "Όλες οι απαντήσεις που παίρνουν βαθμό είναι σωστές." + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "Σωστό" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "Σωστή Απάντηση" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "Λάθος" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "Λήψη νέας εκδοχής του προβλήματος" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "Επιστροφή σε Μέρος 1" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "Μετάβαση στο επόμενο" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "Στην έντυπη μορφή εμφανίζεται πάντα η αρχική μορφή του προβλήματος." + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1402 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint:" +msgstr "Υπόδειξη:" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" +"Αν επιστρέψετε αργότερα, ενδέχεται να επανέλθει στην αρχική του μορφή." + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "Λάθος" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "Μήνυμα" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "Σημείωση:" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "Προεπισκόπηση" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "Προεπισκόπηση απάντησης" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "Ορισμός τυχαίου αριθμού:" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +msgid "Solution:" +msgstr "Απάντηση:" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "Υποβάλετε τις απαντήσεις σας ξανά για να συνεχίσετε." + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "Η απάντηση ΔΕΝ είναι σωστή." + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "Η απάντηση είναι σωστή!" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "Η απάντηση θα βαθμολογηθεί αργότερα." + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "Η ερώτηση δεν έχει απαντηθεί." + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "Η απάντηση θα βαθμολογηθεί αργότερα. " + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "Αυτή είναι μια νέα (επανατυχαιοποιημένη) έκδοση του προβλήματος." + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" +"Εδώ μπορείτε να εισάγετε την απάντησή σας. \n" +"\n" +"Όταν υποβάλετε την απάντησή σας, θα αποθηκευτεί και κατόπιν θα διορθωθεί από τον υπεύθυνο καθηγητή. Εάν ο υπεύθυνος καθηγητής κάνει κάποια σχόλια, τα σχόλια αυτά θα εμφανιστούν σε αυτή τη σελίδα. \n" +"\n" +"Μπορείτε να χρησιμοποιήσετε κώδικα LaTeX στην απάντησή σας, όμως ο κώδικας θα πρέπει να μπει μέσα σε παρενθέσεις και όχι μέσα σε σύμβολα $. " + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3062 +msgid "This problem contains a video which must be viewed online." +msgstr "" +"Αυτό το πρόβλημα περιέχει ένα βίντεο που πρέπει να προβληθεί στο διαδίκτυο." + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "Αυτό το πρόβλημα έχει περισσότερα από ένα μέρη." + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "Σωστό" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "Χωρίς βαθμό" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "Εισάγατε" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "Μπορείτε να κερδίσετε μερική πίστωση σε αυτό το πρόβλημα." + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" +"Μπορείτε να λάβετε νέα εκδοχή του προβλήματος μετά την ημερομηνία λήξης." + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "Μη δεκτές αλλαγές σε απαντήσεις αν συνεχίσετε στο επόμενο μέρος!" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3055 +msgid "Your browser does not support the video tag." +msgstr "Ο περιηγητής σας δεν υποστηρίζει την ετικέτα βίντεο." + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "Το σκορ σας σε αυτήν την προσπάθεια είναι μόνο για αυτό το μέρος˙" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "απάντηση" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "στήλη" + +#. ('j', 'k', '_0') +#. ('j', 'k') +# does not need to be translated +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "i " + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "εάν" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "αλλιώς" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "μέρος" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "πρόβλημα" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "σειρά" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "όταν υποβάλλετε τις απαντήσεις σας" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "το συνολικό σκορ σας είναι για όλα τα μέρη μαζί. " diff --git a/lib/WeBWorK/PG/Localize/en.po b/lib/WeBWorK/PG/Localize/en.po new file mode 100644 index 0000000000..f5acf329f1 --- /dev/null +++ b/lib/WeBWorK/PG/Localize/en.po @@ -0,0 +1,251 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:43-0500\n" +"Last-Translator: Jason Aubrey \n" +"Language-Team: English (United States) (http://www.transifex.com/webwork/" +"webwork2/language/en_US/)\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +# does not need to be translated +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "" diff --git a/lib/WeBWorK/PG/Localize/es.po b/lib/WeBWorK/PG/Localize/es.po new file mode 100644 index 0000000000..c1767029ca --- /dev/null +++ b/lib/WeBWorK/PG/Localize/es.po @@ -0,0 +1,254 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Andrés Forero , 2020 +# Enrique Acosta , 2020 +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:43-0500\n" +"Last-Translator: Enrique Acosta , 2020\n" +"Language-Team: Spanish (http://app.transifex.com/webwork/webwork2/language/" +"es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? " +"1 : 2;\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "" +"%quant(%1,de las preguntas sigue,de las preguntas siguen) sin respuesta." + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "Correcto" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "Pista:" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "incorrecto" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "Nota:" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "Solución:" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "Puedes obtener una fracción del puntaje total en este problema." + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "Problema" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "" diff --git a/lib/WeBWorK/PG/Localize/fr-CA.po b/lib/WeBWorK/PG/Localize/fr-CA.po new file mode 100644 index 0000000000..81c54deb2b --- /dev/null +++ b/lib/WeBWorK/PG/Localize/fr-CA.po @@ -0,0 +1,261 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Hélène Larue , 2017 +# Jonathan Desaulniers , 2018-2019,2021-2022 +# Julie Tremblay , 2016-2017 +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:43-0500\n" +"Last-Translator: Jonathan Desaulniers , 2018-2019,2021-2022\n" +"Language-Team: French (Canada) (http://app.transifex.com/webwork/webwork2/" +"language/fr_CA/)\n" +"Language: fr_CA\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % " +"1000000 == 0 ? 1 : 2;\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "%1% correct" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "%quant(%1, question reste, questions restent) sans réponses." + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "Réussi" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "Bonne réponse" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "Faux" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "Obtenir une nouvelle version du problème" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "Retour à la partie 1" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "Aller à la partie suivante" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" +"Le document sera toujours imprimer avec la version originale du problème." + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "Indice :" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "Indice:" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" +"Si vous revenez plus tard, le problème pourrait être revenu à sa version " +"originale." + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "Erroné" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "Message" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "Note:" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "Définir la source aléatoire à:" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "Solution :" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "Solution:" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "Soumettre votre réponse à nouveau pour passer à la partie suivante." + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "Voici une nouvelle version (à nouveau randomisée) du problème." + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "Ce problème contient une vidéo qui doit être consultée en ligne." + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "Ce problème comporte plusieurs parties." + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "Vrai" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "Non classé" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "Vous pouvez obtenir une partie des points pour ce problème." + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" +"Vous pouvez obtenir une nouvelle version du problème après la date de remise." + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" +"Vous ne pouvez pas modifier vos réponses en passant à la partie suivante!" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "Votre navigateur ne supporte pas l'environnement vidéo." + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" +"Votre résultat pour cette tentative correspond à cette partie seulement;" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "réponse" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "colonne" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "Je" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "Si" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "autrement" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "partie" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "problème" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "ligne" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "Quand vous soumettez vos réponses" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "Votre résultat d'ensemble est pour chacune des parties combinées." diff --git a/lib/WeBWorK/PG/Localize/fr.po b/lib/WeBWorK/PG/Localize/fr.po new file mode 100644 index 0000000000..6ad9848616 --- /dev/null +++ b/lib/WeBWorK/PG/Localize/fr.po @@ -0,0 +1,256 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Jonathan Desaulniers , 2018 +# Michael E Gage , 2014 +# Michael E Gage , 2011 +# Sébastien Labbé , Université du Québec à Montréal, 2011 +# Stéphanie Lanthier , 2011 +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:43-0500\n" +"Last-Translator: Stéphanie Lanthier , 2011\n" +"Language-Team: French (http://app.transifex.com/webwork/webwork2/language/" +"fr/)\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % " +"1000000 == 0 ? 1 : 2;\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "%1% correct" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "%quant(%1, des questions restent, des questions reste) sans réponse." + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "Correct" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "Mise à jour réussie" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "Vous pouvez obtenir une partie des points pour ce problème." + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "" diff --git a/lib/WeBWorK/PG/Localize/he-IL.po b/lib/WeBWorK/PG/Localize/he-IL.po new file mode 100644 index 0000000000..6353af588d --- /dev/null +++ b/lib/WeBWorK/PG/Localize/he-IL.po @@ -0,0 +1,255 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Michael Oren Perlstein , 2018 +# Nathan Wallach , 2018 +# Nathan Wallach , 2022-2023 +# Ran Kiri , 2018 +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:42-0500\n" +"Last-Translator: Nathan Wallach , 2022-2023\n" +"Language-Team: Hebrew (Israel) (http://app.transifex.com/webwork/webwork2/" +"language/he_IL/)\n" +"Language: he_IL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % " +"1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "%1% נכון" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "%quant(%1, מהשאלות לא נענתה, מהשאלות לא נענו)." + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "נכון" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "תשובה נכונה" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr " לא נכון" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "קבל גרסה חדשה של שאלה זו." + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "חזור לחקל 1" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "המשך לחלק הבא" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "בקבצי הדפסה תמיד יודפס הגרסה המקורית של השאלה." + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "רמז:" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "רמז: " + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "אם תחזור לשאלה בהמשל, ייתכן שהשאלה תחזור לגרסה המקורית." + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "לא נכון" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "הודעות" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "הערה:" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "החלף את זרע ההגרלות אל:" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "פתרון:" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "פתרון: " + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "שלח את התשובות שוב כדי להתקדם לחלק הבא." + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "הנה גרסה חדשה של השאלה." + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "השאלה מכילה וידיאו שיש לראות באופן מקוון." + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "לשאלה זו יש יותר מחלק אחד." + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "נכון" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "לא נוקדו" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "ניתן לקבל ניקוד חלקי בשאלה זו" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "אתה יכול לבקש גרסה שונה של שאלה זו לאחר מועד ההגשה." + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "לא ניתן לשנות תשובות כאשר אתה מתקדם לחלק הבא!" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "הדפדפן שלך לא תומך ב-video tag." + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "הציון שקבלת בהגשה זו הוא רק לחקל זה;" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "תשובה" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "עמודה" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "i" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "if" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "אחרת" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "חלק" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "שאלה" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "שורה" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "כאשר אתה מגיש את תשובותיך" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "הציון הכולל הוא על בסיס כל החלקים." diff --git a/lib/WeBWorK/PG/Localize/hu.po b/lib/WeBWorK/PG/Localize/hu.po new file mode 100644 index 0000000000..e2eedf2d86 --- /dev/null +++ b/lib/WeBWorK/PG/Localize/hu.po @@ -0,0 +1,251 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# pcsiba , 2014,2016-2017 +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:44-0500\n" +"Last-Translator: pcsiba , 2014,2016-2017\n" +"Language-Team: Hungarian (http://app.transifex.com/webwork/webwork2/language/" +"hu/)\n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "%1% helyes" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "%quant(%1,kérdés maradt,kérdés maradt) megválaszolatlan." + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "Helyes" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "Helyes válasz" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "Igaz" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "Ezért a feladatért részpontokat kaphat. " + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "válasz" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "oszlop" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "feladat" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "sor" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "" diff --git a/lib/WeBWorK/PG/Localize/ko.po b/lib/WeBWorK/PG/Localize/ko.po new file mode 100644 index 0000000000..78ffde31c8 --- /dev/null +++ b/lib/WeBWorK/PG/Localize/ko.po @@ -0,0 +1,251 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Ji-Young Ham , 2022 +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:44-0500\n" +"Last-Translator: Ji-Young Ham , 2022\n" +"Language-Team: Korean (http://app.transifex.com/webwork/webwork2/language/" +"ko/)\n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "정답" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "정답" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "오답" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "다음 단계로 이동하려면 답변을 다시 제출하십시오." + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "채점되지 않은" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "이 문제의 부분 점수를 받았습니다." + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "이 문제의 새 버전은 마감일 이후에 받을 수 있습니다." + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "입력답을 제출할 때에는" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "전체 점수는 모든 부분을 합한 것입니다." diff --git a/lib/WeBWorK/PG/Localize/pg.pot b/lib/WeBWorK/PG/Localize/pg.pot new file mode 100644 index 0000000000..741e2d962c --- /dev/null +++ b/lib/WeBWorK/PG/Localize/pg.pot @@ -0,0 +1,260 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2016-03-31 10:14-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#. ($numManuallyGraded) +#: /opt/webwork/pg/macros/PG.pl:1398 +msgid "%1 of the answers %plural(%1,has,have) been graded." +msgstr "" + +#. (@answerNames - $numBlank - $numCorrect - $numManuallyGraded) +#: /opt/webwork/pg/macros/PG.pl:1370 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numManuallyGraded) +#: /opt/webwork/pg/macros/PG.pl:1394 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. ($options{resultTitle}) +#: /opt/webwork/pg/macros/PG.pl:1195 +msgid "%1 with message" +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1088 +msgid "%1% correct" +msgstr "" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1383 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1357 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1348 +msgid "All of the computer gradable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1037 +msgid "Answer Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1210 +msgid "Close" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1080 +msgid "Correct" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1261 +msgid "Correct Answer" +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:495 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 /opt/webwork/pg/macros/core/compoundProblem.pl:498 /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:70 +msgid "Graded" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1327 +msgid "Hint" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1326 +msgid "Hint:" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1084 +msgid "Incorrect" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:144 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1242 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/ui/quickMatrixEntry.pl:100 /opt/webwork/pg/macros/ui/quickMatrixEntry.pl:59 +msgid "Quick Entry" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1268 +msgid "Reveal" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1319 +msgid "Solution" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1318 +msgid "Solution:" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1318 +msgid "The answer has been graded." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1336 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1307 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1317 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1327 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:91 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:158 +msgid "This is an essay answer text box. You can type your answer in here and, after you hit submit, it will be saved so that your instructor can grade it at a later date. If your instructor makes any comments on your answer those comments will appear on this page after the question has been graded. You can use LaTeX to make your math equations look pretty. LaTeX expressions should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3026 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:494 +msgid "True" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:68 +msgid "Ungraded" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1237 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1615 +msgid "You can earn partial credit on this problem." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3019 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:545 /opt/webwork/pg/macros/core/PGbasicmacros.pl:556 +msgid "answer" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:568 +msgid "column" +msgstr "" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:41 /opt/webwork/pg/lib/Value/Vector.pm:303 /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:562 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:551 +msgid "problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:568 +msgid "row" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "" diff --git a/lib/WeBWorK/PG/Localize/ru-RU.po b/lib/WeBWorK/PG/Localize/ru-RU.po new file mode 100644 index 0000000000..0495e38bba --- /dev/null +++ b/lib/WeBWorK/PG/Localize/ru-RU.po @@ -0,0 +1,252 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:44-0500\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Russian (Russia) (http://app.transifex.com/webwork/webwork2/" +"language/ru_RU/)\n" +"Language: ru_RU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || " +"(n%100>=11 && n%100<=14)? 2 : 3);\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "Верно" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "Правильные ответы" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "Балл за эту задачу может дробиться. " + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "" diff --git a/lib/WeBWorK/PG/Localize/tr.po b/lib/WeBWorK/PG/Localize/tr.po new file mode 100644 index 0000000000..a5fa6ad025 --- /dev/null +++ b/lib/WeBWorK/PG/Localize/tr.po @@ -0,0 +1,250 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:44-0500\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Turkish (http://app.transifex.com/webwork/webwork2/language/" +"tr/)\n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "%1% doğru" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "Soruların %1 tanesi yanıtsız bırakıldı." + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "Doğru yanıt" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "Bu sorudan kısmi puan alabilirsiniz." + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "" diff --git a/lib/WeBWorK/PG/Localize/zh-CN.po b/lib/WeBWorK/PG/Localize/zh-CN.po new file mode 100644 index 0000000000..e738394966 --- /dev/null +++ b/lib/WeBWorK/PG/Localize/zh-CN.po @@ -0,0 +1,253 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Liping Chen , 2013 +# Liping Chen , 2013 +# 李 明昊 , 2020 +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:45-0500\n" +"Last-Translator: Liping Chen , 2013\n" +"Language-Team: Chinese (China) (http://www.transifex.com/webwork/webwork2/" +"language/zh_CN/)\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "%1% 正确" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "%quant(%1,个问题,个问题) 未完成。" + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "正确" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "这一题你可以得到部分成绩" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "" diff --git a/lib/WeBWorK/PG/Localize/zh-HK.po b/lib/WeBWorK/PG/Localize/zh-HK.po new file mode 100644 index 0000000000..a9a1d45e72 --- /dev/null +++ b/lib/WeBWorK/PG/Localize/zh-HK.po @@ -0,0 +1,251 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Liping Chen , 2013 +msgid "" +msgstr "" +"Project-Id-Version: webwork2\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-10-05 06:45-0500\n" +"Last-Translator: Liping Chen , 2013\n" +"Language-Team: Chinese (Hong Kong) (http://app.transifex.com/webwork/" +"webwork2/language/zh_HK/)\n" +"Language: zh_HK\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. (@answerNames - $numBlank - $numCorrect - $numEssay) +#: /opt/webwork/pg/macros/PG.pl:1412 +msgid "%1 of the answers %plural(%1,is,are) NOT correct." +msgstr "" + +#. ($numEssay) +#: /opt/webwork/pg/macros/PG.pl:1435 +msgid "%1 of the answers will be graded later." +msgstr "" + +#. (round($answerScore * 100) +#: /opt/webwork/pg/macros/PG.pl:1157 +msgid "%1% correct" +msgstr "%1% 正确" + +#. ($numBlank) +#: /opt/webwork/pg/macros/PG.pl:1425 +msgid "%quant(%1,of the questions remains,of the questions remain) unanswered." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1399 +msgid "All of the answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1390 +msgid "All of the gradeable answers are correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1149 +msgid "Correct" +msgstr "正确" + +#: /opt/webwork/pg/macros/PG.pl:1308 +msgid "Correct Answer" +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:314 +msgid "False" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:186 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:187 +msgid "Get a new version of this problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:483 +msgid "Go back to Part 1" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:293 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:498 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:512 +msgid "Go on to next part" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:427 +msgid "Hardcopy will always print the original version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1404 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1405 +msgid "Hint:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1403 +msgid "Hint: " +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:425 +msgid "If you come back to it later, it may revert to its original version." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1153 +msgid "Incorrect" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1319 +msgid "Message" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:396 +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:422 +msgid "Note:" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1108 +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:131 +msgid "Preview" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1295 +msgid "Preview of Your Answer" +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:188 +msgid "Set random seed to:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1395 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1396 +msgid "Solution:" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:1394 +msgid "Solution: " +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:525 +msgid "Submit your answers again to go on to the next part." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1378 +msgid "The answer is NOT correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1351 +msgid "The answer is correct." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1360 +msgid "The answer will be graded later." +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1369 +msgid "The question has not been answered." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:78 +msgid "This answer will be graded at a later time." +msgstr "" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:423 +msgid "This is a new (re-randomized) version of the problem." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:145 +msgid "" +"This is an essay answer text box. You can type your answer in here and, " +"after you hit submit, it will be saved so that your instructor can grade it " +"at a later date. If your instructor makes any comments on your answer those " +"comments will appear on this page after the question has been graded. You " +"can use LaTeX to make your math equations look pretty. LaTeX expressions " +"should be enclosed using the parenthesis notation and not dollar signs." +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3064 +msgid "This problem contains a video which must be viewed online." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:631 +msgid "This problem has more than one part." +msgstr "" + +#: /opt/webwork/pg/macros/parsers/parserPopUp.pl:313 +msgid "True" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGessaymacros.pl:60 +msgid "Ungraded" +msgstr "" + +#: /opt/webwork/pg/macros/PG.pl:1290 +msgid "You Entered" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGanswermacros.pl:1616 +msgid "You can earn partial credit on this problem." +msgstr "这一题你可以得到部分成绩" + +#: /opt/webwork/pg/macros/deprecated/problemRandomize.pl:397 +msgid "You can get a new version of this problem after the due date." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:646 +msgid "You may not change your answers when going on to the next part!" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:3057 +msgid "Your browser does not support the video tag." +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:634 +msgid "Your score for this attempt is for this part only;" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:558 +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:569 +msgid "answer" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "column" +msgstr "" + +# does not need to be translated +#. ('j', 'k', '_0') +#. ('j', 'k') +#: /opt/webwork/pg/lib/Parser/List/Vector.pm:39 +#: /opt/webwork/pg/lib/Value/Vector.pm:303 +#: /opt/webwork/pg/macros/contexts/contextLimitedVector.pl:95 +msgid "i" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:840 +msgid "if" +msgstr "" + +#: /opt/webwork/pg/macros/contexts/contextPiecewiseFunction.pl:843 +msgid "otherwise" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:575 +msgid "part" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:564 +msgid "problem" +msgstr "" + +#: /opt/webwork/pg/macros/core/PGbasicmacros.pl:581 +msgid "row" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:485 +#: /opt/webwork/pg/macros/core/compoundProblem.pl:500 +msgid "when you submit your answers" +msgstr "" + +#: /opt/webwork/pg/macros/core/compoundProblem.pl:638 +msgid "your overall score is for all the parts combined." +msgstr "" diff --git a/lib/WeBWorK/PG/RestrictedClosureClass.pm b/lib/WeBWorK/PG/RestrictedClosureClass.pm index 3f0bad139a..1798bdd2fc 100644 --- a/lib/WeBWorK/PG/RestrictedClosureClass.pm +++ b/lib/WeBWorK/PG/RestrictedClosureClass.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/WeBWorK/PG/Tidy.pm b/lib/WeBWorK/PG/Tidy.pm index 073839c51a..56b42e3833 100644 --- a/lib/WeBWorK/PG/Tidy.pm +++ b/lib/WeBWorK/PG/Tidy.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/lib/WeBWorK/PG/Translator.pm b/lib/WeBWorK/PG/Translator.pm index 97c5881d0f..deb578fcbd 100644 --- a/lib/WeBWorK/PG/Translator.pm +++ b/lib/WeBWorK/PG/Translator.pm @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -15,50 +15,41 @@ package WeBWorK::PG::Translator; -use strict; -use warnings; - -use utf8; -use v5.12; -binmode(STDOUT, ":encoding(UTF-8)"); - -use Opcode; -use Carp; - -use WWSafe; -use PGUtil qw(pretty_print); -use WeBWorK::PG::IO qw(fileFromPath); - =head1 NAME WeBWorK::PG::Translator - Evaluate PG code and evaluate answers safely =head1 SYNPOSIS - my $pt = new WeBWorK::PG::Translator; # create a translator + my $pt = WeBWorK::PG::Translator->new; # create a translator $pt->environment(\%envir); # provide the environment variable for the problem - $pt->initialize(); # initialize the translator - $pt-> set_mask(); # set the operation mask for the translator safe compartment + $pt->initialize; # initialize the translator + $pt->set_mask; # set the operation mask for the translator safe compartment + $pt->source_string($source); # provide the source string for the problem + # or + $pt->source_file($sourceFilePath); # provide the proble file containing the source # Load the unprotected macro files. # These files are evaluated with the Safe compartment wide open. # Other macros are loaded from within the problem using loadMacros. - $pt->unrestricted_load("${courseScriptsDirectory}PG.pl"); + # This should not be done if the safe cache is used which is only the case if $ENV{MOJO_MODE} exists. + $pt->unrestricted_load("${pgMacrosDirectory}PG.pl"); - $pt->translate(); # translate the problem (the following pieces of information are created) + $pt->translate; # translate the problem (the following pieces of information are created) - $PG_PROBLEM_TEXT_ARRAY_REF = $pt->ra_text(); # output text for the body of the HTML file (in array form) - $PG_PROBLEM_TEXT_REF = $pt->r_text(); # output text for the body of the HTML file - $PG_HEADER_TEXT_REF = $pt->r_header; # text for the header of the HTML file - $PG_POST_HEADER_TEXT_REF = $pt->r_post_header + $PG_PROBLEM_TEXT_REF = $pt->r_text; # reference to output text for the body of problem + $PG_HEADER_TEXT_REF = $pt->r_header; # reference to text for the header in HTML output + $PG_POST_HEADER_TEXT_REF = $pt->r_post_header; $PG_ANSWER_HASH_REF = $pt->rh_correct_answers; # a hash of answer evaluators $PG_FLAGS_REF = $pt->rh_flags; # misc. status flags. - $pt->process_answers; # evaluates all of the answers + $pt->process_answers; # evaluates all of the answers + my $rh_answer_results = $pt->rh_evaluated_answers; # provides a hash of the results of evaluating the answers. + my $rh_problem_result = $pt->grade_problem(%options); # grades the problem. - my $rh_answer_results = $pt->rh_evaluated_answers; # provides a hash of the results of evaluating the answers. - my $rh_problem_result = $pt->grade_problem; # grades the problem using the default problem grading method. + $pt->post_process_content; # Execute macro or problem hooks that further modify the problem content. + $pt->stringify_answers; # Convert objects to strings in the answer hash =head1 DESCRIPTION @@ -66,14 +57,20 @@ This module defines an object which will translate a problem written in the Prob =cut -=head2 be_strict +use strict; +use warnings; -This creates a substitute for C which cannot be used in PG problem -sets or PG macro files. Use this way to imitate the behavior of C +use utf8; +use v5.12; +binmode(STDOUT, ":encoding(UTF-8)"); - BEGIN { be_strict(); } +use Opcode; +use Carp; +use Mojo::DOM; -=cut +use WWSafe; +use PGUtil qw(pretty_print); +use WeBWorK::PG::IO qw(fileFromPath); BEGIN { # Setup the safe compartment for the standalone renderer. @@ -94,7 +91,7 @@ BEGIN { # The first item is the main package. $module =~ s/\.pm$//; - eval "package Main; require $module; import $module;"; + eval "package main; require $module; import $module;"; warn "Failed to evaluate module $module: $@" if $@; push @$ra_included_modules, "\%${module}::"; @@ -137,24 +134,17 @@ BEGIN { # Stash the safe cache in a package variable. $WeBWorK::Translator::safeCache = $safeCache; } - - # Allows the use of strict within macro packages. - sub be_strict { - require ww_strict; - strict::import(); - return; - } - - # Also define in Main:: for PG modules. - sub Main::be_strict { return &be_strict; } } =head2 evaluate_modules - Usage: $obj->evaluate_modules('WWPlot', 'Fun', 'Circle'); +Adds modules to the list of modules which can be used by the PG problems. + +For example, -Adds the modules WWPlot.pm, Fun.pm and Circle.pm in the courseScripts directory to the list of modules -which can be used by the PG problems. + $obj->evaluate_modules('LaTeXImage', 'DragNDrop'); + +adds modules to the C and C modules. =cut @@ -167,7 +157,7 @@ sub evaluate_modules { # Ensure that the name is in fact a base name. s/\.pm$//; - eval "package Main; require $_; import $_"; + eval "package main; require $_; import $_"; warn "Failed to evaluate module $_: $@" if $@; # Record this in the appropriate place. @@ -179,13 +169,13 @@ sub evaluate_modules { =head2 load_extra_packages - Usage: $obj->load_extra_packages('AlgParserWithImplicitExpand', - 'Expr','ExprWithImplicitExpand'); +Loads extra packages for modules that contain more than one package. Works in +conjunction with evaluate_modules. It is assumed that the file containing the +extra packages (along with the base package name which is the same as the name +of the file minus the .pm extension) has already been loaded using +evaluate_modules. -Loads extra packages for modules that contain more than one package. Works in conjunction with -evaluate_modules. It is assumed that the file containing the extra packages (along with the base -package name which is the same as the name of the file minus the .pm extension) has already been -loaded using evaluate_modules + Usage: $obj->load_extra_packages('AlgParserWithImplicitExpand', 'ExprWithImplicitExpand'); =cut @@ -209,7 +199,7 @@ sub load_extra_packages { =head2 new - Creates the translator object. +Creates the translator object. =cut @@ -220,25 +210,24 @@ sub new { my $safe_cmpt = exists($ENV{MOJO_MODE}) ? $WeBWorK::Translator::safeCache : WWSafe->new; my $self = { - preprocess_code => \&default_preprocess_code, - postprocess_code => \&default_postprocess_code, - envir => undef, - PG_PROBLEM_TEXT_ARRAY_REF => [], - PG_PROBLEM_TEXT_REF => 0, - PG_HEADER_TEXT_REF => 0, - PG_POST_HEADER_TEXT_REF => 0, - PG_ANSWER_HASH_REF => {}, - PG_FLAGS_REF => {}, - rh_pgcore => undef, - safe => $safe_cmpt, - safe_compartment_name => $safe_cmpt->root, - errors => '', - source => '', - rh_correct_answers => {}, - rh_student_answers => {}, - rh_evaluated_answers => {}, - rh_problem_result => {}, - rh_problem_state => { + preprocess_code => \&default_preprocess_code, + postprocess_code => \&default_postprocess_code, + envir => undef, + PG_PROBLEM_TEXT_REF => 0, + PG_HEADER_TEXT_REF => 0, + PG_POST_HEADER_TEXT_REF => 0, + PG_ANSWER_HASH_REF => {}, + PG_FLAGS_REF => {}, + rh_pgcore => undef, + safe => $safe_cmpt, + safe_compartment_name => $safe_cmpt->root, + errors => '', + source => '', + rh_correct_answers => {}, + rh_student_answers => {}, + rh_evaluated_answers => {}, + rh_problem_result => {}, + rh_problem_state => { recorded_score => 0, num_of_correct_ans => 0, num_of_incorrect_ans => 0, @@ -249,40 +238,18 @@ sub new { return bless $self, $class; } -=pod - -(b) The following routines defined within the PG module are shared: - - &be_strict - &read_whole_problem_file - &convertPath - &surePathToTmpFile - &fileFromPath - &directoryFromPath - &PG_answer_eval - &PG_restricted_eval - &send_mail_to - -In addition the environment hash C<%envir> is shared. This variable is unpacked -when PG.pl is run and provides most of the environment variables for each problem -template. +=head2 initialize -=for html - environment variables +The following translator methods are shared to the safe compartment: -(c) Sharing macros: + &PG_answer_eval + &PG_restricted_eval + &PG_macro_file_eval -The macros shared with the safe compartment are +Also all methods that are exported by WeBWorK::PG::IO are shared. - '&read_whole_problem_file' - '&convertPath' - '&surePathToTmpFile' - '&fileFromPath' - '&directoryFromPath' - '&PG_answer_eval' - '&PG_restricted_eval' - '&be_strict' - '&send_mail_to' +In addition the environment hash C<%envir> is shared. This variable is unpacked +when PG.pl is run. =cut @@ -294,7 +261,6 @@ my @Translator_shared_subroutine_array = qw( &PG_answer_eval &PG_restricted_eval &PG_macro_file_eval - &be_strict ); sub initialize { @@ -302,7 +268,7 @@ sub initialize { my $safe_cmpt = $self->{safe}; $safe_cmpt->share_from('WeBWorK::PG::Translator', \@Translator_shared_subroutine_array); - $safe_cmpt->share_from('WeBWorK::PG::IO', \@WeBWorK::PG::IO::EXPORT); + $safe_cmpt->share_from('WeBWorK::PG::IO', \@WeBWorK::PG::IO::EXPORT_OK); no strict; local (%envir) = %{ $self->{envir} }; @@ -445,11 +411,6 @@ sub nameSpace { return $self->{safe}->root; } -sub a_text { - my $self = shift; - return @{ $self->{PG_PROBLEM_TEXT_ARRAY_REF} }; -} - sub header { my $self = shift; return ${ $self->{PG_HEADER_TEXT_REF} }; @@ -475,11 +436,6 @@ sub h_answers { return %{ $self->{PG_ANSWER_HASH_REF} }; } -sub ra_text { - my $self = shift; - return $self->{PG_PROBLEM_TEXT_ARRAY_REF}; -} - sub r_text { my $self = shift; return $self->{PG_PROBLEM_TEXT_REF}; @@ -526,10 +482,10 @@ sub errors { =head2 set_mask -(e) Now we close the safe compartment. Only the certain operations can be used -within PG problems and the PG macro files. These include the subroutines -shared with the safe compartment as defined above and most Perl commands which -do not involve file access, access to the system or evaluation. +Limit allowed operations in the safe compartment. Only the certain operations +can be used within PG problems and the PG macro files. These include the +subroutines shared with the safe compartment as defined above and most Perl +commands which do not involve file access, access to the system or evaluation. Specifically the following are allowed: @@ -646,7 +602,7 @@ sub PG_errorMessage { =head2 Translate -(3) B +B The input text is subjected to some global replacements. @@ -703,26 +659,23 @@ Note that there are several other replacements that are now done that are not documented here. See the C method for all replacements that are done. -(4) B +B Evaluate the text within the safe compartment. Save the errors. The safe compartment is a new one unless the $safeCompartment was set to zero in which case the previously defined safe compartment is used. (See item 1.) -(5) B +B The error provided by Perl is truncated slightly and returned. In the text string which would normally contain the rendered problem. The original text string is given line numbers and concatenated to the errors. -(6) B +B Sets the following hash keys of the translator object: - PG_PROBLEM_TEXT_ARRAY_REF: Reference to an array of strings containing the - rendered text. - PG_PROBLEM_TEXT_REF: Reference to a string resulting from joining the above - array with the empty string. + PG_PROBLEM_TEXT_REF: Reference to a string containing the rendered text. PG_HEADER_TEXT_REF: Reference to a string containing material to be placed in the header. PG_POST_HEADER_TEXT_REF: Reference to a string containing material to @@ -739,10 +692,9 @@ Sets the following hash keys of the translator object: my %XML = ('&' => '&', '<' => '<', '>' => '>', '"' => '"', '\'' => '''); sub translate { - my $self = shift; - my @PROBLEM_TEXT_OUTPUT = (); - my $safe_cmpt = $self->{safe}; - my $evalString = $self->{source}; + my $self = shift; + my $safe_cmpt = $self->{safe}; + my $evalString = $self->{source}; $self->{errors} .= qq{ERROR: This problem file was empty!\n} unless ($evalString); $self->{errors} .= qq{ERROR: You must define the environment before translating.} unless defined($self->{envir}); @@ -771,7 +723,7 @@ sub translate { # PG preprocessing code $evalString = 'BEGIN { my $eval = __FILE__; $main::envir{__files__}{$eval} = "' - . $self->{envir}{probFileName} . '" };' . "\n" + . $self->{envir}{probFileName} . '" };' . &{ $self->{preprocess_code} }($evalString); my ($PG_PROBLEM_TEXT_REF, $PG_HEADER_TEXT_REF, $PG_POST_HEADER_TEXT_REF, $PG_ANSWER_HASH_REF, $PG_FLAGS_REF, @@ -785,6 +737,7 @@ sub translate { # WARNING and DEBUG tracks are being handled elsewhere (in Problem.pm?) $self->{errors} .= "ERRORS from evaluating PG file:\n$@\n" if $@; + my @PROBLEM_TEXT_OUTPUT; push(@PROBLEM_TEXT_OUTPUT, split(/^/, $$PG_PROBLEM_TEXT_REF)) if ref($PG_PROBLEM_TEXT_REF) eq 'SCALAR'; # This is better than using defined($$PG_PROBLEM_TEXT_REF) # Because more pleasant feedback is given when the problem doesn't render. @@ -838,11 +791,10 @@ sub translate { } } - $PG_FLAGS_REF->{'error_flag'} = 1 if $self->{errors}; + $PG_FLAGS_REF->{error_flag} = 1 if $self->{errors}; my $PG_PROBLEM_TEXT = join("", @PROBLEM_TEXT_OUTPUT); - $self->{PG_PROBLEM_TEXT_REF} = \$PG_PROBLEM_TEXT; - $self->{PG_PROBLEM_TEXT_ARRAY_REF} = \@PROBLEM_TEXT_OUTPUT; + $self->{PG_PROBLEM_TEXT_REF} = \$PG_PROBLEM_TEXT; # Make sure that these variables are defined. If the eval failed with # errors, one or more of these variables won't be defined. @@ -860,7 +812,7 @@ sub translate { =cut -=head3 access methods +=head3 access methods $obj->rh_student_answers @@ -917,38 +869,21 @@ sub process_answers { ? @{ $self->{PG_FLAGS_REF}->{ANSWER_ENTRY_ORDER} } : keys %{$rh_correct_answers}; - # Define custom warn/die handlers for answer evaluation. These used to be inside the for loop around the conditional - # involving $new_rf_fun, but for efficiency we've moved it out here. This means that the handlers will be active - # during the code before and after the actual answer evaluation. - + # Define custom warn/die handlers for answer evaluation. my $outer_sig_warn = $SIG{__WARN__}; local $SIG{__WARN__} = sub { - ref $outer_sig_warn eq "CODE" + ref $outer_sig_warn eq 'CODE' ? &$outer_sig_warn(PG_errorMessage('message', $_[0])) : warn PG_errorMessage('message', $_[0]); }; - # The die handler is a closure over %errorTable and $outer_sig_die. - # - # %errorTable accumulates a "full" error message for each error that occurs during answer evaluation. then, right - # after the evaluation (which is done within a call to Safe::reval), $@ is checked and it's value is looked up in - # %errorTable to get the full error to report. - # - # my question: Why is this a hash? This is die, so once one occurs, we exit the reval. - # Wouldn't it be sufficient to have a scalar like $backtrace_for_last_error? - # - # Note that %errorTable is cleared for each answer. - my %errorTable; + my $fullerror; my $outer_sig_die = $SIG{__DIE__}; local $SIG{__DIE__} = sub { - my $fullerror = PG_errorMessage('traceback', @_); - my ($error, $traceback) = split /\n/, $fullerror, 2; - $fullerror =~ s/\n /   /g; - $fullerror =~ s/\n//g; + $fullerror = PG_errorMessage('traceback', @_); + my ($error) = split /\n/, $fullerror, 2; $error .= "\n"; - $errorTable{$error} = $fullerror; - - ref $outer_sig_die eq "CODE" ? &$outer_sig_die($error) : die $error; + ref $outer_sig_die eq 'CODE' ? &$outer_sig_die($error) : die $error; }; my $PG = $self->{rh_pgcore}; @@ -958,7 +893,7 @@ sub process_answers { $PG->debug_message("Executing answer evaluator $ans_name ") if $local_debug; # gather answers and answer evaluator - local ($new_rf_fun, $new_temp_ans) = (undef, undef); + local ($new_rf_fun, $new_temp_ans); # This has all answer evaluators AND answer blanks (just to be sure). my $answergrp = $PG->{PG_ANSWERS_HASH}->{$ans_name}; my $responsegrp = $answergrp->response_obj; @@ -993,24 +928,18 @@ sub process_answers { $self->{safe}->share('$new_rf_fun', '$new_temp_ans'); - # Clear %errorTable for each problem - %errorTable = (); # Is the error table being used? Perhaps by math objects? - my ($rh_ans_evaluation_result, $new_rh_ans_evaluation_result); if (ref($new_rf_fun) eq 'CODE') { $PG->warning_message('CODE objects cannot be used directly as answer evaluators. Use AnswerEvaluator'); } elsif (!$skip_evaluation) { - # Get full traceback, but save it in local variable $errorTable so that we can add it later. This is - # because some evaluators use eval to trap errors and then report them in the message column of the results - # table, and we don't want to include the traceback there. - $new_rh_ans_evaluation_result = $self->{safe}->reval('$new_rf_fun->evaluate($new_temp_ans, ans_label => \'' . $ans_name . '\')'); - $@ = $errorTable{$@} if $@ && defined $errorTable{$@}; # Are we redefining error messages here? - # The following needs more work for the new rh_ans_evaluation - if (ref($new_rh_ans_evaluation_result) =~ /AnswerHash/i) { + if ($@) { + $PG->warning_message($@); + $PG->debug_message(split /\n/, $fullerror) if $fullerror && $self->{envir}{view_problem_debugging_info}; + } elsif (ref($new_rh_ans_evaluation_result) =~ /AnswerHash/i) { $PG->warning_message( "Evaluation error in new process: Answer $ans_name:
                      \n", $new_rh_ans_evaluation_result->error_flag(), @@ -1020,7 +949,7 @@ sub process_answers { && ref $new_rh_ans_evaluation_result && defined $new_rh_ans_evaluation_result->error_flag(); } else { - $PG->warning_message(' The evaluated answer is not an answer hash ' + $PG->warning_message('The evaluated answer is not an answer hash ' . ($new_rh_ans_evaluation_result // '') . ': |' . ref($new_rh_ans_evaluation_result) . '|.'); @@ -1087,7 +1016,6 @@ sub grade_problem { use strict; die $@ if $@; - $self->stringify_answers; return ($self->{rh_problem_result}, $self->{rh_problem_state}); } @@ -1202,6 +1130,68 @@ sub avg_problem_grader { return (\%problem_result, \%problem_state); } +=head2 post_process_content + +Call hooks added via macros or the problem via C to +post process content. Hooks are called in the order they were added. + +This method should be called in the rendering process after answer processing +has occurred. + +If the display mode is TeX, then each hook subroutine is passed a reference to +the problem text string generated in the C method. + +For all other display modes each hook subroutine is passed two Mojo::DOM +objects. The first containing the parsed problem text string, and the second +contains the parsed header text string, both of which were generated in the +C method. After all hooks are called and modifications are made to +the Mojo::DOM contents by the hooks, the Mojo::DOM objects are converted back to +strings and the translator problem text and header references are updated with +the contents of those strings. + +=cut + +sub post_process_content { + my $self = shift; + + my $outer_sig_warn = $SIG{__WARN__}; + my @warnings; + local $SIG{__WARN__} = sub { push(@warnings, $_[0]) }; + + my $outer_sig_die = $SIG{__DIE__}; + local $SIG{__DIE__} = sub { + ref $outer_sig_die eq "CODE" + ? $outer_sig_die->(PG_errorMessage('traceback', $_[0])) + : die PG_errorMessage('traceback', $_[0]); + }; + + if ($self->{rh_pgcore}{displayMode} eq 'TeX') { + our $PG_PROBLEM_TEXT_REF = $self->{PG_PROBLEM_TEXT_REF}; + $self->{safe}->share('$PG_PROBLEM_TEXT_REF'); + $self->{safe}->reval('for (@{ $main::PG->{content_post_processors} }) { $_->($PG_PROBLEM_TEXT_REF); }', 1); + warn "ERRORS from post processing PG text:\n$@\n" if $@; + } else { + $self->{safe}->share_from('main', [qw(%Mojo::Base:: %Mojo::Collection:: %Mojo::DOM::)]); + our $problemDOM = + Mojo::DOM->new->xml($self->{rh_pgcore}{displayMode} eq 'PTX')->parse(${ $self->{PG_PROBLEM_TEXT_REF} }); + our $pageHeader = Mojo::DOM->new(${ $self->{PG_HEADER_TEXT_REF} }); + $self->{safe}->share('$problemDOM', '$pageHeader'); + $self->{safe}->reval('for (@{ $main::PG->{content_post_processors} }) { $_->($problemDOM, $pageHeader); }', 1); + warn "ERRORS from post processing PG text:\n$@\n" if $@; + + $self->{PG_PROBLEM_TEXT_REF} = \($problemDOM->to_string); + $self->{PG_HEADER_TEXT_REF} = \($pageHeader->to_string); + } + + if (@warnings) { + ref $outer_sig_warn eq "CODE" + ? $outer_sig_warn->(PG_errorMessage('message', @warnings)) + : warn PG_errorMessage('message', @warnings); + } + + return; +} + =head2 PG_restricted_eval PG_restricted_eval($string) @@ -1230,7 +1220,7 @@ sub PG_restricted_eval { my $out = PG_restricted_eval_helper($string); my $err = $@; - my $err_report = $err if $err =~ /\S/; + my $err_report = $err =~ /\S/ ? $err : undef; return wantarray ? ($out, $err, $err_report) : $out; } @@ -1243,7 +1233,7 @@ sub PG_restricted_eval_helper { # Many macros redefine methods using PG_restricted_eval. This hides those warnings. no warnings 'redefine'; - return eval("package main;\n$code"); + return eval("package main; $code"); } sub PG_macro_file_eval { @@ -1255,10 +1245,17 @@ sub PG_macro_file_eval { local $SIG{__DIE__} = 'DEFAULT'; + if ($string =~ /^=/) { + $string = "\n$string"; + warn "The first line of a macro must not contain a POD directive at $filePath line 1.\n" + . "A new line will be added, but this will result in errors and warnings from " + . "this file being reported on the incorrect line number.\n"; + } + my ($out, $errors) = - PG_macro_file_eval_helper('package main; be_strict();' + PG_macro_file_eval_helper('package main; strict->import;' . 'BEGIN { my $eval = __FILE__; $main::envir{__files__}{$eval} = "' - . $filePath . '" };' . "\n" + . $filePath . '" };' . $string); if ($warnings) { @@ -1278,7 +1275,6 @@ sub PG_macro_file_eval { # This is another helper that doesn't use any lexicals. # It would nice to be able to remove the "no strict" call so "use strict" applies to the files that it evaluates. -# Note that the "be_strict" method is not the same. sub PG_macro_file_eval_helper { my $string = shift; diff --git a/lib/ww_strict.pm b/lib/ww_strict.pm deleted file mode 100644 index 72cd15ed88..0000000000 --- a/lib/ww_strict.pm +++ /dev/null @@ -1,123 +0,0 @@ -package ww_strict; - -$strict::VERSION = "1.03"; - -=head1 NAME - -strict - Perl pragma to restrict unsafe constructs - -=head1 SYNOPSIS - - use strict; - - use strict "vars"; - use strict "refs"; - use strict "subs"; - - use strict; - no strict "vars"; - -=head1 DESCRIPTION - -If no import list is supplied, all possible restrictions are assumed. -(This is the safest mode to operate in, but is sometimes too strict for -casual programming.) Currently, there are three possible things to be -strict about: "subs", "vars", and "refs". - -=over 6 - -=item C - -This generates a runtime error if you -use symbolic references (see L). - - use strict 'refs'; - $ref = \$foo; - print $$ref; # ok - $ref = "foo"; - print $$ref; # runtime error; normally ok - $file = "STDOUT"; - print $file "Hi!"; # error; note: no comma after $file - -=item C - -This generates a compile-time error if you access a variable that wasn't -declared via "our" or C, -localized via C, or wasn't fully qualified. Because this is to avoid -variable suicide problems and subtle dynamic scoping issues, a merely -local() variable isn't good enough. See L and -L. - - use strict 'vars'; - $X::foo = 1; # ok, fully qualified - my $foo = 10; # ok, my() var - local $foo = 9; # blows up - - package Cinna; - our $bar; # Declares $bar in current package - $bar = 'HgS'; # ok, global declared via pragma - -The local() generated a compile-time error because you just touched a global -name without fully qualifying it. - -Because of their special use by sort(), the variables $a and $b are -exempted from this check. - -=item C - -This disables the poetry optimization, generating a compile-time error if -you try to use a bareword identifier that's not a subroutine, unless it -appears in curly braces or on the left hand side of the "=E" symbol. - - - use strict 'subs'; - $SIG{PIPE} = Plumber; # blows up - $SIG{PIPE} = "Plumber"; # just fine: bareword in curlies always ok - $SIG{PIPE} = \&Plumber; # preferred form - - - -=back - -See L. - - -=cut - -$strict::VERSION = "1.01"; - -my %bitmask = ( - refs => 0x00000002, - subs => 0x00000200, - vars => 0x00000400 -); - -sub bits { - my $bits = 0; - my @wrong; - foreach my $s (@_) { - push @wrong, $s unless exists $bitmask{$s}; - $bits |= $bitmask{$s} || 0; - } - if (@wrong) { - #require Carp; - Carp::croak("Unknown 'strict' tag(s) '@wrong'"); - } - $bits; -} - -my $default_bits = bits(qw(refs subs vars)); - -sub import { - shift; - $^H |= @_ ? bits(@_) : $default_bits; -} - -sub unimport { - shift; - $^H &= ~bits(@_ ? @_ : qw(refs subs vars)); -} - -1; -__END__ - diff --git a/macros/PG.pl b/macros/PG.pl index 2223f9a9d5..e3f4a80246 100644 --- a/macros/PG.pl +++ b/macros/PG.pl @@ -1,7 +1,88 @@ -#use AnswerEvaluator; +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2024 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 + +PG.pl - Provides core Program Generation Language functionality. + +=head1 SYNPOSIS + +In a PG problem: + + DOCUMENT(); # should be the first statment in the problem + + loadMacros(.....); # (optional) load other macro files if needed. + + HEADER_TEXT(...); # (optional) used only for inserting javaScript into problems. + + TEXT( # insert text of problems + "Problem text to be displayed. ", + "Enter 1 in this blank:", + ans_rule(30) # ans_rule(30) defines an answer blank 30 characters long. + # It is defined in PGbasicmacros.pl. + ); + + ANS(answer_evalutors); # see PGanswermacros.pl for examples of answer evaluatiors. + + ENDDOCUMENT() # must be the last statement in the problem + +=head1 DESCRIPTION + +This file provides the fundamental macros that define the PG language. It +maintains a problem's text, header text, and answers: -# provided by the translator -# initialize PGcore and PGrandom +=over + +=item * + +Problem text: The text to appear in the body of the problem. See L +below. + +=item * + +Header text: When a problem is processed in an HTML-based display mode, this +variable can contain text that the caller should place in the HEAD of the +resulting HTML page. See L below. + +=item * + +Implicitly labeled answers: Answers that have not been explicitly assigned +names, and are associated with their answer blanks by the order in which they +appear in the problem. These types of answers are designated using the L +macro. + +=item * + +Explicitly labeled answers: Answers that have been explicitly assigned names +with the L macro, or a macro that uses it. An explicitly labeled +answer is associated with its answer blank by name. + +=item * + +"Extra" answers: Names of answer blanks that do not have a 1-to-1 correspondence +to an answer evaluator. For example, in matrix problems, there will be several +input fields that correspond to the same answer evaluator. + +=back + +=head1 MACROS + +This file is automatically loaded into the namespace of every PG problem. The +macros within can then be called to define the structure of the problem. + +=cut sub _PG_init { $main::VERSION = "PG-2.15"; @@ -18,7 +99,10 @@ sub _PG_init { sub not_null { $PG->not_null(@_) } -sub pretty_print { $PG->pretty_print(shift, $main::displayMode) } +sub pretty_print { + my ($input, $display_mode, $print_level) = @_; + $PG->pretty_print($input, $display_mode // $main::displayMode, $print_level // 5); +} sub encode_pg_and_html { PGcore::encode_pg_and_html(@_) } @@ -32,8 +116,14 @@ sub WARN_MESSAGE { $PG->warning_message("---- " . join(" ", caller()) . " ------", @msg, "__________________________"); } -sub DOCUMENT { +=head2 DOCUMENT +C should be the first executable statement in any problem. It +initializes variables and defines the problem environment. + +=cut + +sub DOCUMENT { # get environment $rh_envir = \%envir; #KLUDGE FIXME # warn "rh_envir is ",ref($rh_envir); @@ -80,30 +170,110 @@ sub DOCUMENT { $main::displayMode = $PG->{displayMode}; $main::PG = $PG; +=head2 TEXT + +C concatenates its arguments and appends them to the stored problem text +string. It is used to define the text which will appear in the body of the +problem. It can be used more than once in a file. For example, + + TEXT("string1", "string2", "string3"); + +This macro has no effect if rendering has been stopped with the +C macro. + +This macro defines text which will appear in the problem. All text must be +passed to this macro, passed to another macro that calls this macro, or included +via a BEGIN_TEXT/END_TEXT or BEGIN_PGML/END_PGML block which uses this macro +internally. No other statements in a PG file will directly appear in the output. +Think of this as the "print" function for the PG language. + +Spaces are placed between the arguments during concatenation, but no spaces are +introduced between the existing content of the header text string and the new +content being appended. + +=cut + sub TEXT { $PG->TEXT(@_); } +=head2 HEADER_TEXT + +C concatenates its arguments and appends them to the stored +header text string. It can be used more than once in a file. For example, + + HEADER_TEXT("string1", "string2", "string3"); + +The macro is used for material which is destined to be placed in the HEAD of +the page when in HTML mode, such as JavaScript code. + +Spaces are placed between the arguments during concatenation, but no spaces are +introduced between the existing content of the header text string and the new +content being appended. + +=cut + sub HEADER_TEXT { $PG->HEADER_TEXT(@_); } +=head2 POST_HEADER_TEXT + +DEPRECATED + +Content added by this method is appended just after the page head. This method +should no longer be used. There is no valid reason to add content after the +page head, and not in the problem itself. + +=cut + sub POST_HEADER_TEXT { $PG->POST_HEADER_TEXT(@_); } -# We expect valid HTML language codes, but there can also include a region code, or other -# settings. -# See https://www.w3.org/International/questions/qa-choosing-language-tags -# Example settings: en-US, en-UK, he-IL -# Some special language codes (zh-Hans) are longer -# http://www.rfc-editor.org/rfc/bcp/bcp47.txt -# https://www.w3.org/International/articles/language-tags/ -# https://www.w3.org/International/questions/qa-lang-2or3.en.html -# http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry -# https://www.w3schools.com/tags/ref_language_codes.asp -# https://www.w3schools.com/tags/ref_country_codes.asp -# Tester at https://r12a.github.io/app-subtags/ +=head2 SET_PROBLEM_LANGUAGE + +Valid HTML language codes are expected, but a region code or other settings may +be included. See L. + + SET_PROBLEM_LANGUAGE($language) + +Example language codes: en-US, en-UK, he-IL + +Some special language codes (e.g. zh-Hans) are longer. See the following +references. + +=over + +=item * + +L + +=item * + +L + +=item * + +L + +=item * + +L + +=item * + +L + +=item * + +L + +=back + +There is a tester located at L + +=cut sub SET_PROBLEM_LANGUAGE { my $requested_lang = shift; @@ -118,18 +288,25 @@ sub SET_PROBLEM_LANGUAGE { $PG->{flags}->{"language"} = $selected_lang; } -# SET_PROBLEM_TEXTDIRECTION to set the HTML DIRection attribute to be applied -# to the DIV element containing this problem. +=head2 SET_PROBLEM_TEXTDIRECTION + +Call C to set the HTML C attribute to be applied +to the C
                      element containing the problem. + + SET_PROBLEM_TEXTDIRECTION($dir) + +Only valid settings for the HTML C attribute are permitted. -# We only permit valid settings for the HTML direction attribute: -# dir="ltr|rtl|auto" -# https://www.w3schools.com/tags/att_global_dir.asp + dir="ltr|rtl|auto" -# It is likely that only problems written in RTL scripts -# will need to call the following function to set the base text direction -# for the problem. +See L. -# Note the flag may not be set, and then webwork2 will use default behavior. +It is likely that only problems written in RTL scripts will need to call the +following function to set the base text direction for the problem. + +Note the flag may not be set, and then the default behavior will be used. + +=cut sub SET_PROBLEM_TEXTDIRECTION { my $requested_dir = shift; @@ -147,15 +324,16 @@ sub SET_PROBLEM_TEXTDIRECTION { } } -=head4 ADD_CSS_FILE +=head2 ADD_CSS_FILE -Request that the problem HTML page also include additional CSS files -from the webwork2/htdocs/ directory or from an external location. +Request that the problem HTML page also include additional CSS files from the +C directory or from an external location. ADD_CSS_FILE($file, $external); -If external is 1, it is assumed the full url is provided. If external is 0 or -not given, then file name will be prefixed with the webwork2/htdocs/ directory. +If external is 1, it is assumed the full URL is provided. If external is 0 or +not given, then file will be served from the C directory (if found). + For example: ADD_CSS_FILE("css/rtl.css"); @@ -185,23 +363,24 @@ () } } -=head4 ADD_JS_FILE +=head2 ADD_JS_FILE -Request that the problem HTML page also include additional JS files -from the webwork2/htdocs/ directory or from an external location. +Request that the problem HTML page also include additional JavaScript files from +the C directory or from an external location. ADD_JS_FILE($file, $external); -If external is 1, it is assumed the full url is provided. If external is 0 or -not given, then file name will be prefixed with the webwork2/htdocs/ directory. +If external is 1, it is assumed the full URL is provided. If external is 0 or +not given, then file name will be served from the C directory (if +found). Additional attributes can be passed as a hash reference in the optional third -argument. These attributes will be added as attributes to the script tag. +argument. These attributes will be added as attributes to the script tag. For example: ADD_JS_FILE("js/Base64/Base64.js"); - ADD_JS_FILE("//web.geogebra.org/4.4/web/web.nocache.js", 1); + ADD_JS_FILE("https://cdn.geogebra.org/apps/deployggb.js", 1); ADD_JS_FILE("js/GraphTool/graphtool.js", 0, { id => "gt_script", defer => undef }); =cut @@ -216,11 +395,13 @@ sub ADD_JS_FILE { # Some problems use jquery-ui still, and so the requestor should also load the js for that if those problems are used, # although those problems should also be rewritten to not use jquery-ui. sub load_js() { - ADD_JS_FILE('js/InputColor/color.js', 0, { defer => undef }); - ADD_JS_FILE('js/Base64/Base64.js', 0, { defer => undef }); - ADD_JS_FILE('js/Knowls/knowl.js', 0, { defer => undef }); - ADD_JS_FILE('js/ImageView/imageview.js', 0, { defer => undef }); - ADD_JS_FILE('js/Essay/essay.js', 0, { defer => undef }); + + ADD_JS_FILE('js/Feedback/feedback.js', 0, { defer => undef }); + ADD_JS_FILE('js/Base64/Base64.js', 0, { defer => undef }); + ADD_JS_FILE('js/Knowls/knowl.js', 0, { defer => undef }); + ADD_JS_FILE('js/Problem/details-accordion.js', 0, { defer => undef }); + ADD_JS_FILE('js/ImageView/imageview.js', 0, { defer => undef }); + ADD_JS_FILE('js/Essay/essay.js', 0, { defer => undef }); if ($envir{useMathQuill}) { ADD_JS_FILE('node_modules/mathquill/dist/mathquill.js', 0, { defer => undef }); @@ -247,64 +428,121 @@ sub sageReturnedFail { return (not defined($obj) or (defined($obj->{success}) and $obj->{success} == 0)); } -sub LABELED_ANS { - my @in = @_; - my @out = (); - while (@in) { - my $label = shift @in; - $ans_eval = shift @in; - push @out, $label, $ans_eval; - } - $PG->LABELED_ANS(@out); # returns pointer to the labeled answer group -} +=head2 NAMED_ANS + +Associates answer names with answer evaluators. If the given anwer name has a +response group in the PG_ANSWERS_HASH, then the evaluator is added to that +response group. Otherwise the name and evaluator are added to the hash of +explicitly named answer evaluators. They will be paired with exlplicitly +named answer rules by name. This allows pairing of answer evaluators and +answer rules that may not have been entered in the same order. + +An example of the usage is: + + TEXT(NAMED_ANS_RULE("name1"), NAMED_ANS_RULE("name2")); + NAMED_ANS(name1 => answer_evaluator1, name2 => answer_evaluator2); + +=cut sub NAMED_ANS { - &LABELED_ANS(@_); # returns pointer to the labeled answer group + my @in = @_; + $PG->NAMED_ANS(@in); +} + +=head2 LABELED_ANS + +Alias for NAMED_ANS + +=cut + +sub LABELED_ANS { + my @in = @_; + NAMED_ANS(@in); } +=head2 ANS + +Registers answer evaluators to be implicitly associated with answer names. If +there is an answer name in the implicit answer name stack, then a given answer +evaluator will be paired with the first name in the stack. Otherwise the +evaluator will be pushed onto the implicit answer evaluator stack. This is the +standard method for entering answers. + +An example of the usage is: + + TEXT(ans_rule(), ans_rule(), ans_rule()); + ANS($answer_evaluator1, $answer_evaluator2, $answer_evaluator3); + +In the above example, C will be associated with the first +answer rule, C with the second, and C with +the third. In practice, the arguments to C will usually be calls to an +answer evaluator generator such as the C method of MathObjects or the +C macro in L. + +=cut + sub ANS { - #warn "using PGnew for ANS"; - $PG->ANS(@_); # returns pointer to the labeled answer group + $PG->ANS(@_); } +=head2 RECORD_ANS_NAME + +Records the name for an answer blank. Used internally by L to +record the order of answer blanks. All answer blanks must eventually be +recorded via this method. + + RECORD_ANS_NAME('name', 'VALUE'); + +=cut + sub RECORD_ANS_NAME { - $PG->record_ans_name(@_); + my ($name, $value) = @_; + return $PG->record_ans_name($name, $value); } -sub inc_ans_rule_count { - #$PG->{unlabeled_answer_blank_count}++; - #my $num = $PG->{unlabeled_answer_blank_count}; - DEBUG_MESSAGE(" No increment done. Using PG to inc_ans_rule_count = $num ", caller(2)); - warn " using PG to inc_ans_rule_count = $num ", caller(2); - $PG->{unlabeled_answer_blank_count}; +=head2 RECORD_IMPLICIT_ANS_NAME + +Records the name for an answer blank that is implicitly named. Used +internally by L to record the order of answer blanks that are +implicitly nameed. This must also be called by a macro for answer blanks +created by it that need to be implicitly named. + + RECORD_IMPLICIT_ANS_NAME('name'); + +=cut + +sub RECORD_IMPLICIT_ANS_NAME { + my ($name) = @_; + return $PG->record_implicit_ans_name($name); } sub ans_rule_count { - $PG->{unlabeled_answer_blank_count}; + scalar keys %{ $PG->{PG_ANSWERS_HASH} }; } +=head2 NEW_ANS_NAME + +Generates an anonymous answer name from the internal count. This method takes +no arguments. + +=cut + sub NEW_ANS_NAME { - return "" if $PG_STOP_FLAG; - #my $number=shift; - # we have an internal count so the number not actually used. - my $name = $PG->record_unlabeled_ans_name(); - $name; + return $PG->new_ans_name; } -sub NEW_ARRAY_NAME { - return "" if $PG_STOP_FLAG; - my $name = $PG->record_unlabeled_array_name(); - $name; -} +=head2 ANS_NUM_TO_NAME -# new subroutine -sub NEW_ANS_BLANK { - return "" if $PG_STOP_FLAG; - $PG->record_unlabeled_ans_name(@_); -} +Generates an answer name from the supplied answer number, but does not add it +to the list of implicitly-named answers. This is deprecated, and most likely +will not give something useful. + + ANS_NUM_TO_NAME($num); + +=cut sub ANS_NUM_TO_NAME { - $PG->new_label(@_); # behaves as in PG.pl + $PG->new_label(@_); } sub store_persistent_data { @@ -322,325 +560,968 @@ sub get_persistent_data { return $PG->get_persistent_data($label); } -sub RECORD_FORM_LABEL { # this stores form data (such as sticky answers), but does nothing more - # it's a bit of hack since we are storing these in the - # KEPT_EXTRA_ANSWERS queue even if they aren't answers per se. - #FIXME - # warn "Using RECORD_FORM_LABEL -- deprecated? use $PG->store_persistent_data instead."; - RECORD_EXTRA_ANSWERS(@_); +sub add_content_post_processor { + my $handler = shift; + $PG->add_content_post_processor($handler); + return; } -sub RECORD_EXTRA_ANSWERS { - return "" if $PG_STOP_FLAG; - my $label = shift; # the label of the input box or textarea - eval(q!push(@main::KEPT_EXTRA_ANSWERS, $label)!) - ; #put the labels into the hash to be caught later for recording purposes - $label; +=head2 RECORD_FORM_LABEL -} +Stores the name of a form field in the "extra" answers list. This is used to +keep track of answer blanks that are not associated with an answer evaluator. + + RECORD_FORM_LABEL("name"); + +=cut -sub NEW_ANS_ARRAY_NAME { # this keeps track of the answers within an array which are entered implicitly, - # rather than with a specific label - return "" if $PG_STOP_FLAG; - my $number = shift; - $main::vecnum = -1; - my $row = shift; - my $col = shift; - # my $array_ans_eval_label = "ArRaY"."$number"."__"."$vecnum".":"; - my $label = - $PG->{QUIZ_PREFIX} . $PG->{ARRAY_PREFIX} . "$number" . "__" . "$vecnum" . "-" . "$row" . "-" . "$col" . "__"; - # my $response_group = new PGresponsegroup($label,undef); - # $PG->record_ans_name($array_ans_eval_label, $response_group); - # What does vecnum do? - # The name is simply so that it won't conflict when placed on the HTML page - # my $array_label = shift; - $PG->record_array_name($label); # returns $array_label, $ans_label +# This stores form data (such as sticky answers), but does nothing more. +# It's a bit of hack since we are storing these in the +# KEPT_EXTRA_ANSWERS queue even if they aren't answers per se. +sub RECORD_FORM_LABEL { + RECORD_EXTRA_ANSWERS(@_); } -sub NEW_ANS_ARRAY_NAME_EXTENSION { - NEW_ANS_ARRAY_ELEMENT_NAME(@_); +sub RECORD_EXTRA_ANSWERS { + my $label = shift; + # Put the labels into the hash to be caught later for recording purposes. + eval(q!push(@main::KEPT_EXTRA_ANSWERS, $label)!); + return $label; } -sub NEW_ANS_ARRAY_ELEMENT_NAME { # creates a new array element answer name and records it +=head2 NEW_ANS_ARRAY_NAME_EXTENSION - return "" if $PG_STOP_FLAG; - my $number = shift; +Generate an additional answer name for an existing array (vector) element and +add it to the list of "extra" answers. + + NEW_ANS_ARRAY_NAME_EXTENSION($row, $col); + +=cut + +# Creates a new array element answer name and records it. +sub NEW_ANS_ARRAY_NAME_EXTENSION { my $row_num = shift; my $col_num = shift; if ($row_num == 0 && $col_num == 0) { $main::vecnum += 1; } - # my $ans_label = "ArRaY".sprintf("%04u", $number); - my $ans_label = $PG->new_array_label($number); + my $ans_label = $PG->new_ans_name(); my $element_ans_label = $PG->new_array_element_label($ans_label, $row_num, $col_num, vec_num => $vecnum); my $response = new PGresponsegroup($ans_label, $element_ans_label, undef); $PG->extend_ans_group($ans_label, $response); - $element_ans_label; -} - -sub NEW_LABELED_ANS_ARRAY { #not in PG_original - my $ans_label = shift; - my @response_list = @_; - #$PG->extend_ans_group($ans_label,@response_list); - $PG->{PG_ANSWERS_HASH}->{$ans_label}->insert_responses(@response_list); - # should this return an array of labeled answer blanks??? -} - -sub EXTEND_ANS_ARRAY { #not in PG_original - my $ans_label = shift; - my @response_list = @_; - #$PG->extend_ans_group($ans_label,@response_list); - $PG->{PG_ANSWERS_HASH}->{$ans_label}->append_responses(@response_list); + return $element_ans_label; } sub CLEAR_RESPONSES { my $ans_label = shift; - # my $response_label = shift; - # my $ans_value = shift; - if (defined($PG->{PG_ANSWERS_HASH}->{$ans_label})) { - my $responsegroup = $PG->{PG_ANSWERS_HASH}->{$ans_label}->{response}; + if (defined $PG->{PG_ANSWERS_HASH}{$ans_label}) { + my $responsegroup = $PG->{PG_ANSWERS_HASH}{$ans_label}{response}; if (ref($responsegroup)) { $responsegroup->clear; } else { - $responsegroup = $PG->{PG_ANSWERS_HASH}->{$ans_label}->{response} = new PGresponsegroup($label); + $responsegroup = $PG->{PG_ANSWERS_HASH}{$ans_label}{response} = new PGresponsegroup($label); } } - ''; + return; } #FIXME -- examine the difference between insert_response and extend_response sub INSERT_RESPONSE { - my $ans_label = shift; - my $response_label = shift; - my $ans_value = shift; - my $selected = shift; - # warn "\n\nin PG.pl\nanslabel $ans_label responselabel $response_label value $ans_value"; - if (defined($PG->{PG_ANSWERS_HASH}->{$ans_label})) { - my $responsegroup = $PG->{PG_ANSWERS_HASH}->{$ans_label}->{response}; + my ($ans_label, $response_label, $ans_value, $selected) = @_; + if (defined($PG->{PG_ANSWERS_HASH}{$ans_label})) { + my $responsegroup = $PG->{PG_ANSWERS_HASH}{$ans_label}{response}; $responsegroup->append_response($response_label, $ans_value, $selected); - # warn "There are ", scalar($responsegroup->responses), " $responsegroup responses." ; } - ''; + return; } -sub EXTEND_RESPONSE { # for radio buttons and checkboxes - my $ans_label = shift; - my $response_label = shift; - my $ans_value = shift; - my $selected = shift; - # warn "\n\nin PG.pl \nanslabel $ans_label responselabel $response_label value $ans_value"; +# For radio buttons and checkboxes. +sub EXTEND_RESPONSE { + my ($ans_label, $response_label, $ans_value, $selected) = @_; if (defined($PG->{PG_ANSWERS_HASH}->{$ans_label})) { my $responsegroup = $PG->{PG_ANSWERS_HASH}->{$ans_label}->{response}; $responsegroup->extend_response($response_label, $ans_value, $selected); - # warn "\n$responsegroup responses are now ", pretty_print($response_group); } - ''; + return; } -sub ENDDOCUMENT { - # Insert MathQuill responses if MathQuill is enabled. Add responses to each answer's response group that store the - # latex form of the students' answers and add corresponding hidden input boxes to the page. - if ($envir{useMathQuill}) { - for my $answerLabel (keys %{ $PG->{PG_ANSWERS_HASH} }) { - my $answerGroup = $PG->{PG_ANSWERS_HASH}{$answerLabel}; - my $mq_opts = $answerGroup->{ans_eval}{rh_ans}{mathQuillOpts} // {}; +=head2 ENDDOCUMENT - # This is a special case for multi answers. This is used to obtain mathQuillOpts set - # specifically for individual parts. - my $multiAns; - my $part; - if ($answerGroup->{ans_eval}{rh_ans}{type} =~ /MultiAnswer(?:\((\d*)\))?/) { - # This will only be set if singleResult is not enabled. - $part = $1; - # The MultiAnswer object passes itself as the first optional argument to the evaluator it creates. - # Loop through the evaluators to find it. - for (@{ $answerGroup->{ans_eval}{evaluators} }) { - $multiAns = $_->[1] if (ref($_->[1]) && ref($_->[1]) eq "parser::MultiAnswer"); - } - # Pass the mathQuillOpts of the main MultiAnswer object on to each part - # (unless the part already has the option set). - if (defined $multiAns) { - for (@{ $multiAns->{cmp} }) { - $_->rh_ans(mathQuillOpts => $mq_opts) unless defined $_->{rh_ans}{mathQuillOpts}; - } - } - } +When PG problems are evaluated, the result of evaluating the entire problem is +interpreted as the return value of C. Furthermore, a post +processing hook is added that injects feedback into the problem text. +Therefore, C must be the last executable statement of every +problem. It can only appear once. It returns a list consisting of: - next if $mq_opts =~ /^\s*disabled\s*$/i; +=over - my $response_obj = $answerGroup->response_obj; - my $responseCount = -1; - for my $response ($response_obj->response_labels) { - ++$responseCount; - next if ref($response_obj->{responses}{$response}); +=item * - my $ansHash = - defined $multiAns - ? $multiAns->{cmp}[ $part // $responseCount ]{rh_ans} - : $answerGroup->{ans_eval}{rh_ans}; - my $mq_part_opts = $ansHash->{mathQuillOpts} // $mq_opts; - next if $mq_part_opts =~ /^\s*disabled\s*$/i; +A reference to a string containing the rendered text of the problem. - my $context = $ansHash->{correct_value}->context if $ansHash->{correct_value}; - $mq_part_opts->{rootsAreExponents} = 0 - if $context && $context->functions->get('root') && !defined $mq_part_opts->{rootsAreExponents}; +=item * - my $name = "MaThQuIlL_$response"; - my $answer_value = ''; - $answer_value = $inputs_ref->{$name} if defined($inputs_ref->{$name}); - RECORD_EXTRA_ANSWERS($name); - $answer_value = encode_pg_and_html($answer_value); - my $data_mq_opts = - scalar(keys %$mq_part_opts) - ? qq!data-mq-opts="@{[encode_pg_and_html(JSON->new->encode($mq_part_opts))]}"! - : ""; - TEXT(MODES( - TeX => "", - PTX => "", - HTML => qq!! - )); - } - } - } +A reference to a string containing text to be placed in the HEAD block +when in and HTML-based mode (e.g. for JavaScript). - # check that answers match - # gather up PG_FLAGS elements +=item * - $PG->{flags}->{showPartialCorrectAnswers} = defined($showPartialCorrectAnswers) ? $showPartialCorrectAnswers : 1; - $PG->{flags}->{recordSubmittedAnswers} = defined($recordSubmittedAnswers) ? $recordSubmittedAnswers : 1; - $PG->{flags}->{refreshCachedImages} = defined($refreshCachedImages) ? $refreshCachedImages : 0; - $PG->{flags}->{hintExists} = defined($hintExists) ? $hintExists : 0; - $PG->{flags}->{solutionExists} = defined($solutionExists) ? $solutionExists : 0; - $PG->{flags}->{comment} = defined($pgComment) ? $pgComment : ''; +A reference to a string containing text to be placed immediately after the HEAD +block when in and HTML-based mode. - # install problem grader - if (defined($PG->{flags}->{PROBLEM_GRADER_TO_USE})) { - # problem grader defined within problem -- no further action needed - } elsif (defined($rh_envir->{PROBLEM_GRADER_TO_USE})) { - if (ref($rh_envir->{PROBLEM_GRADER_TO_USE}) eq 'CODE') { # user defined grader - $PG->{flags}->{PROBLEM_GRADER_TO_USE} = $rh_envir->{PROBLEM_GRADER_TO_USE}; - } elsif ($rh_envir->{PROBLEM_GRADER_TO_USE} eq 'std_problem_grader') { - if (defined(&std_problem_grader)) { - $PG->{flags}->{PROBLEM_GRADER_TO_USE} = \&std_problem_grader; # defined in PGanswermacros.pl - } # std_problem_grader is the default in any case so don't give a warning. - } elsif ($rh_envir->{PROBLEM_GRADER_TO_USE} eq 'avg_problem_grader') { - if (defined(&avg_problem_grader)) { - $PG->{flags}->{PROBLEM_GRADER_TO_USE} = \&avg_problem_grader; # defined in PGanswermacros.pl - } - } else { - warn "Error: " . $PG->{flags}->{PROBLEM_GRADER_TO_USE} . "is not a known program grader."; +=item * + +A reference to the hash mapping answer names to answer evaluators. + +=item * + +A reference to a hash containing various flags. This includes the following +flags: + +=over + +=item * + +C: determines whether students are told which of +their answers in a problem are wrong. + +=item * + +C: determines whether students submitted answers are +saved. + +=item * + +C: determines whether the cached image of the problem in +typeset mode is always refreshed (i.e. setting this to 1 means cached images are +not used). + +=item * + +C: indicates the existence of a solution. + +=item * + +C: indicates the existence of a hint. + +=item * + +C: contents of COMMENT commands if any. + +=item * + +C: a reference to the chosen problem grader. +C chooses the problem grader as follows: + +=over + +=item * + +If a problem grader has been chosen in the problem by calling +C, it is used. + +=item * + +Otherwise, if the C PG environment variable contains a +reference to a subroutine, it is used. + +=item * + +Otherwise, if the C PG environment variable contains the +string C or the string C, +C<&std_problem_grader> or C<&avg_problem_grader> are used. These graders are +defined in L. + +=item * + +Otherwise, the C flag will contain an empty value and the +PG translator should select C<&std_problem_grader>. + +=back + +=back + +=item * + +A reference to the C object for this problem. + +=back + +The post processing hook added in this method adds a feedback button for each +answer response group that when clicked opens a popover containing feedback for +the answer. A result class is also added to each C (see this +option below) for coloring answer rules via CSS. In addition visually hidden +spans are added that provide feedback for screen reader users. Each +C will be C these spans. + +When and what feedback is shown is determined by translator options described in +L as well as options described below. The hook handles +standard answer types effectively, but macros that add special answer types and +in some case problems (particularly those that use C questions with +C true) may need to help the method for proper placement of the +feedback button and other aspects of feedback. + +There are several options that can be modified, and a few different ways to make +these modifications. Unfortunately, this is perhaps a little bit complicated to +understand, and that really can not be helped. The reason for this is the +extremely loose connection between answer rules, answer labels, and answer +evaluators in PG. + +How these options are set can be controlled in three ways. + +First, an answer hash can have the C key set to a CODE +reference. If this is the case, then the subroutine referenced by this key will +be called and passed the answer hash itself, a reference to the hash of options +described below (any of which can be modified by this subroutine), and a +Mojo::DOM object containing the problem text. Note that if this method sets the +C option, then the other ways of controlling how these options +are set will not be used. + +Second, an element can be added to the DOM that contains an answer rule that has +the class C, and if that answer rule is initially chosen +to be the C and that is not set by the C +method, then this added element will replace it as the C. + +Third, data attributes may be added to elements in the DOM will affect where the +feedback button will be placed. The following data attributes are honored. + +=over + +=item * + +C: If an element in the DOM has this data +attribute and the value of this attribute is the answer name (or label), then +the element that has this data attribute will be used for the C +option described below. + +=item * + +C: If the C is not set by the +C method of the answer hash, and the C also has +this attribute, then the value of this attribute will be used for the +C option described below. + +=item * + +C: If the C is not set by the +C method of the answer hash, and the C also has +this attribute, then the value of this attribute will be used for the +C option described below. + +=back + +The options that can be modified are as follows. + +=over + +=item * + +C: This is the title that is displayed in the feedback popover for +the answers in the response group. By default this is "Answer Preview", +"Correct", "Incorrect", or "n% correct", depending on the status of the answer +and the type of submission. Those strings are translated via C. +Usually this should not be changed, but in some cases the default status titles +are not appropriate for certain types of answers. For example, the +L macros changes this to "Ungraded" for essay answers. + +=item * + +C: This is the CSS class that is added to each answer input in the +response group. By default it is set to the empty string, "correct", +"incorrect", or "partially-correct" depending on the status of the answer and +the type of submission. + +=item * + +C: This is the bootstrap button class added to the feedback button. +By default it is "btn-info", "btn-success", "btn-danger", or "btn-warning" +depending on the status of the answer and the type of submission. + +=item * + +C: This is a string containing additional space separated CSS +classes to add to the feedback button. This is "ms-2" by default. Macros can +change this to affect positioning of the button. This generally should not be +used to change the appearance of the button. + +=item * + +C: This is a Mojo::Collection of elements in the DOM to which +the feedback C and C attribute will be added. By +default this is all elements in the DOM that have a name in the list of response +labels for the response group. Note that for radio buttons and checkboxes, only +the checked elements will be in this collection by default. + +=item * + +C: This is the element in the DOM to insert the feedback button +in or around. How the element is inserted is determined by the C +option. How this option is set is slightly complicated. First, if this option +is set by the answer hash C method, then that is used. If the +C method does not exist or does not set this option, then +initially the last C is used for this. However, if that last +C is contained in another DOM element that has the +C class, then that is used for this instead. If such a +container is not found and there is an element in the DOM that has the +C attribute set whose value is equal to the name +of this last C, then that element is used for the +C instead. Finally, if the C determined as just +described happens to be a radio button or checkbox, then the C +will instead be the parent of the radio button or checkbox (which will hopefully +be the label for that input). + +=item * + +C: The Mojo::DOM method to use to insert the feedback button +relative to the C. It can be C (insert after the +C), C (insert as the last child of +C), C (insert before C), or +C (insert as the first child of C). + +=item * + +C: This is a boolean value that is 1 by default. If true and +the display mode is HTML_MathJax, then the answer previews are wrapped in a +math/tex type script tag. + +=item * + +C: This is a boolean value that is 1 by default. If true and the +translator option C is also true, then the student's +evaluated (or "Entered") answer is shown in the feedback popover if the student +has entered an answer. + +=item * + +C: This is a boolean value that is 1 by default. If true and the +translator option C is also true, then a preview of the +student's answer is shown in the feedback popover. Most likely this should +always be true, and most likely this option (and the translator option) +shouldn't even exist! + +=item * + +C: This is a boolean value that is 1 by default. If this is true +and the translator option C is nonzero, then a preview of +the correct answer is shown in the feedback popover. In other words, this option +prevents showing correct answers even if the frontend requests that correct +answers be shown. + +=item * + +C: This is a boolean value. This should be true if a student has +answered a question, and false otherwise. By default this is set to 1 if the +responses for all answers in the response group are non-empty, and 0 otherwise. +For radio buttons and checkboxes this is if one of the inputs are checked or +not. However, for some answers a non-empty response can occur even if a student +has not answered a question (for example, this occurs for answers to questions +created with the L macro) . So macros that create answers +with responses like that should override this. + +=item * + +C: This is a boolean value. This should be true if the answer is +not graded by the PG problem grader, but is graded manually at a later time, and +should be false if the PG problem grader sets the grade for this answer. For +example, essay answers created by the PGessaymacros.pl macro set this to true. + +=item * + +C: This is a boolean value. This should be true if the answer is +not graded by the PG problem grader, but is graded manually at a later time, and +the answer has changed. + +=back + +=cut + +sub ENDDOCUMENT { + # Insert MathQuill responses if MathQuill is enabled. Add responses to each answer's response group that store the + # latex form of the students' answers and add corresponding hidden input boxes to the page. + if ($envir{useMathQuill} && $main::displayMode =~ /HTML/i) { + for my $answerLabel (keys %{ $PG->{PG_ANSWERS_HASH} }) { + my $answerGroup = $PG->{PG_ANSWERS_HASH}{$answerLabel}; + my $mq_opts = $answerGroup->{ans_eval}{rh_ans}{mathQuillOpts} // {}; + + # This is a special case for multi answers. This is used to obtain mathQuillOpts set + # specifically for individual parts. + my $multiAns; + my $part; + if ($answerGroup->{ans_eval}{rh_ans}{type} =~ /MultiAnswer(?:\((\d*)\))?/) { + # This will only be set if singleResult is not enabled. + $part = $1; + # The MultiAnswer object passes itself as the first optional argument to the evaluator it creates. + # Loop through the evaluators to find it. + for (@{ $answerGroup->{ans_eval}{evaluators} }) { + $multiAns = $_->[1] if (ref($_->[1]) && ref($_->[1]) eq "parser::MultiAnswer"); + } + # Pass the mathQuillOpts of the main MultiAnswer object on to each part + # (unless the part already has the option set). + if (defined $multiAns) { + for (@{ $multiAns->{cmp} }) { + $_->rh_ans(mathQuillOpts => $mq_opts) unless defined $_->{rh_ans}{mathQuillOpts}; + } + } + } + + next if $mq_opts =~ /^\s*disabled\s*$/i; + + my $response_obj = $answerGroup->response_obj; + my $responseCount = -1; + for my $response ($response_obj->response_labels) { + ++$responseCount; + next if ref($response_obj->{responses}{$response}); + + my $ansHash = + defined $multiAns + ? $multiAns->{cmp}[ $part // $responseCount ]{rh_ans} + : $answerGroup->{ans_eval}{rh_ans}; + my $mq_part_opts = $ansHash->{mathQuillOpts} // $mq_opts; + next if $mq_part_opts =~ /^\s*disabled\s*$/i; + + my $context = $ansHash->{correct_value}->context if $ansHash->{correct_value}; + $mq_part_opts->{rootsAreExponents} = 0 + if $context && $context->functions->get('root') && !defined $mq_part_opts->{rootsAreExponents}; + + my $name = "MaThQuIlL_$response"; + RECORD_EXTRA_ANSWERS($name); + + add_content_post_processor(sub { + my $problemContents = shift; + my $input = $problemContents->at(qq{input[name="$response"]}) + || $problemContents->at(qq{textarea[name="$response"]}); + return unless $input; + $input->append( + Mojo::DOM->new_tag( + 'input', + type => 'hidden', + name => $name, + id => $name, + value => $inputs_ref->{$name} // '', + scalar(keys %$mq_part_opts) + ? (data => { mq_opts => JSON->new->encode($mq_part_opts) }) + : '' + )->to_string + ); + }); + } } - } elsif (defined(&std_problem_grader)) { - $PG->{flags}->{PROBLEM_GRADER_TO_USE} = \&std_problem_grader; # defined in PGanswermacros.pl - } else { - # PGtranslator will install its default problem grader } - # add javaScripts - if ($rh_envir->{displayMode} eq 'HTML_jsMath') { - TEXT(''); - } elsif ($rh_envir->{displayMode} eq 'HTML_asciimath') { - TEXT(''); - my $STRING = join("", @{ $PG->{HEADER_ARRAY} }); - unless ($STRING =~ m/mathplayer/) { - HEADER_TEXT('' . "\n" - . ''); - } + # Gather flags + $PG->{flags}{showPartialCorrectAnswers} = $showPartialCorrectAnswers // 1; + $PG->{flags}{recordSubmittedAnswers} = $recordSubmittedAnswers // 1; + $PG->{flags}{refreshCachedImages} = $refreshCachedImages // 0; + $PG->{flags}{hintExists} = $hintExists // 0; + $PG->{flags}{solutionExists} = $solutionExists // 0; + $PG->{flags}{comment} = $pgComment // ''; + + if ($main::displayMode =~ /HTML/i && ($rh_envir->{showFeedback} || $rh_envir->{forceShowAttemptResults})) { + add_content_post_processor(sub { + my $problemContents = shift; + + my $numCorrect = 0; + my $numBlank = 0; + my $numManuallyGraded = 0; + my $needsGrading = $rh_envir->{needs_grading}; + + my @answerNames = keys %{ $PG->{PG_ANSWERS_HASH} }; + + my $showCorrectOnly = + $rh_envir->{showCorrectAnswers} + && $rh_envir->{forceScaffoldsOpen} + && $rh_envir->{forceShowAttemptResults} + && !$rh_envir->{showAttemptAnswers} + && !$rh_envir->{showAttemptPreviews} + && !$rh_envir->{showMessages}; + + for my $answerLabel (@answerNames) { + my $response_obj = $PG->{PG_ANSWERS_HASH}{$answerLabel}->response_obj; + my $ansHash = $PG->{PG_ANSWERS_HASH}{$answerLabel}{ans_eval}{rh_ans}; + + my $answerScore = $ansHash->{score} // 0; + + my %options = ( + resultTitle => maketext('Answer Preview'), + resultClass => '', + btnClass => 'btn-info', + btnAddClass => 'ms-2', + feedbackElements => Mojo::Collection->new, + insertElement => undef, + insertMethod => 'append', # Can be append, append_content, prepend, or prepend_content. + wrapPreviewInTex => defined $ansHash->{non_tex_preview} ? !$ansHash->{non_tex_preview} : 1, + showEntered => 1, + showPreview => 1, + showCorrect => 1, + answerGiven => 0, + manuallyGraded => 0, + needsGrading => 0 + ); + + # Determine if the student gave an answer to any of the questions in this response group and find the + # inputs associated to this response group that the correct/incorrect/partially-correct feedback classes + # will be added to. + for my $responseLabel ($response_obj->response_labels) { + my $response = $response_obj->get_response($responseLabel); + my $elements = $problemContents->find(qq{[name="$responseLabel"]}); + + if (ref($response) eq 'ARRAY') { + # This is the case of checkboxes or radios. + # Feedback classes are added only to those that are checked. + for (@$response) { $options{answerGiven} = 1 if $_->[1] =~ /^checked$/i; } + my $checked = $elements->grep(sub { + my $element = $_; + grep { $_->[0] eq $element->attr('value') && $_->[1] =~ /^checked$/i } @$response; + }); + $elements = $checked if @$checked; + } else { + $options{answerGiven} = 1 if defined $response && $response =~ /\S/; + } + push(@{ $options{feedbackElements} }, @$elements); + } + + if (($rh_envir->{showAttemptResults} && $PG->{flags}{showPartialCorrectAnswers}) + || $rh_envir->{forceShowAttemptResults}) + { + if ($showCorrectOnly) { + $options{resultClass} = 'correct-only'; + } elsif ($answerScore >= 1) { + $options{resultTitle} = maketext('Correct'); + $options{resultClass} = 'correct'; + $options{btnClass} = 'btn-success'; + } elsif ($answerScore == 0) { + $options{resultTitle} = maketext('Incorrect'); + $options{resultClass} = 'incorrect'; + $options{btnClass} = 'btn-danger'; + } else { + $options{resultTitle} = maketext('[_1]% correct', round($answerScore * 100)); + $options{resultClass} = 'partially-correct'; + $options{btnClass} = 'btn-warning'; + } + } + + # If a feedback_options method is provided, it can override anything set above. + $ansHash->{feedback_options}->($ansHash, \%options, $problemContents) + if ref($ansHash->{feedback_options}) eq 'CODE'; + + # Update the counts. This should be after the custom feedback_options call as that method can change + # some of the options. (The draggableProof.pl macro changes the answerGiven option, and the + # PGessaymacros.pl macro changes the manuallyGraded and needsGrading options.) + ++$numCorrect if $answerScore >= 1; + ++$numManuallyGraded if $options{manuallyGraded}; + $needsGrading = 1 if $options{needsGrading}; + ++$numBlank unless $options{manuallyGraded} || $options{answerGiven} || $answerScore >= 1; + + # Don't show the results popover if there is nothing to show. + next + unless @{ $options{feedbackElements} } + && ($answerScore > 0 + || $options{answerGiven} + || $ansHash->{ans_message} + || $rh_envir->{showCorrectAnswers}); + + next if $showCorrectOnly && !$options{showCorrect}; + + # Find an element to insert the button in or around if one has not been provided. + unless ($options{insertElement}) { + # Use the last feedback element by default. + $options{insertElement} = $options{feedbackElements}->last; + + # Check to see if the last feedback element is contained in a feedback container. If so use that. + # Note that this class should not be used by PG or macros directly. It is provided for authors to + # use as an override. + my $ancestorContainer = $options{insertElement}->ancestors('.ww-feedback-container')->first; + if ($ancestorContainer) { + $options{insertElement} = $ancestorContainer; + $options{insertMethod} = 'append_content'; + } else { + # Otherwise check to see if the last feedback element has a special element to attach the + # button to defined in its data attributes, and if so use that instead. + $options{insertElement} = $problemContents->at( + '[data-feedback-insert-element="' . $options{insertElement}->attr('name') . '"]') + || $options{insertElement}; + } + + # For radio or checkbox answers place the feedback button after the label by default. + if (lc($options{insertElement}->attr('type')) =~ /^(radio|checkbox)$/) { + $options{btnAddClass} = 'ms-3'; + $options{insertElement} = $options{insertElement}->parent; + } + + # Check to see if this element has details for placement defined in its data attributes. + $options{btnAddClass} = $options{insertElement}->attr->{'data-feedback-btn-add-class'} + if $options{insertElement} && $options{insertElement}->attr->{'data-feedback-btn-add-class'}; + $options{insertMethod} = $options{insertElement}->attr->{'data-feedback-insert-method'} + if $options{insertElement} && $options{insertElement}->attr->{'data-feedback-insert-method'}; + } + + # Add the correct/incorrect/partially-correct class to the feedback elements. + for (@{ $options{feedbackElements} }) { + $_->attr(class => join(' ', $options{resultClass}, $_->attr->{class} || ())) + if $options{resultClass}; + } + + my $previewAnswer = sub { + my ($preview, $wrapPreviewInTex, $fallback) = @_; + + return $fallback unless defined $preview && $preview =~ /\S/; + + if ($main::displayMode eq 'HTML' || !$wrapPreviewInTex) { + return $preview; + } elsif ($main::displayMode eq 'HTML_dpng') { + return $rh_envir->{imagegen}->add($preview); + } elsif ($main::displayMode eq 'HTML_MathJax') { + return Mojo::DOM->new_tag('script', type => 'math/tex; mode=display', sub {$preview}) + ->to_string; + } + }; + + my $feedbackLine = sub { + my ($title, $line, $class) = @_; + $class //= ''; + return '' unless defined $line && $line =~ /\S/; + return ( + $title + ? Mojo::DOM->new_tag( + 'div', + class => 'card-header text-center p-1', + sub { Mojo::DOM->new_tag('h4', class => 'card-title fs-6 m-0', $title); } + ) + : '' + ) . Mojo::DOM->new_tag('div', class => "card-body text-center $class", sub {$line}); + }; + + my $answerPreview = $previewAnswer->($ansHash->{preview_latex_string}, $options{wrapPreviewInTex}); + + # Create the feedback button and popover, and insert the button at the requested location. + my $feedback = Mojo::DOM->new_tag( + 'button', + type => 'button', + class => "ww-feedback-btn btn btn-sm $options{btnClass} $options{btnAddClass}" + . ($rh_envir->{showMessages} && $ansHash->{ans_message} ? ' with-message' : ''), + 'aria-label' => ( + $rh_envir->{showMessages} && $ansHash->{ans_message} + ? maketext('[_1] with message', $options{resultTitle}) + : $options{resultTitle} + ), + data => { + $showCorrectOnly ? (show_correct_only => 1) : ( + bs_title => Mojo::DOM->new_tag( + 'div', + class => 'd-flex align-items-center justify-content-between', + 'data-bs-theme' => 'dark', + sub { + Mojo::DOM->new_tag('span', style => 'width:20.4px') + . Mojo::DOM->new_tag('span', class => 'mx-3', $options{resultTitle}) + . Mojo::DOM->new_tag( + 'button', + type => 'button', + class => 'btn-close', + 'aria-label' => maketext('Close') + ); + } + )->to_string + ), + answer_label => $answerLabel, + bs_toggle => 'popover', + bs_trigger => 'click', + bs_placement => $showCorrectOnly ? 'right' : 'bottom', + bs_html => 'true', + bs_custom_class => join(' ', 'ww-feedback-popover', $options{resultClass} || ()), + bs_fallback_placements => $showCorrectOnly ? '["left","top","bottom"]' : '[]', + bs_content => Mojo::DOM->new_tag( + 'div', + id => "$answerLabel-feedback", + sub { + Mojo::DOM->new_tag( + 'div', + class => 'card', + sub { + ( + $rh_envir->{showMessages} && $ansHash->{ans_message} + ? $feedbackLine->( + '', $ansHash->{ans_message} =~ s/\n/
                      /gr, + 'feedback-message' + ) + : '' + ) + . ($rh_envir->{showAttemptAnswers} && $options{showEntered} + ? $feedbackLine->(maketext('You Entered'), $ansHash->{student_ans}) + : '') + . ( + $rh_envir->{showAttemptPreviews} && $options{showPreview} + ? $feedbackLine->( + maketext('Preview of Your Answer'), + ( + (defined $answerPreview && $answerPreview =~ /\S/) + || $rh_envir->{showAttemptAnswers} + ? $answerPreview + : $ansHash->{student_ans} + ) + ) + : '' + ) + . ( + $rh_envir->{showCorrectAnswers} && $options{showCorrect} + ? do { + my $correctAnswer = $previewAnswer->( + $ansHash->{correct_ans_latex_string}, + $options{wrapPreviewInTex}, + $ansHash->{correct_ans} + ); + $showCorrectOnly + ? $feedbackLine->( + '', + Mojo::DOM->new_tag( + 'div', + class => + 'd-flex justify-content-between align-items-center gap-1', + sub { + $correctAnswer + . Mojo::DOM->new_tag( + 'button', + type => 'button', + class => 'btn-close', + 'aria-label' => maketext('Close') + ); + } + ) + ) + : $feedbackLine->( + maketext('Correct Answer'), + $rh_envir->{showCorrectAnswers} > 1 ? $correctAnswer + : Mojo::DOM->new_tag( + 'button', + type => 'button', + class => 'reveal-correct-btn btn btn-secondary btn-sm', + maketext('Reveal') + ) + . Mojo::DOM->new_tag( + 'div', + class => 'd-none', + sub {$correctAnswer} + ) + ); + } + : '' + ); + } + ); + } + )->to_string, + }, + sub { Mojo::DOM->new_tag('i', class => $options{resultClass}) } + )->to_string; + + if ($options{insertElement} && $options{insertElement}->can($options{insertMethod})) { + my $insertMethod = $options{insertMethod}; + $options{insertElement}->$insertMethod($feedback); + } + } + + # Generate the result summary if results are being shown. + # FIXME: This is set up to occur when it did previously. That is it ignores the value of + # $PG->{flags}{showPartialCorrectAnswers}. It seems that is incorrect, as it makes that setting rather + # pointless. The summary still reveals if the answer is correct or not. + if ($rh_envir->{showAttemptResults} || $rh_envir->{forceShowAttemptResults}) { + my @summary; + + if (@answerNames == 1) { + if ($numCorrect == 1) { + push( + @summary, + Mojo::DOM->new_tag( + 'div', + class => 'alert alert-success mb-2 p-1', + maketext('The answer is correct.') + ) + ); + } elsif ($numManuallyGraded) { + push( + @summary, + Mojo::DOM->new_tag( + 'div', + class => 'alert alert-info mb-2 p-1', + $needsGrading + ? maketext('The answer will be graded later.') + : maketext('The answer has been graded.') + ) + ); + } elsif ($numBlank) { + push( + @summary, + Mojo::DOM->new_tag( + 'div', + class => 'alert alert-warning mb-2 p-1', + maketext('The question has not been answered.') + ) + ); + } else { + push( + @summary, + Mojo::DOM->new_tag( + 'div', + class => 'alert alert-danger mb-2 p-1', + maketext('The answer is NOT correct.') + ) + ); + } + } else { + if ($numCorrect + $numManuallyGraded == @answerNames) { + if ($numManuallyGraded) { + push( + @summary, + Mojo::DOM->new_tag( + 'div', + class => 'alert alert-success mb-2 p-1', + maketext('All of the computer gradable answers are correct.') + ) + ); + } else { + push( + @summary, + Mojo::DOM->new_tag( + 'div', + class => 'alert alert-success mb-2 p-1', + maketext('All of the answers are correct.') + ) + ); + } + } elsif ($numBlank + $numManuallyGraded + $numCorrect != @answerNames) { + push( + @summary, + Mojo::DOM->new_tag( + 'div', + class => 'alert alert-danger mb-2 p-1', + maketext( + '[_1] of the answers [plural,_1,is,are] NOT correct.', + @answerNames - $numBlank - $numCorrect - $numManuallyGraded + ) + ) + ); + } + if ($numBlank) { + push( + @summary, + Mojo::DOM->new_tag( + 'div', + class => 'alert alert-warning mb-2 p-1', + maketext( + '[quant,_1,of the questions remains,of the questions remain] unanswered.', + $numBlank + ) + ) + ); + } + if ($numManuallyGraded) { + push( + @summary, + Mojo::DOM->new_tag( + 'div', + class => 'alert alert-info mb-2 p-1', + $needsGrading + ? maketext('[_1] of the answers will be graded later.', $numManuallyGraded) + : maketext( + '[_1] of the answers [plural,_1,has,have] been graded.', + $numManuallyGraded + ) + ) + ); + } + } + $PG->{result_summary} = join('', @summary); + } + }); + } + # Install problem grader. + # WeBWorK::PG::Translator will install its default problem grader if none of the conditions below are true. + if (defined($PG->{flags}{PROBLEM_GRADER_TO_USE})) { + # Problem grader defined within problem. No further action needed. + } elsif (defined($rh_envir->{PROBLEM_GRADER_TO_USE})) { + if (ref($rh_envir->{PROBLEM_GRADER_TO_USE}) eq 'CODE') { + # User defined grader. + $PG->{flags}{PROBLEM_GRADER_TO_USE} = $rh_envir->{PROBLEM_GRADER_TO_USE}; + } elsif ($rh_envir->{PROBLEM_GRADER_TO_USE} eq 'std_problem_grader') { + $PG->{flags}{PROBLEM_GRADER_TO_USE} = \&std_problem_grader if (defined(&std_problem_grader)); + } elsif ($rh_envir->{PROBLEM_GRADER_TO_USE} eq 'avg_problem_grader') { + $PG->{flags}{PROBLEM_GRADER_TO_USE} = \&avg_problem_grader if (defined(&avg_problem_grader)); + } else { + warn "Error: $PG->{flags}{PROBLEM_GRADER_TO_USE} is not a known problem grader."; + } + } elsif (defined(&std_problem_grader)) { + $PG->{flags}{PROBLEM_GRADER_TO_USE} = \&std_problem_grader; } + TEXT(MODES(%{ $rh_envir->{problemPostamble} })); - @PG_ANSWERS = (); if ($inputs_ref->{showResourceInfo} && $rh_envir->{show_resource_info}) { - my %resources = %{ $PG->{PG_alias}->{resource_list} }; - my $str = ''; - my @resource_names = (); - foreach my $key (keys %resources) { - $str .= knowlLink("$key$BR", value => "$key$BR" . pretty_print($resources{$key}) . "$BR$BR", base64 => 0); - push @resource_names, $key; - } - if ($str eq '') { - $str = "No auxiliary resources
                      "; + if (keys %{ $PG->{PG_alias}{resource_list} }) { + $PG->debug_message( + '

                      Resources

                        ' . join( + '', + map { + '
                      • ' . knowlLink($_, value => pretty_print($PG->{PG_alias}{resource_list}{$_})) . '
                      • ' + } + sort keys %{ $PG->{PG_alias}{resource_list} } + ) + . '
                      ' + ); } else { - my $summary = "## RESOURCES('" . join("','", @resource_names) . "')$BR\n"; - $PG->debug_message($summary . $str); + $PG->debug_message('No auxiliary resources.'); } } + if ($inputs_ref->{showPGInfo} && $rh_envir->{show_pg_info}) { my $context = $$Value::context->{flags}; $PG->debug_message( - $HR, "Form variables", $BR, pretty_print($inputs_ref), $HR, "Environment variables", - $BR, pretty_print(\%envir), $HR, "Context flags", $BR, pretty_print($context), + "$HR

                      Form variables

                      " . pretty_print($inputs_ref) . '
                      ', + "$HR

                      Environment variables

                      " . pretty_print(\%envir) . '
                      ', + "$HR

                      Context flags

                      " . pretty_print($context) . '
                      ' ); } - #warn keys %{ $PG->{PG_ANSWERS_HASH} }; - @PG_ANSWER_ENTRY_ORDER = (); - my $ans_debug = 0; - foreach my $key (keys %{ $PG->{PG_ANSWERS_HASH} }) { - $answergroup = $PG->{PG_ANSWERS_HASH}->{$key}; - #warn "$key is defined =", defined($answergroup), "PG object is $PG"; - ################# + my (%PG_ANSWERS_HASH, @PG_ANSWER_ENTRY_ORDER); + for my $key (keys %{ $PG->{PG_ANSWERS_HASH} }) { + my $answergroup = $PG->{PG_ANSWERS_HASH}{$key}; + # EXTRA ANSWERS KLUDGE - ################# - # The first response in each answer group is placed in @PG_ANSER_ENTRY_ORDER and %PG_ANSWERS_HASH - # The remainder of the response keys are placed in the EXTRA ANSWERS ARRAY - if (defined($answergroup)) { - my @response_keys = $answergroup->{response}->response_labels; + # The first response label in each answer group is placed in the @PG_ANSWER_ENTRY_ORDER array, and the first + # response evaluator is placed in %PG_ANSWERS_HASH identified by its label. The remainder of the response + # labels are placed in the @KEPT_EXTRA_ANSWERS array. + if (defined $answergroup) { if ($inputs_ref->{showAnsGroupInfo} && $rh_envir->{show_answer_group_info}) { $PG->debug_message(pretty_print($answergroup)); $PG->debug_message(pretty_print($answergroup->{response})); } - my $response_key = $response_keys[0]; - my $answer_key = $answergroup->{ans_label}; - #unshift @response_keys, $response_key unless ($response_key eq $answer_group->{ans_label}); - # don't save the first response key if it is the same as the ans_label - # maybe we should insure that the first response key is always the same as the answer label? - # warn "first response key label and answer key label don't agree" - # unless ($response_key eq $answer_key); - - # even if no answer blank is printed for it? or a hidden answer blank? - # this is still a KLUDGE - # for compatibility the first response key is closer to the old method than the $ans_label - # this is because a response key might indicate an array but an answer label won't - #push @PG_ANSWERS, $response_key,$answergroup->{ans_eval}; - $PG_ANSWERS_HASH{$answer_key} = $answergroup->{ans_eval}; - push @PG_ANSWER_ENTRY_ORDER, $answer_key; - # @KEPT_EXTRA_ANSWERS could be replaced by saving all of the responses for this answergroup - push @KEPT_EXTRA_ANSWERS, @response_keys; + + $PG_ANSWERS_HASH{ $answergroup->{ans_label} } = $answergroup->{ans_eval}; + push @PG_ANSWER_ENTRY_ORDER, $answergroup->{ans_label}; + + push @KEPT_EXTRA_ANSWERS, $answergroup->{response}->response_labels; } else { - warn "$key is ", join("|", %{ $PG->{PG_ANSWERS_HASH}->{$key} }); + warn "$key does not have a valid answer group."; } } - $PG->{flags}->{KEPT_EXTRA_ANSWERS} = \@KEPT_EXTRA_ANSWERS; - $PG->{flags}->{ANSWER_ENTRY_ORDER} = \@PG_ANSWER_ENTRY_ORDER; - - # these should not be needed any longer since PG_alias warning queue is attached to PGcore's - # $PG->warning_message( @{ $PG->{PG_alias}->{flags}->{WARNING_messages}} ); - # $PG->debug_message( @{ $PG->{PG_alias}->{flags}->{DEBUG_messages}} ); + $PG->{flags}{KEPT_EXTRA_ANSWERS} = \@KEPT_EXTRA_ANSWERS; + $PG->{flags}{ANSWER_ENTRY_ORDER} = \@PG_ANSWER_ENTRY_ORDER; - warn "KEPT_EXTRA_ANSWERS", join(" ", @KEPT_EXTRA_ANSWERS), $BR if $ans_debug == 1; - warn "PG_ANSWER_ENTRY_ORDER", join(" ", @PG_ANSWER_ENTRY_ORDER), $BR if $ans_debug == 1; - # not needed for the moment: - # warn "DEBUG messages", join( "$BR",@{$PG->get_debug_messages} ) if $ans_debug==1; - warn "INTERNAL_DEBUG messages", join("$BR", @{ $PG->get_internal_debug_messages }) if $ans_debug == 1; - $STRINGforOUTPUT = join("", @{ $PG->{OUTPUT_ARRAY} }); - $STRINGforHEADER_TEXT = join("", @{ $PG->{HEADER_ARRAY} }); - $STRINGforPOSTHEADER_TEXT = join("", @{ $PG->{POST_HEADER_ARRAY} }); - # warn pretty_print($PG->{PG_ANSWERS_HASH}); - #warn "printing another warning"; + my $STRINGforOUTPUT = join('', @{ $PG->{OUTPUT_ARRAY} }); + my $STRINGforHEADER_TEXT = join('', @{ $PG->{HEADER_ARRAY} }); + my $STRINGforPOSTHEADER_TEXT = join('', @{ $PG->{POST_HEADER_ARRAY} }); (\$STRINGforOUTPUT, \$STRINGforHEADER_TEXT, \$STRINGforPOSTHEADER_TEXT, \%PG_ANSWERS_HASH, $PG->{flags}, $PG); } sub alias { - #warn "alias called ",@_; - $PG->{PG_alias}->make_alias(@_); + my $aux_file_id = shift; + return $PG->{PG_alias}->make_alias($aux_file_id); } sub get_resource { - $PG->{PG_alias}->get_resource(@_); + my $aux_file_id = shift; + return $PG->{PG_alias}->get_resource($aux_file_id); } sub maketext { @@ -652,24 +1533,16 @@ sub insertGraph { } sub findMacroFile { - $PG->{PG_alias}->findMacroFile(@_); -} - -sub findAppletCodebase { - my $appletName = shift; - my $url = eval { $PG->{PG_alias}->findAppletCodebase($appletName) }; - # warn is already trapped under the old system - $PG->warning_message("While using findAppletCodebase to search for applet$appletName: $@") if $@; - $url; + $PG->{PG_loadMacros}->findMacroFile(@_); } sub loadMacros { $PG->{PG_loadMacros}->loadMacros(@_); } -=head2 Problem Grader Subroutines - -=cut +# This is a stub for deprecated problems that call this method. Some of the GeoGebra +# problems that do so actually work even though this method does nothing. +sub findAppletCodebase { return ''; } ## Problem Grader Subroutines @@ -765,30 +1638,69 @@ sub ParserDefineLog { } } -=head2 Filter utilities +=head2 includePGproblem + +Essentially runs the pg problem specified by C<$filePath>, which is a path +relative to the top of the templates directory. The output of that problem +appears in the given problem. + + includePGproblem($filePath); + +=cut + +sub includePGproblem { + my $filePath = shift; + my %save_envir = %main::envir; + my $fullfilePath = $PG->envir("templateDirectory") . $filePath; + my $r_string = $PG->read_whole_problem_file($fullfilePath); + if (ref($r_string) eq 'SCALAR') { + $r_string = $$r_string; + } + + # The problem calling this should provide DOCUMENT and ENDDOCUMENT, + # so we remove them from the included file. + $r_string =~ s/^\s*(END)?DOCUMENT(\(\s*\));?//gm; + + # Reset the problem path so that static images can be found via + # their relative paths. + eval('$main::envir{probFileName} = $filePath'); + # now update the PGalias object + my $save_PGalias = $PG->{PG_alias}; + my $temp_PGalias = PGalias->new( + \%main::envir, + WARNING_messages => $PG->{WARNING_messages}, + DEBUG_messages => $PG->{DEBUG_messages}, + ); + $PG->{PG_alias} = $temp_PGalias; + $PG->includePGtext($r_string); + # Reset the environment to what it was before. + %main::envir = %save_envir; + $PG->{PG_alias} = $save_PGalias; +} -These two subroutines can be used in filters to set default options. They -help make filters perform in uniform, predictable ways, and also make it -easy to recognize from the code which options a given filter expects. +sub beginproblem; # announce that beginproblem is a macro +=head1 FILTER UTILITIES -=head4 assign_option_aliases +These two subroutines can be used in filters to set default options. They help +make filters perform in uniform, predictable ways, and also make it easy to +recognize from the code which options a given filter expects. -Use this to assign aliases for the standard options. It must come before set_default_options -within the subroutine. +=head2 assign_option_aliases - assign_option_aliases(\%options, - 'alias1' => 'option5' - 'alias2' => 'option7' - ); +Use this to assign aliases for the standard options. It must come before +set_default_options within the subroutine. + assign_option_aliases(\%options, + alias1 => 'option5' + alias2 => 'option7' + ); -If the subroutine is called with an option " alias1 => 23 " it will behave as if it had been -called with the option " option5 => 23 " +If the subroutine is called with an option C<< alias1 => 23 >> it will behave as +if it had been called with the option C<< option5 => 23 >>. =cut -# ^function assign_option_aliases sub assign_option_aliases { my $rh_options = shift; warn "The first entry to set_default_options must be a reference to the option hash" @@ -815,35 +1727,37 @@ sub assign_option_aliases { } -=head4 set_default_options +=head2 set_default_options - set_default_options(\%options, - '_filter_name' => 'filter', - 'option5' => .0001, - 'option7' => 'ascii', - 'allow_unknown_options => 0, - } + set_default_options(\%options, + _filter_name => 'filter', + option5 => .0001, + option7 => 'ascii', + allow_unknown_options => 0, + } -Note that the first entry is a reference to the options with which the filter was called. +Note that the first entry is a reference to the options with which the filter +was called. -The option5 is set to .0001 unless the option is explicitly set when the subroutine is called. +The C is set to .0001 unless the option is explicitly set when the +subroutine is called. -The B<'_filter_name'> option should always be set, although there is no error if it is missing. -It is used mainly for debugging answer evaluators and allows -you to keep track of which filter is currently processing the answer. +The C<_filter_name> option should always be set, although there is no error if +it is missing. It is used mainly for debugging answer evaluators and allows you +to keep track of which filter is currently processing the answer. -If B<'allow_unknown_options'> is set to 0 then if the filter is called with options which do NOT appear in the -set_default_options list an error will be signaled and a warning message will be printed out. This provides -error checking against misspelling an option and is generally what is desired for most filters. +If C is set to 0 then if the filter is called with +options which do NOT appear in the set_default_options list an error will be +signaled and a warning message will be printed out. This provides error checking +against misspelling an option and is generally what is desired for most filters. -Occasionally one wants to write a filter which accepts a long list of options, not all of which are known in advance, -but only uses a subset of the options -provided. In this case, setting 'allow_unkown_options' to 1 prevents the error from being signaled. +Occasionally one wants to write a filter which accepts a long list of options, +not all of which are known in advance, but only uses a subset of the options +provided. In this case, setting C to 1 prevents the error +from being signaled. =cut -# ^function set_default_options -# ^uses pretty_print sub set_default_options { my $rh_options = shift; warn "The first entry to set_default_options must be a reference to the option hash" @@ -864,442 +1778,10 @@ sub set_default_options { } } -=over - -=item includePGproblem($filePath) - - includePGproblem($filePath); - - Essentially runs the pg problem specified by $filePath, which is - a path relative to the top of the templates directory. The output - of that problem appears in the given problem. - -=back - -=cut - -# ^function includePGproblem -# ^uses %envir -# ^uses &read_whole_problem_file -# ^uses &includePGtext -sub includePGproblem { - my $filePath = shift; - my %save_envir = %main::envir; - my $fullfilePath = $PG->envir("templateDirectory") . $filePath; - my $r_string = $PG->read_whole_problem_file($fullfilePath); - if (ref($r_string) eq 'SCALAR') { - $r_string = $$r_string; - } - - # The problem calling this should provide DOCUMENT and ENDDOCUMENT, - # so we remove them from the included file. - $r_string =~ s/^\s*(END)?DOCUMENT(\(\s*\));?//gm; - - # Reset the problem path so that static images can be found via - # their relative paths. - eval('$main::envir{probFileName} = $filePath'); - # now update the PGalias object - my $save_PGalias = $PG->{PG_alias}; - my $temp_PGalias = PGalias->new( - \%main::envir, - WARNING_messages => $PG->{WARNING_messages}, - DEBUG_messages => $PG->{DEBUG_messages}, - ); - $PG->{PG_alias} = $temp_PGalias; - $PG->includePGtext($r_string); - # Reset the environment to what it was before. - %main::envir = %save_envir; - $PG->{PG_alias} = $save_PGalias; -} - -sub beginproblem; # announce that beginproblem is a macro - -1; -__END__ - -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 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 - -PG.pl - Provides core Program Generation Language functionality. - -=head1 SYNPOSIS - -In a PG problem: - - DOCUMENT(); # should be the first statment in the problem - - loadMacros(.....); # (optional) load other macro files if needed. - - HEADER_TEXT(...); # (optional) used only for inserting javaScript into problems. - - TEXT( # insert text of problems - "Problem text to be displayed. ", - "Enter 1 in this blank:", - ANS_RULE(1,30) # ANS_RULE() defines an answer blank 30 characters long. - # It is defined in F - ); - - ANS(answer_evalutors); # see F for examples of answer evaluatiors. - - ENDDOCUMENT() # must be the last statement in the problem - -=head1 DESCRIPTION - -This file provides the fundamental macros that define the PG language. It -maintains a problem's text, header text, and answers: - -=over - -=item * - -Problem text: The text to appear in the body of the problem. See TEXT() -below. - -=item * - -Header text: When a problem is processed in an HTML-based display mode, -this variable can contain text that the caller should place in the HEAD of the -resulting HTML page. See HEADER_TEXT() below. - -=item * - -Implicitly-labeled answers: Answers that have not been explicitly -assigned names, and are associated with their answer blanks by the order in -which they appear in the problem. These types of answers are designated using -the ANS() macro. - -=item * - -Explicitly-labeled answers: Answers that have been explicitly assigned -names with the LABELED_ANS() macro, or a macro that uses it. An explicitly- -labeled answer is associated with its answer blank by name. - -=item * - -"Extra" answers: Names of answer blanks that do not have a 1-to-1 -correspondance to an answer evaluator. For example, in matrix problems, there -will be several input fields that correspond to the same answer evaluator. - -=back - -=head1 USAGE - -This file is automatically loaded into the namespace of every PG problem. The -macros within can then be called to define the structure of the problem. - -DOCUMENT() should be the first executable statement in any problem. It -initializes vriables and defines the problem environment. - -ENDDOCUMENT() must be the last executable statement in any problem. It packs -up the results of problem processing for delivery back to WeBWorK. - -The HEADER_TEXT(), TEXT(), and ANS() macros add to the header text string, -body text string, and answer evaluator queue, respectively. - -=over - -=item HEADER_TEXT() - - HEADER_TEXT("string1", "string2", "string3"); - -HEADER_TEXT() concatenates its arguments and appends them to the stored header -text string. It can be used more than once in a file. - -The macro is used for material which is destined to be placed in the HEAD of -the page when in HTML mode, such as JavaScript code. - -Spaces are placed between the arguments during concatenation, but no spaces are -introduced between the existing content of the header text string and the new -content being appended. - - - -=item TEXT() - - TEXT("string1", "string2", "string3"); - -TEXT() concatenates its arguments and appends them to the stored problem text -string. It is used to define the text which will appear in the body of the -problem. It can be used more than once in a file. - -This macro has no effect if rendering has been stopped with the STOP_RENDERING() -macro. - -This macro defines text which will appear in the problem. All text must be -passed to this macro, passed to another macro that calls this macro, or included -in a BEGIN_TEXT/END_TEXT block, which uses this macro internally. No other -statements in a PG file will directly appear in the output. Think of this as the -"print" function for the PG language. - -Spaces are placed between the arguments during concatenation, but no spaces are -introduced between the existing content of the header text string and the new -content being appended. - - - -=item ANS() - - TEXT(ans_rule(), ans_rule(), ans_rule()); - ANS($answer_evaluator1, $answer_evaluator2, $answer_evaluator3); - -Adds the answer evaluators listed to the list of unlabeled answer evaluators. -They will be paired with unlabeled answer rules (a.k.a. answer blanks) in the -order entered. This is the standard method for entering answers. - -In the above example, answer_evaluator1 will be associated with the first -answer rule, answer_evaluator2 with the second, and answer_evaluator3 with the -third. In practice, the arguments to ANS() will usually be calls to an answer -evaluator generator such as the cmp() method of MathObjects or the num_cmp() -macro in L. - - - -=item LABELED_ANS() - - TEXT(labeled_ans_rule("name1"), labeled_ans_rule("name2")); - LABELED_ANS(name1 => answer_evaluator1, name2 => answer_evaluator2); - -Adds the answer evaluators listed to the list of labeled answer evaluators. -They will be paired with labeled answer rules (a.k.a. answer blanks) in the -order entered. This allows pairing of answer evaluators and answer rules that -may not have been entered in the same order. - - - - -=item STOP_RENDERING() - - STOP_RENDERING() unless all_answers_are_correct(); - -Temporarily suspends accumulation of problem text and storing of answer blanks -and answer evaluators until RESUME_RENDERING() is called. - - - -=item RESUME_RENDERING() - - RESUME_RENDERING(); - -Resumes accumulating problem text and storing answer blanks and answer -evaluators. Reverses the effect of STOP_RENDERING(). - - - -=item ENDDOCUMENT() - - ENDDOCUMENT(); - -When PG problems are evaluated, the result of evaluating the entire problem is -interpreted as the return value of ENDDOCUMENT(). Therefore, ENDDOCUMENT() must -be the last executable statement of every problem. It can only appear once. It -returns a list consisting of: - -=back - -=over - -=item * - -A reference to a string containing the rendered text of the problem. - -=item * - -A reference to a string containing text to be placed in the HEAD block -when in and HTML-based mode (e.g. for JavaScript). - -=item * - -A reference to the hash mapping answer labels to answer evaluators. - -=item * - -A reference to a hash containing various flags: - - - -=item * - -C: determines whether students are told which of their answers in a problem are wrong. - -=item * - -C: determines whether students submitted answers are saved. - -=item * - -C: determines whether the cached image of the problem in typeset mode is always refreshed -(i.e. setting this to 1 means cached images are not used). - -=item * - -C: indicates the existence of a solution. - -=item * - -C: indicates the existence of a hint. - -=item * - -C: contents of COMMENT commands if any. - -=item * - -C: a reference to the chosen problem grader. -ENDDOCUMENT chooses the problem grader as follows: - -=over - -=item * - -If a problem grader has been chosen in the problem by calling -C, it is used. - -=item * - -Otherwise, if the C PG environment variable -contains a reference to a subroutine, it is used. - -=item * - -Otherwise, if the C PG environment variable -contains the string C or the string C, -C<&std_problem_grader> or C<&avg_problem_grader> are used. These graders are defined -in L. - -=item * - -Otherwise, the PROBLEM_GRADER_TO_USE flag will contain an empty value -and the PG translator should select C<&std_problem_grader>. - -=back - -=back - - - -=cut - - -################################################################################ - -=head1 PRIVATE MACROS - -These macros should only be used by other macro files. In practice, they are -used exclusively by L. - -=over - -=item inc_ans_rule_count() - -DEPRECATED - -Increments the internal count of the number of answer blanks that have been -defined ($ans_rule_count) and returns the new count. This should only be used -when one is about to define a new answer blank, for example with NEW_ANS_NAME(). - -=cut - -=item RECORD_ANS_NAME() - - RECORD_ANS_NAME("label", "VALUE"); - -Records the label for an answer blank. Used internally by L -to record the order of explicitly-labelled answer blanks. - -=cut - -=item NEW_ANS_NAME() - - NEW_ANS_NAME(); - -Generates an anonymous answer label from the internal count The label is -added to the list of implicity-labeled answers. Used internally by -L to generate labels for unlabeled answer blanks. - -=cut - -=item ANS_NUM_TO_NAME() - - ANS_NUM_TO_NAME($num); - -Generates an answer label from the supplied answer number, but does not add it -to the list of inplicitly-labeled answers. Used internally by -L in generating answers blanks that use radio buttons or -check boxes. (This type of answer blank uses multiple HTML INPUT elements with -the same label, but the label should only be added to the list of implicitly- -labeled answers once.) - -=cut - -=item RECORD_FROM_LABEL() - - RECORD_FORM_LABEL("label"); - -Stores the label of a form field in the "extra" answers list. This is used to -keep track of answer blanks that are not associated with an answer evaluator. - -=cut - -=item NEW_ANS_ARRAY_NAME() - - NEW_ANS_ARRAY_NAME($num, $row, $col); - -Generates a new answer label for an array (vector) element and adds it to the -list of implicitly-labeled answers. - -=cut - -=item NEW_ANS_ARRAY_NAME_EXTENSION() - - NEW_ANS_ARRAY_NAME_EXTENSION($num, $row, $col); - -Generate an additional answer label for an existing array (vector) element and -add it to the list of "extra" answers. - -=cut - -=item get_PG_ANSWERS_HASH() - - get_PG_ANSWERS_HASH(); - get_PG_ANSWERS_HASH($key); - - - -=cut - -=item includePGproblem($filePath) - - includePGproblem($filePath); - - Essentially runs the pg problem specified by $filePath, which is - a path relative to the top of the templates directory. The output - of that problem appears in the given problem. - -=cut - -=back - =head1 SEE ALSO L, L. =cut - - - 1; diff --git a/macros/answers/ConditionalHint.pl b/macros/answers/ConditionalHint.pl index 959434756b..4c3f731e32 100644 --- a/macros/answers/ConditionalHint.pl +++ b/macros/answers/ConditionalHint.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/answers/PGfunctionevaluators.pl b/macros/answers/PGfunctionevaluators.pl index f7c7117ec0..dc3907eeb5 100644 --- a/macros/answers/PGfunctionevaluators.pl +++ b/macros/answers/PGfunctionevaluators.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -50,7 +50,7 @@ =head2 MathObjects and answer evaluators =cut -BEGIN { be_strict() } +BEGIN { strict->import; } # Until we get the PG cacheing business sorted out, we need to use # PG_restricted_eval to get the correct values for some(?) PG environment diff --git a/macros/answers/PGmiscevaluators.pl b/macros/answers/PGmiscevaluators.pl index fd91a20154..e156a57209 100644 --- a/macros/answers/PGmiscevaluators.pl +++ b/macros/answers/PGmiscevaluators.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -29,7 +29,7 @@ =head2 MathObjects and answer evaluators =cut -BEGIN { be_strict() } +BEGIN { strict->import; } sub _PGmiscevaluators_init { } =head1 checkbox_cmp diff --git a/macros/answers/PGstringevaluators.pl b/macros/answers/PGstringevaluators.pl index 15c1270c26..617a120649 100644 --- a/macros/answers/PGstringevaluators.pl +++ b/macros/answers/PGstringevaluators.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -35,7 +35,7 @@ =head2 MathObjects and answer evaluators =cut -BEGIN { be_strict() } +BEGIN { strict->import; } sub _PGstringevaluators_init { } =head1 String Filters diff --git a/macros/answers/answerComposition.pl b/macros/answers/answerComposition.pl index b8a470804e..e6de04be37 100644 --- a/macros/answers/answerComposition.pl +++ b/macros/answers/answerComposition.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/answers/answerCustom.pl b/macros/answers/answerCustom.pl index 24c4464a54..edbdc16736 100644 --- a/macros/answers/answerCustom.pl +++ b/macros/answers/answerCustom.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/answers/answerHints.pl b/macros/answers/answerHints.pl index 2df9a6ab76..91cadeb46f 100644 --- a/macros/answers/answerHints.pl +++ b/macros/answers/answerHints.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -144,13 +144,13 @@ sub AnswerHints { cmp_options => [], @options, ); - next if $options{checkTypes} && $correct->type ne $student->type; next if !$options{processPreview} && $ans->{isPreview}; $wrongList = [$wrongList] unless ref($wrongList) eq 'ARRAY'; foreach my $wrong (@{$wrongList}) { if (ref($wrong) eq 'CODE') { - if (($ans->{score} < 1 || $options{checkCorrect}) + if ((!$options{checkTypes} || $correct->type eq $student->type) + && ($ans->{score} < 1 || $options{checkCorrect}) && ($ans->{ans_message} eq "" || $options{replaceMessage})) { # Make the call to run the function inside an eval to trap errors @@ -166,16 +166,12 @@ sub AnswerHints { } } } else { - $wrong = Value::makeValue($wrong); - if ( - ( - $ans->{score} < 1 - || $options{checkCorrect} - || AnswerHints::Compare($correct, $wrong, $ans) - ) - && ($ans->{ans_message} eq "" || $options{replaceMessage}) - && AnswerHints::Compare($wrong, $student, $ans, @{ $options{cmp_options} }) - ) + unless (Value::isValue($wrong)) { + $wrong = main::Formula($wrong); + $wrong = $wrong->{tree}->Compute if $wrong->{tree}{canCompute}; + } + if (($ans->{ans_message} eq "" || $options{replaceMessage}) + && AnswerHints::Compare($wrong, $student, $ans, @{ $options{cmp_options} })) { $ans->{ans_message} = $ans->{error_message} = $message; $ans->{score} = $options{score} if defined $options{score}; @@ -197,19 +193,19 @@ package AnswerHints; # and returns true if the two values match and false otherwise. # sub Compare { - my $self = shift; - my $other = shift; - my $ans = shift; - $ans = bless { %{$ans}, @_ }, ref($ans); # make a copy + my ($self, $other, $ans, @options) = @_; + return 0 unless $self->typeMatch($other); # make sure these can be compared + $ans = bless { %{$ans}, @options }, ref($ans); # make a copy $ans->{typeError} = 0; $ans->{ans_message} = $ans->{error_message} = ""; $ans->{score} = 0; - if (sprintf("%p", $self) ne sprintf("%p", $ans->{correct_value})) { + + if ($self->address != $ans->{correct_value}->address) { $ans->{correct_ans} = $self->string; $ans->{correct_value} = $self; $ans->{correct_formula} = Value->Package("Formula")->new($self); } - if (sprintf("%p", $other) ne sprintf("%p", $ans->{student_value})) { + if ($other->address != $ans->{student_value}->address) { $ans->{student_ans} = $other->string; $ans->{student_value} = $other; $ans->{student_formula} = Value->Package("Formula")->new($other); diff --git a/macros/answers/answerVariableList.pl b/macros/answers/answerVariableList.pl index 1612f2ad9e..bbdff5c4a3 100644 --- a/macros/answers/answerVariableList.pl +++ b/macros/answers/answerVariableList.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/answers/extraAnswerEvaluators.pl b/macros/answers/extraAnswerEvaluators.pl index 6f71c7ac95..e30ce3611f 100644 --- a/macros/answers/extraAnswerEvaluators.pl +++ b/macros/answers/extraAnswerEvaluators.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/answers/unorderedAnswer.pl b/macros/answers/unorderedAnswer.pl index 829bdfca8e..3a9bd06de9 100644 --- a/macros/answers/unorderedAnswer.pl +++ b/macros/answers/unorderedAnswer.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/capa/PG_CAPAmacros.pl b/macros/capa/PG_CAPAmacros.pl index 25f5197396..f279fe5d1a 100644 --- a/macros/capa/PG_CAPAmacros.pl +++ b/macros/capa/PG_CAPAmacros.pl @@ -1,7 +1,5 @@ -BEGIN { - be_strict(); -} +BEGIN { strict->import; } sub CAPA_ans { my $ans = shift; diff --git a/macros/contexts/contextABCD.pl b/macros/contexts/contextABCD.pl index e6cab808db..e788c6af09 100644 --- a/macros/contexts/contextABCD.pl +++ b/macros/contexts/contextABCD.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextAlternateDecimal.pl b/macros/contexts/contextAlternateDecimal.pl index a546a7be84..2b14de7e09 100644 --- a/macros/contexts/contextAlternateDecimal.pl +++ b/macros/contexts/contextAlternateDecimal.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextAlternateIntervals.pl b/macros/contexts/contextAlternateIntervals.pl index d1c6666df8..f35943e2bf 100644 --- a/macros/contexts/contextAlternateIntervals.pl +++ b/macros/contexts/contextAlternateIntervals.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextArbitraryString.pl b/macros/contexts/contextArbitraryString.pl index 94e5b8a075..e212ffca4c 100644 --- a/macros/contexts/contextArbitraryString.pl +++ b/macros/contexts/contextArbitraryString.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextBaseN.pl b/macros/contexts/contextBaseN.pl new file mode 100644 index 0000000000..94b3402c6a --- /dev/null +++ b/macros/contexts/contextBaseN.pl @@ -0,0 +1,337 @@ + +=head1 NAME + +contextBaseN.pl - Implements a MathObject class and context for numbers +in non-decimal bases + +=head1 DESCRIPTION + +This context implements positive integers and some operations on integers in a non-decimal base +greater than or equal to 2. The numbers will be stored internally in decimal, though parsed +and shown in the chosen base. + +In addition, basic integer arithemetic (+,-,*,/,%,^) are available for these numbers. +Division is defined in an integer sense. + +The original purpose for this is simple conversion and operations in another base, however +it is not limited to this. + +To use a non-decimal base MathObject, first load the contextBaseN.pl file: + + loadMacros('contextBaseN.pl'); + +There are two contexts: C and C, where the former +allows operations between numbers and the latter only allows numbers. To use either, +one must set the base. For example: + + Context('BaseN')->setBase(5); + +Now most numerical strings in Compute, Formula, and student answers will be read in base five. + + $a = Compute('104'); + $b = Compute('233'); + $sum = $a+$b # this is the base-5 number 342 (decimal 97) + +or a shorter way: + + $sum = Compute('104+233'); + +Also, when a string is the argument to some other Math Object and that string needs to +be parsed, numerical substrings will be read in base 5: + + $point = Point('(104, 233)'); # this is (29, 68) in base ten + +For Math Object constructors that directly accept a number or numbers as arguments, +the numbers will be read in base ten. All of the following should be read in base ten: + + $r = Real(29); + $r = Real('68'); + $p = Point(29, 68); + +For many problems, one may wish to not allow operators in the student answers. Use +'LimitedBaseN' for this. + + Context('LimitedBaseN')->setBase(5); + $sum = Compute("104+233"); # There will be an error on this line now. + +In both contexts, rather than pass the base as a number, another option is to pass the +digits used for the number to the C method. For example, if one wants to use base-12 +and use the alternative digits 0..9,'T','E', then + + Context('BaseN')->setBase([0 .. 9, 'T', 'E']); + +Then one can use the digits 'T' and 'E' in a number like: + + Compute('9TE'); + +A few strings can be passed to the C method with preset meanings: + + C for [0,1] + C for [0 .. 7] + C for [0 .. 9] + C for [0 .. 9, 'A', 'B'] + C for [0 .. 9, 'A' .. 'F'] + C for ['A' .. 'Z', 'a' .. 'z', 0 .. 9, '_', '?'] + +The last two digits for C are nonstandard. We want to avoid '+' and '/' here as they have arithmetic meaning. + +=head2 Sample PG problem + +A simple PG problem that asks a student to convert a number into base-5: + + DOCUMENT(); + loadMacros(qw(PGstandard.pl PGML.pl contextBaseN.pl)); + + Context('LimitedBaseN')->setBase(5); + + # decimal number picked randomly. + $a = random(130,500); + $a_5 = Real($a); # converts $a to base-5 + + BEGIN_PGML + Convert [$a] to base-5: + + [$a] = [__]*{$a_5} + END_PGML + ENDDOCUMENT(); + +The star variant answer blank will print the base in subscript after the answer blank. +=cut + +sub _contextBaseN_init { + context::BaseN::Init(@_); + sub convertBase { context::BaseN::convert(@_); } +} + +package context::BaseN; + +# Define the contexts 'BaseN' and 'LimitedBaseN' +sub Init { + my $context = $main::context{BaseN} = context::BaseN::Context->new(); + $context = $main::context{LimitedBaseN} = $context->copy; + $context->{name} = 'LimitedBaseN'; + $context->operators->undefine($context->operators->names); + $context->parens->undefine('|', '{', '['); +} + +=head2 convertBase + +The function C converts the value from or to other bases depending on the options +in C. The input C is a positive number or string version of a positive number in some base. + +=head3 options + +=over + +=item * C the base that C is in. Default is 10. Can take the same values as C. + +=item * C the base that C will be converted to. Default is 10. Can take the same values as C. + +=back + +=head3 Examples + +For the following, since C is not used, the base of C is assumed to be 10. + + convertBase(58, to => 5); # returns 213 + convertBase(58, to => 8); # returns 72 + convertBase(734, to => 16); # returns 2DE + +For the following, since C is not used, these are converted to base 10. + + convertBase(213, from => 5); # returns 58 + convertBase(72, from => 8); # returns 58 + convertBase('2DE', from => 16); # returns 734 + +Both C and C can be used together. + + convertBase(213, from => 5, to => 8); # returns 72 + +If one wants to use a different set of digits, say 0..9, 'T', 'E' for base-12 as an example + + convertBase(565, to => [0 .. 9, 'T', 'E']); # returns '3E1' + +=cut + +my $convertContext; + +sub convert { + my ($value, %options) = @_; + my $from = $options{'from'} // 10; + my $to = $options{'to'} // 10; + + $convertContext = $main::context{BaseN}->copy unless $convertContext; + if ($from != 10) { + $convertContext->setBase($from); + $value = $convertContext->fromBase($value); + } + if ($to != 10) { + $convertContext->setBase($to); + $value = $convertContext->toBase($value); + } + return $value; +} + +package context::BaseN::Context; +our @ISA = ('Parser::Context'); + +# Create a Context based on Numeric that allows +, -, *, /, %, and ^ on BaseN integers. + +sub new { + my $self = shift; + my $class = ref($self) || $self; + my $context = bless Parser::Context->getCopy('Numeric'), $class; + $context->{name} = 'BaseN'; + $context->{parser}{Number} = 'context::BaseN::Number'; + $context->{value}{Real} = 'context::BaseN::Real'; + $context->functions->disable('All'); + $context->constants->clear(); + $context->{pattern}{number} = '[' . join('', 0 .. 9, 'A' .. 'Z') . ']+'; + $context->{precedence}{BaseN} = $context->{precedence}{special}; + $context->flags->set(limits => [ -1000, 1000, 1 ]); + $context->operators->add( + '%' => { + class => 'context::BaseN::BOP::modulo', + precedence => 3, + associativity => 'left', + type => 'bin', + string => ' % ', + TeX => '\mathbin{\%}', + } + ); + return $context; +} + +# set the base of the context. Either an integer that is at least 2, an arrayref of digits, +# or a preset: 'binary', 'octal', 'decimal', 'duodecimal', 'hexadecimal', or 'base64'. +sub setBase { + my ($self, $base) = @_; + my $digits; + + $base = [ 0, 1 ] if ($base eq 'binary'); + $base = [ 0 .. 7 ] if ($base eq 'octal'); + $base = [ 0 .. 9 ] if ($base eq 'decimal'); + $base = [ 0 .. 9, 'A', 'B' ] if ($base eq 'duodecimal'); + $base = [ 0 .. 9, 'A' .. 'F' ] if ($base eq 'hexadecimal'); + $base = [ 'A' .. 'Z', 'a' .. 'z', 0 .. 9, '_', '?' ] if ($base eq 'base64'); + + if (ref($base) eq 'ARRAY') { + $digits = $base; + $base = scalar(@$digits); + die 'Base must be at least 2' unless $base >= 2; + } else { + die 'Base must be an integer' unless $base == int($base); + die 'Base must be at least 2' unless $base >= 2; + die 'You must provide a digit list for bases bigger than 36' if $base > 36; + $digits = [ ('0' .. '9', 'A' .. 'Z')[ 0 .. $base - 1 ] ]; + } + + $self->{base} = $base; + $self->{digits} = $digits; + $self->{digitMap} = { map { ($digits->[$_], $_) } (0 .. $base - 1) }; + $self->{pattern}{number} = '[' . join('', @$digits) . ']+'; + my $msg = 'Numbers should consist only of the digits: ' . join(',', @$digits); + $self->{error}{msg}{"Variable '%s' is not defined in this context"} = $msg; + $self->{error}{msg}{"'%s' is not defined in this context"} = $msg; + $self->update; +} + +sub copy { + my $self = shift; + my $copy = $self->SUPER::copy; + $copy->{base} = $self->{base}; + $copy->{digits} = $self->{digits}; + $copy->{digitMap} = $self->{digitMap}; + return $copy; +} + +# Convert a number in base10 to the given base. +sub toBase { + my ($self, $base10) = @_; + my $b = $self->{base}; + my $digits = $self->{digits}; + + my @baseB; + do { + my $d = $base10 % $b; + $base10 = ($base10 - $d) / $b; + unshift(@baseB, $digits->[$d]); + } while $base10; + + return join('', @baseB); +} + +# Convert a number in a given base to base 10. +sub fromBase { + my ($self, $baseB) = @_; + my $b = $self->{base}; + my $digits = $self->{digits}; + my $digit = $self->{digitMap}; + + my $base10 = 0; + for my $d (split('', $baseB)) { + die 'The number should only consist of the digits: ' . join(',', @$digits) unless defined($digit->{$d}); + $base10 = $base10 * $b + $digit->{$d}; + } + + return $base10; +} + +# A replacement for Parser::Number that accepts numbers in a non-decimal base and +# converts them to decimal for internal use +package context::BaseN::Number; +our @ISA = ('Parser::Number'); + +# Create a new number in the given base and convert to base 10. +sub new { + my ($self, $equation, $value, $ref) = @_; + my $context = $equation->{context}; + + Value::Error('The base must be set for this context') unless $context->{base}; + + $value = $context->fromBase($value); + return $self->SUPER::new($equation, $value, $ref); +} + +sub eval { + $self = shift; + return $self->Package('Real')->make($self->context, $self->{value}); +} + +# Modulo operator +package context::BaseN::BOP::modulo; +our @ISA = ('Parser::BOP::divide'); + +# +# Do the division. +# +sub _eval { $_[1] % $_[2] } + +# A replacement for Value::Real that handles non-decimal integers +package context::BaseN::Real; +our @ISA = ('Value::Real'); + +# Stringify and TeXify the number in the context's base +sub string { + my $self = shift; + return $self->context->toBase($self->value); +} + +sub TeX { + my $self = shift; + return '\text{' . $self->string . '}'; +} + +sub ans_array { + my $self = shift; + return $self->ans_rule(@_) . main::math_ev3('_{' . $self->context->{base} . '}'); +} + +# Define division as integer division. +sub div { + my ($self, $l, $r, $other) = Value::checkOpOrderWithPromote(@_); + Value::Error("Division by zero") if $r->{data}[0] == 0; + return $self->inherit($other)->make(int($l->{data}[0] / $r->{data}[0])); +} + +1; diff --git a/macros/contexts/contextBoolean.pl b/macros/contexts/contextBoolean.pl new file mode 100644 index 0000000000..a3bdcf7e65 --- /dev/null +++ b/macros/contexts/contextBoolean.pl @@ -0,0 +1,578 @@ + +=head1 NAME + +contextBoolean.pl - Implements a MathObject class for Boolean expressions + +=head1 DESCRIPTION + +Load this file: + + loadMacros('contextBoolean.pl'); + +and then select the context: + + Context('Boolean'); + +=head2 CONSTANTS + +This constant recognizes two constants by default, C and C. The following are all equivalent: + + $T = Compute('1'); + $T = Boolean('T'); + $T = Context()->T; + $T = context::Boolean->T; + +=head2 VARIABLES + +By default, this context has two variables, C

                      and C. More variables can be added through the usual +means of modifying context: + + Context->variables->add( r => 'Boolean' ); + +=head2 OPERATORS + +Changing the LaTeX representations of the boolean operators is handled through the operators C, C, +C, and C. Note the extra space following the LaTeX command. + + Context->operators->set( not => { TeX => '\neg ' } ); + + +=head3 Aliases and Alternatives + +Modifications to the operators should be applied to the string versions of each operator: 'or', 'xor', 'and', +and 'not'; rather than to any of the following aliases or alternatives. + +=over + +=item OR + +The 'or' operator is indicated by C, C<+>, C<\\/>, C, or unicode C. + +=item AND + +The 'and' operator is indicated by C, C<*>, whitespace (as with implicit multiplication), C, C, +or unicode C. + +=item XOR + +The 'xor' operator is indicated by C, C<\>\<>, C, or unicodes C, C. + +=item NOT + +The 'not' operator is indicated by C, C<->, C, C<~>, or unicodes C, C. + +A right-associative version of the 'not' operator is also available by using C<'> or C<`> following the expression +to be negated. + +=back + +=head2 OPERATOR PRECEDENCE + +=over + +=item S>> + +This context supports two paradigms for operation precedence: C (default) and C. + +The default setting, C, gives all boolean operations the same priority, meaning that parenthesis +are the only manner by which an expression will evaluate operations to the right before those to the left. + + $a = Compute("T or T and F"); # $a == F + +The C setting priortizes C < C < C < C. + + Context()->setPrecedence('oxan'); + $b = Compute("T or T and F"); # $b == T + +=back + +=head2 REDUCTION + +The context also handles C with the following reduction rules: + +=over + +=item C<'x||1'> + + $f = Formula('p or T')->reduce; # $f == T + +=item C<'x||0'> + + $f = Formula('p or F')->reduce; # $f == Formula('p') + +=item C<'x&&1'> + + $f = Formula('p and T')->reduce; # $f == Formula('p') + +=item C<'x&&0'> + + $f = Formula('p and F')->reduce; # $f == F + +=item C<'!!x'> + + $f = Formula('not not p')->reduce; # $f == Formula('p'); + +=back + +=head2 COMPARISON + +Boolean Formula objects are considered equal whenever the two expressions generate the same truth table. + + $f = Formula('not (p or q)'); + $g = Formula('(not p) and (not q)'); + # $f == $g is true + +=cut + +sub _contextBoolean_init { context::Boolean::Init() } + +package context::Boolean; + +sub Init { + my $context = $main::context{Boolean} = Parser::Context->getCopy('Numeric'); + $context->{name} = 'Boolean'; + + $context->{parser}{Number} = 'context::Boolean::Number'; + $context->{parser}{Formula} = 'context::Boolean::Formula'; + $context->{value}{Formula} = 'context::Boolean::Formula'; + $context->{value}{Boolean} = 'context::Boolean::Boolean'; + $context->{value}{Real} = 'context::Boolean::Boolean'; + $context->{precedence}{Boolean} = $context->{precedence}{Real}; + + # Disable unnecessary context stuff + $context->functions->disable('All'); + $context->strings->clear(); + $context->lists->clear(); + + # Define our logic operators + $context->operators->are( + 'or' => { + class => 'context::Boolean::BOP::or', + precedence => 3, + associativity => 'left', + type => 'bin', + rightparens => 'same', + string => ' or ', + TeX => '\vee ', + perl => '||', + # alternatives => ["\x{2228}"], + }, + 'and' => { + class => 'context::Boolean::BOP::and', + precedence => 3, + associativity => 'left', + type => 'bin', + rightparens => 'same', + string => ' and ', + TeX => '\wedge ', + perl => '&&', + # alternatives => ["\x{2227}"], + }, + 'xor' => { + class => 'context::Boolean::BOP::xor', + precedence => 3, + associativity => 'left', + type => 'bin', + rightparens => 'same', + string => ' xor ', + perl => '!=', + TeX => '\oplus ', + # alternatives => [ "\x{22BB}", "\x{2295}" ], + }, + 'not' => { + class => 'context::Boolean::UOP::not', + precedence => 3, + associativity => 'left', + type => 'unary', + string => 'not ', + TeX => '\mathord{\sim}', + perl => '!', + # alternatives => ["\x{00AC}"], + }, + '`' => { + class => 'context::Boolean::UOP::not', + precedence => 3, + associativity => 'right', + type => 'unary', + string => '`', + TeX => '^\prime', + perl => '!', + }, + ' ' => { + class => 1, + precedence => 3, + associativity => 'left', + type => 'bin', + string => 'and', + hidden => 1 + }, + '*' => { alias => 'and' }, + '/\\' => { alias => 'and' }, + 'wedge' => { alias => 'and', alternatives => ["\x{2227}"] }, + '+' => { alias => 'or' }, + '\\/' => { alias => 'or' }, + 'vee' => { alias => 'or', alternatives => ["\x{2228}"] }, + '-' => { alias => 'not', alternatives => ["\x{00AC}"] }, + '!' => { alias => 'not' }, + '~' => { alias => 'not', alternatives => ["\x{223C}"] }, + '\'' => { alias => '`' }, + '><' => { alias => 'xor' }, + 'oplus' => { alias => 'xor', alternatives => [ "\x{22BB}", "\x{2295}" ] }, + ); + + # redefine, but disable, some usual context tokens for 'clearer' error messages + $context->operators->redefine([ ',', 'fn' ], from => 'Numeric'); + $context->lists->redefine('List', from => 'Numeric'); + $context->operators->redefine([ '/', '^', '**' ], from => 'Numeric'); + $context->operators->undefine('/', '^', '**'); + delete $context->operators->get('/')->{space}; + + # Set default variables 'p' and 'q' + $Parser::Context::Variables::type{Boolean} = $Parser::Context::Variables::type{Real}; + $context->variables->are( + p => 'Boolean', + q => 'Boolean', + ); + + # Set up new reduction rules: + $context->reductions->set('x||1' => 1, 'x||0' => 1, 'x&&1' => 1, 'x&&0' => 1, '!!x' => 1); + + # Define constants for 'True' and 'False' + $context->constants->{namePattern} = qr/(?:\w|[\x{22A4}\x{22A5}])+/; + $context->constants->are( + T => { + value => context::Boolean::Boolean->new($context, 1), + string => 'T', + TeX => '\top', + perl => 'context::Boolean->T', + isConstant => 1, + alternatives => ["\x{22A4}"] + }, + F => { + value => context::Boolean::Boolean->new($context, 0), + string => 'F', + TeX => '\bot', + perl => 'context::Boolean->F', + isConstant => 1, + alternatives => ["\x{22A5}"] + }, + 'True' => { alias => 'T' }, + 'False' => { alias => 'F' }, + ); + + # add our methods to this context + bless $context, 'context::Boolean::Context'; + + # allow authors to create Boolean values + main::PG_restricted_eval('sub Boolean { Value->Package("Boolean()")->new(@_) }'); +} + +# top-level access to context-specific T and F +sub T { + my $context = main::Context(); + Value::Error("Context must be a Boolean context") unless $context->can('T'); + return $context->T; +} + +sub F { + my $context = main::Context(); + Value::Error("Context must be a Boolean context") unless $context->can('F'); + return $context->F; +} + +# Subclass the Parser::Context to override copy() and add T and F functions +package context::Boolean::Context; +our @ISA = ('Parser::Context'); + +sub copy { + my $self = shift->SUPER::copy(@_); + ## update the T and F constants to refer to this context + $self->constants->set( + T => { value => context::Boolean::Boolean->new($self, 1) }, + F => { value => context::Boolean::Boolean->new($self, 0) } + ); + return $self; +} + +# Access to the constant T and F values +sub F { shift->constants->get('F')->{value} } +sub T { shift->constants->get('T')->{value} } + +# Easy setting of precedence to different types +sub setPrecedence { + my ($self, $order) = @_; + if ($order eq 'equal') { + $self->operators->set( + or => { precedence => 3 }, + xor => { precedence => 3 }, + and => { precedence => 3 }, + ' ' => { precedence => 3 }, + not => { precedence => 3 }, + '`' => { precedence => 3 }, + ); + } elsif ($order eq 'oxan') { + $self->operators->set( + or => { precedence => 1 }, + xor => { precedence => 2 }, + and => { precedence => 3 }, + ' ' => { precedence => 3 }, + not => { precedence => 6 }, + '`' => { precedence => 6 }, + ); + } else { + Value::Error("Unknown precedence class '%s'", $order); + } +} + +# Subclass Parser::Number to return the constant T or F +package context::Boolean::Number; +our @ISA = ('Parser::Number'); + +sub eval { + my $self = shift; + return $self->context->constants->get(('F', 'T')[ $self->{value} ])->{value}; +} + +sub perl { + my $self = shift; + return $self->context->constants->get(('F', 'T')[ $self->{value} ])->{perl}; +} + +# Subclass Value::Formula for boolean formulas +package context::Boolean::Formula; +our @ISA = ('Value::Formula'); + +sub cmp_defaults { return (shift->SUPER::cmp_defaults(@_), mathQuillOpts => { spaceBehavesLikeTab => \0 }) } + +# use every combination of T/F across all variables +sub createRandomPoints { + my $self = shift; + my $context = $self->{context}; + my @variables = $context->variables->names; + my @points; + my @values; + + my $T = $context->T; + my $F = $context->F; + + my $f = $self->{f}; + $f = $self->{f} = $self->perlFunction(undef, \@variables) unless $f; + + foreach my $combination (0 .. 2**@variables - 1) { + my @point = map { $combination & 2**$_ ? $T : $F } (0 .. $#variables); + my $value = &$f(@point); + push @points, \@point; + push @values, $value; + } + + $self->{test_points} = \@points; + $self->{test_values} = \@values; + return \@points; +} + +sub createPointValues { + my $self = shift; + my $context = $self->context; + my $points = shift || $self->{test_points} || $self->createRandomPoints; + my @vars = $context->variables->variables; + my @params = $context->variables->parameters; + + my $f = $self->{f}; + $f = $self->{f} = $self->perlFunction(undef, [ @vars, @params ]) unless $f; + + my (@values, $v); + foreach my $p (@$points) { + $v = eval { &$f(@$p) }; + Value::Error("Can't evaluate formula on test point (%s)", join(',', @{$p})) unless (defined $v); + push @values, $v; + } + + $self->{test_points} = $points; + $self->{test_values} = \@values; + + return \@values; +} + +package context::Boolean::BOP; +our @ISA = qw(Parser::BOP); + +sub _check { + my $self = shift; + return if $self->checkNumbers; + $self->Error("Operands of '%s' must be 'Boolean'", $self->{bop}); +} + +sub perl { + my $self = shift; + my $l = $self->{lop}; + my $r = $self->{rop}; + my $bop = $self->{def}{perl} || $self->{def}{string}; + my $lPerl = $self->{lop}->perl(1) . '->value'; + my $rPerl = $self->{rop}->perl(2) . '->value'; + my $result = "$lPerl $bop $rPerl"; + return "($result ? context::Boolean->T : context::Boolean->F)"; +} + +# remove once UOP::string passses 'same' as second argument +sub string { + my ($self, $precedence, $showparens, $position, $outerRight) = @_; + $showparens = "same" if !($position // '') && !($showparens // ''); + return $self->SUPER::string($precedence, $showparens, $position, $outerRight); +} + +# remove once UOP::TeX passses 'same' as second argument +sub TeX { + my ($self, $precedence, $showparens, $position, $outerRight) = @_; + $showparens = "same" if !($position // '') && !($showparens // ''); + return $self->SUPER::TeX($precedence, $showparens, $position, $outerRight); +} + +package context::Boolean::BOP::or; +our @ISA = qw(context::Boolean::BOP); + +sub _eval { + my ($self, $l, $r) = @_; + return ($l->value || $r->value ? $self->context->T : $self->context->F); +} + +sub _reduce { + my $self = shift; + my $reduce = $self->context->{reduction}; + my $l = $self->{lop}; + my $r = $self->{rop}; + + return $self unless ($l->{isConstant} || $r->{isConstant}); + + if ($l->{isConstant}) { + return $l->eval->value ? ($reduce->{'x||1'} ? $l : $self) : ($reduce->{'x||0'} ? $r : $self); + } else { + return $r->eval->value ? ($reduce->{'x||1'} ? $r : $self) : ($reduce->{'x||0'} ? $l : $self); + } +} + +package context::Boolean::BOP::and; +our @ISA = qw(context::Boolean::BOP); + +sub _eval { + my ($self, $l, $r) = @_; + return ($l->value && $r->value ? $self->context->T : $self->context->F); +} + +sub _reduce { + my $self = shift; + my $reduce = $self->context->{reduction}; + my $l = $self->{lop}; + my $r = $self->{rop}; + + return $self unless ($l->{isConstant} || $r->{isConstant}); + + if ($l->{isConstant}) { + return $l->eval->value ? ($reduce->{'x&&1'} ? $r : $self) : ($reduce->{'x&&0'} ? $l : $self); + } else { + return $r->eval->value ? ($reduce->{'x&&1'} ? $l : $self) : ($reduce->{'x&&0'} ? $r : $self); + } +} + +package context::Boolean::BOP::xor; +our @ISA = qw(context::Boolean::BOP); + +sub _eval { + my ($self, $l, $r) = @_; + return ($l->value != $r->value ? $self->context->T : $self->context->F); +} + +package context::Boolean::UOP::not; +our @ISA = qw(Parser::UOP); + +sub _check { + my $self = shift; + return if $self->checkNumber; + $self->Error("Operands of '%s' must be 'Boolean'", $self->{uop}); +} + +sub _reduce { + my $self = shift; + my $context = $self->context; + my $reduce = $context->{reduction}; + my $op = $self->{op}; + + if ($op->isNeg && $reduce->{'!!x'}) { + delete $op->{op}{noParens}; + return $op->{op}; + } + + if ($op->{isConstant} && $context->flag('reduceConstants')) { + return $self->Item('Value')->new($self->{equation}, [ 1 - $op->eval ]); + } + return $self; +} + +sub isNeg {1} + +sub _eval { + my ($self, $op) = @_; + return (!($op->value) ? $self->context->T : $self->context->F); +} + +sub perl { + my $self = shift; + my $op = $self->{def}{perl} || $self->{def}{string}; + my $perl = $self->{op}->perl(1) . '->value'; + my $result = "$op $perl"; + return "($result ? context::Boolean->T : context::Boolean->F)"; +} + +package context::Boolean::Boolean; +our @ISA = qw(Value::Real); + +sub new { + my $self = shift; + my $value = $self->SUPER::new(@_); + $value->checkBoolean unless $value->classMatch("Formula"); + return $value; +} + +sub make { + my $self = shift; + my $result = $self->SUPER::make(@_); + $result->checkBoolean unless $result->classMatch("Formula"); + return $result; +} + +sub checkBoolean { + my $self = shift; + $self->Error("Numeric values can only be 1 or 0 in this context") + unless ($self->value == 1 || $self->value == 0); +} + +sub compare { + my ($self, $l, $r) = Value::checkOpOrderWithPromote(@_); + return $l->value <=> $r->value; +} + +# use the context settings +sub string { + my $self = shift; + my $const = $self->context->constants; + my $T = $const->get('T')->{string} // 'T'; + my $F = $const->get('F')->{string} // 'F'; + return ($F, $T)[ $self->value ]; +} + +# use the context settings +sub TeX { + my $self = shift; + my $const = $self->context->constants; + my $T = $const->get('T')->{TeX} // '\top'; + my $F = $const->get('F')->{TeX} // '\bot'; + return ($F, $T)[ $self->value ]; +} + +sub perl { + my $self = shift; + return $self->value ? 'context::Boolean->T' : 'context::Boolean->F'; +} + +sub cmp_defaults { shift->SUPER::cmp_defaults(@_) } + +1; diff --git a/macros/contexts/contextComplexExtras.pl b/macros/contexts/contextComplexExtras.pl index 11aeea2431..300cd3a588 100644 --- a/macros/contexts/contextComplexExtras.pl +++ b/macros/contexts/contextComplexExtras.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextComplexJ.pl b/macros/contexts/contextComplexJ.pl index 01ce146aa6..2c6c37a978 100644 --- a/macros/contexts/contextComplexJ.pl +++ b/macros/contexts/contextComplexJ.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextCongruence.pl b/macros/contexts/contextCongruence.pl index 0b4169ace4..da78b75662 100644 --- a/macros/contexts/contextCongruence.pl +++ b/macros/contexts/contextCongruence.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextCurrency.pl b/macros/contexts/contextCurrency.pl index ed2d9f0d65..1b68d1c9d0 100644 --- a/macros/contexts/contextCurrency.pl +++ b/macros/contexts/contextCurrency.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -250,13 +250,9 @@ sub new { precedence => 10, associativity => $associativity, type => "unary", - string => ( - ($main::displayMode eq 'TeX' or $main::displayMode eq 'PTX') - ? Currency::quoteTeX($symbol) - : $symbol - ), - TeX => Currency::quoteTeX($symbol), - class => 'Currency::UOP::currency' + string => $symbol, + TeX => Currency::quoteTeX($symbol), + class => 'Currency::UOP::currency' }, ); $context->{parser}{Number} = "Currency::Number"; @@ -269,6 +265,7 @@ sub new { forceDecimals => 0, noExtraDecimals => 1, trimTrailingZeros => 0, + legacyTeXStrings => 0, ); $context->{_initialized} = 1; $context->update; @@ -342,7 +339,7 @@ sub addSymbol { $symbol => { %{$def}, associativity => $associativity, - string => ($main::displayMode eq 'TeX' ? Currency::quoteTeX($string) : $string), + string => $string, TeX => Currency::quoteTeX($string), } ); @@ -380,7 +377,7 @@ sub update { $context->operators->set( $data->{symbol} => { associativity => $data->{associativity}, - string => ($main::displayMode eq 'TeX' ? Currency::quoteTeX($string) : $string), + string => $string, TeX => Currency::quoteTeX($string), } ); @@ -526,7 +523,11 @@ sub format { my $currency = ($self->{currency} || $self->context->{currency}); my ($symbol, $comma, $decimal) = ($currency->{symbol}, $currency->{comma}, $currency->{decimal}); $symbol = $self->context->operators->get($symbol)->{$type} || $symbol; - $comma = "{$comma}" if $type eq 'TeX'; + $symbol = Currency::quoteTeX($symbol) + if $self->context->flag('legacyTeXStrings') + && $type eq 'string' + && $main::displayMode eq 'TeX'; + $comma = "{$comma}" if $type eq 'TeX'; my $s = ($self->value >= 0 ? "" : "-"); my $c = main::prfmt(CORE::abs($self->value), "%.2f"); $c =~ s/\.00// if $self->getFlag('trimTrailingZeros'); @@ -537,6 +538,14 @@ sub format { return $c; } +sub stringify { + my $self = shift; + return $self->TeX if $self->context->flag('StringifyAsTeX'); + my $legacy = $self->context->flag('legacyTeXStrings'); + my $string = $self->string; + return $main::displayMode eq 'TeX' && !$legacy ? Currency::quoteTeX($string) : $string; +} + sub string { (shift)->format("string") } sub TeX { (shift)->format("TeX") } diff --git a/macros/contexts/contextFiniteSolutionSets.pl b/macros/contexts/contextFiniteSolutionSets.pl index 942e3ff6a0..ce6bdc0ffc 100644 --- a/macros/contexts/contextFiniteSolutionSets.pl +++ b/macros/contexts/contextFiniteSolutionSets.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -103,6 +103,7 @@ sub _contextFiniteSolutionSets_init { "none" => { alias => 'no real solutions' }, "no solution" => { alias => 'no real solutions' }, "no solutions" => { alias => 'no real solutions' }, + "\x{2205}" => { alias => 'no real solutions' }, #Hack. Investigate making all of this be a constant. "{}" => { alias => 'no real solutions' }, "{ }" => { alias => 'no real solutions' }, diff --git a/macros/contexts/contextForm.pl b/macros/contexts/contextForm.pl index ee2c65b6b9..dbb1ea67cb 100644 --- a/macros/contexts/contextForm.pl +++ b/macros/contexts/contextForm.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -76,6 +76,11 @@ sub _contextForm_init { $context->{cmpDefaults}{Formula}{checker} = sub { my ($correct, $student, $ans) = @_; return 0 if $ans->{isPreview} || $correct != $student; + my $origContext = Context(); + my $newContext = $origContext; + Context($newContext); + $correct = Formula("$correct"); + $student = Formula("$ans->{student_formula}"); $student = $ans->{student_formula}; my $setSqrt = Context()->flag("setSqrt"); my $setRoot = Context()->flag("setRoot"); @@ -98,6 +103,7 @@ sub _contextForm_init { bizarroMul => 0, bizarroDiv => 0 ); + Context($origContext); Value::Error(Context()->flag('wrongFormMessage')) unless $OK; return $OK; }; diff --git a/macros/contexts/contextFraction.pl b/macros/contexts/contextFraction.pl index 3cb7850e1b..dbdcbfe64f 100644 --- a/macros/contexts/contextFraction.pl +++ b/macros/contexts/contextFraction.pl @@ -196,6 +196,10 @@ =head1 DESCRIPTION fraction to lowest terms, and the C method returns true when the fraction is reduced and false otherwise. +Fraction objects also have the C and C methods to return the +numerator and denominator. Note that these will be the unreduced numerator +and denominator when the C is set to 0. + If you wish to convert a fraction to its numeric (real number) form, use the C constructor to coerce it to a real. E.g., @@ -296,59 +300,34 @@ sub Init { main::PG_restricted_eval('sub Fraction {Value->Package("Fraction()")->new(@_)};'); } -# # contFrac($x, $maxdenominator) -# -# Recursive subroutine that takes positive real input $x and outputs -# an array (a,b) where a/b is a very good fraction approximation with -# b no larger than maxdenominator -# - +# Subroutine that takes a positive real input $x and outputs an array +# (a,b) where a/b is a very good fraction approximation with b no +# larger than maxdenominator. sub contFrac { - my $x = shift; - my $maxdenominator = shift; - my %sequences = @_; # an => continued fraction sequence (reference) - # hn => sequence of numerators (reference) - # kn => sequence of denominators (reference) - - # dereference sequences - my @an = (int($x)); - @an = @{ $sequences{"an"} } if defined($sequences{"an"}); - my @hn = (int($x)); - @hn = @{ $sequences{"hn"} } if defined($sequences{"hn"}); - my @kn = (1); - @kn = @{ $sequences{"kn"} } if defined($sequences{"kn"}); - - # calculate what real the continued fraciton process leaves at this level + my ($x, $maxdenominator) = @_; + my $step = $x; - for my $i (0 .. $#an - 1) { $step = ($step - $an[$i])**(-1); } - # if this is an integer, stop - if ($step == int($step)) { return ($hn[-1], $kn[-1]); } - - $step = ($step - $an[-1])**(-1); - - # next integer from continued fraction sequence - # next numerator and denominator, according to continued fraction formulas - my $newa = int($step); - my $newh; - my $newk; - if ($#an > 0) { $newh = $newa * $hn[-1] + $hn[-2]; } - else { $newh = $newa * $an[0] + 1; } - if ($#an > 0) { $newk = $newa * $kn[-1] + $kn[-2]; } - else { $newk = $newa; } - - # machine rounding error may begin to make denominators skyrocket out of control - if ($newk > $maxdenominator) { return ($hn[-1], $kn[-1]); } - - #otherwise, create sequence references and pass one level deeper - @an = (@an, $newa); - @hn = (@hn, $newh); - @kn = (@kn, $newk); - my $anref = \@an; - my $hnref = \@hn; - my $knref = \@kn; - return contFrac($x, $maxdenominator, an => $anref, hn => $hnref, kn => $knref); + my $n = int($step); + my ($h0, $h1, $k0, $k1) = (1, $n, 0, 1); + + # End when $step is an integer. + while ($step != $n) { + $step = 1 / ($step - $n); + + # Compute the next integer from the continued fraction sequence. + $n = int($step); + # Compute the next numerator and denominator according to the continued fraction formulas. + my ($newh, $newk) = ($n * $h1 + $h0, $n * $k1 + $k0); + + # Machine rounding error may begin to make denominators skyrocket out of control + last if ($newk > $maxdenominator); + + ($h0, $h1, $k0, $k1) = ($h1, $newh, $k1, $newk); + } + + return ($h1, $k1); } # @@ -934,6 +913,14 @@ sub isReduced { return $a == $c && $b == $d; } +sub num { + return (shift->value)[0]; +} + +sub den { + return (shift->value)[1]; +} + ################################################## # # Formatting diff --git a/macros/contexts/contextInequalities.pl b/macros/contexts/contextInequalities.pl index 66721205fc..6f3d134e4f 100644 --- a/macros/contexts/contextInequalities.pl +++ b/macros/contexts/contextInequalities.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextInequalitiesAllowStrings.pl b/macros/contexts/contextInequalitiesAllowStrings.pl index 1ef5d89600..fe29c5d147 100644 --- a/macros/contexts/contextInequalitiesAllowStrings.pl +++ b/macros/contexts/contextInequalitiesAllowStrings.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextInequalitySetBuilder.pl b/macros/contexts/contextInequalitySetBuilder.pl index 72799d90e0..d93e18ea39 100644 --- a/macros/contexts/contextInequalitySetBuilder.pl +++ b/macros/contexts/contextInequalitySetBuilder.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -155,7 +155,8 @@ sub UseVerticalSuchThat { type => "bin", string => " | ", TeX => ' \mid ', - class => "InequalitySetBuilder::BOP::suchthat" + class => "InequalitySetBuilder::BOP::suchthat", + alternatives => ["\x{2223}"] }, "_suchthat" => { alias => "|", hidden => 1 }, ); diff --git a/macros/contexts/contextInteger.pl b/macros/contexts/contextInteger.pl index 7693795d5b..86b2613807 100644 --- a/macros/contexts/contextInteger.pl +++ b/macros/contexts/contextInteger.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextIntegerFunctions.pl b/macros/contexts/contextIntegerFunctions.pl index 453ae3976e..f0aeafbbf5 100644 --- a/macros/contexts/contextIntegerFunctions.pl +++ b/macros/contexts/contextIntegerFunctions.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextLimitedComplex.pl b/macros/contexts/contextLimitedComplex.pl index e1f7b275b0..fbb3e04386 100644 --- a/macros/contexts/contextLimitedComplex.pl +++ b/macros/contexts/contextLimitedComplex.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextLimitedNumeric.pl b/macros/contexts/contextLimitedNumeric.pl index 5ffc41c23f..3541eb11e3 100644 --- a/macros/contexts/contextLimitedNumeric.pl +++ b/macros/contexts/contextLimitedNumeric.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextLimitedPoint.pl b/macros/contexts/contextLimitedPoint.pl index b283fb907a..fad6617f73 100644 --- a/macros/contexts/contextLimitedPoint.pl +++ b/macros/contexts/contextLimitedPoint.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextLimitedPolynomial.pl b/macros/contexts/contextLimitedPolynomial.pl index 546dfc1f6f..58c3fdf3a7 100644 --- a/macros/contexts/contextLimitedPolynomial.pl +++ b/macros/contexts/contextLimitedPolynomial.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextLimitedPowers.pl b/macros/contexts/contextLimitedPowers.pl index 32a108d600..e3b65c2c4a 100644 --- a/macros/contexts/contextLimitedPowers.pl +++ b/macros/contexts/contextLimitedPowers.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextLimitedRadical.pl b/macros/contexts/contextLimitedRadical.pl index 8c7bbd291f..49569d2ecd 100644 --- a/macros/contexts/contextLimitedRadical.pl +++ b/macros/contexts/contextLimitedRadical.pl @@ -128,6 +128,20 @@ sub _contextLimitedRadical_init { }; } +########################### +# +# Convenience +# +# Pass $a,$b, get Formula("$a sqrt($b)") but simplified + +sub preprad { + my ($a, $b) = @_; + return Formula("0") if $a == 0 || $b == 0; + return Formula("$a") if $b == 1; + my $simplifieda = abs($a) == 1 ? ($a > 0 ? '' : '-') : $a; + return Formula("$simplifieda sqrt($b)"); +} + ########################### # # Create root(n, x) diff --git a/macros/contexts/contextLimitedRadicalComplex.pl b/macros/contexts/contextLimitedRadicalComplex.pl index 394d5fd93f..15202a58c4 100644 --- a/macros/contexts/contextLimitedRadicalComplex.pl +++ b/macros/contexts/contextLimitedRadicalComplex.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextLimitedVector.pl b/macros/contexts/contextLimitedVector.pl index a16c7cefb2..0f752800cb 100644 --- a/macros/contexts/contextLimitedVector.pl +++ b/macros/contexts/contextLimitedVector.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextMatrixExtras.pl b/macros/contexts/contextMatrixExtras.pl index 29e5164260..c0b6dd0f2b 100644 --- a/macros/contexts/contextMatrixExtras.pl +++ b/macros/contexts/contextMatrixExtras.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextPartition.pl b/macros/contexts/contextPartition.pl index a16ed4dda9..e5d58d57ec 100644 --- a/macros/contexts/contextPartition.pl +++ b/macros/contexts/contextPartition.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextPercent.pl b/macros/contexts/contextPercent.pl index 73e346d66b..a7dcc4139f 100644 --- a/macros/contexts/contextPercent.pl +++ b/macros/contexts/contextPercent.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextPeriodic.pl b/macros/contexts/contextPeriodic.pl index 88ff35c17a..f818a80b2d 100644 --- a/macros/contexts/contextPeriodic.pl +++ b/macros/contexts/contextPeriodic.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextPermutation.pl b/macros/contexts/contextPermutation.pl index 2e67ff58aa..88c46d1c2e 100644 --- a/macros/contexts/contextPermutation.pl +++ b/macros/contexts/contextPermutation.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextPermutationUBC.pl b/macros/contexts/contextPermutationUBC.pl index dc55b0db93..c15682f669 100644 --- a/macros/contexts/contextPermutationUBC.pl +++ b/macros/contexts/contextPermutationUBC.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextPiecewiseFunction.pl b/macros/contexts/contextPiecewiseFunction.pl index 56428320ad..73600fb880 100644 --- a/macros/contexts/contextPiecewiseFunction.pl +++ b/macros/contexts/contextPiecewiseFunction.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextPolynomialFactors.pl b/macros/contexts/contextPolynomialFactors.pl index 918a79fd5a..35ae5f9853 100644 --- a/macros/contexts/contextPolynomialFactors.pl +++ b/macros/contexts/contextPolynomialFactors.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextRationalFunction.pl b/macros/contexts/contextRationalFunction.pl index 362537f14e..b04892b45c 100644 --- a/macros/contexts/contextRationalFunction.pl +++ b/macros/contexts/contextRationalFunction.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextRestrictedDomains.pl b/macros/contexts/contextRestrictedDomains.pl index e433dedd8c..5ecb9b836a 100644 --- a/macros/contexts/contextRestrictedDomains.pl +++ b/macros/contexts/contextRestrictedDomains.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextScientificNotation.pl b/macros/contexts/contextScientificNotation.pl index 72242d0725..f75acb3650 100644 --- a/macros/contexts/contextScientificNotation.pl +++ b/macros/contexts/contextScientificNotation.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextString.pl b/macros/contexts/contextString.pl index 62cdb269df..b19c8c4ee7 100644 --- a/macros/contexts/contextString.pl +++ b/macros/contexts/contextString.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextTF.pl b/macros/contexts/contextTF.pl index 00e39768a8..49bb5c1eb7 100644 --- a/macros/contexts/contextTF.pl +++ b/macros/contexts/contextTF.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/contexts/contextTrigDegrees.pl b/macros/contexts/contextTrigDegrees.pl index 60c13de378..60165f2495 100644 --- a/macros/contexts/contextTrigDegrees.pl +++ b/macros/contexts/contextTrigDegrees.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/core/MathObjects.pl b/macros/core/MathObjects.pl index fdb677cbd4..6d3d830f20 100644 --- a/macros/core/MathObjects.pl +++ b/macros/core/MathObjects.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/core/PGML.pl b/macros/core/PGML.pl index 1025b47dd6..1f82ace885 100644 --- a/macros/core/PGML.pl +++ b/macros/core/PGML.pl @@ -28,6 +28,11 @@ sub Warning { sub Sort { return main::lex_sort(@_) } +sub isRegexp { + my $ref = shift; + return $ref && ref($ref) =~ m/Regexp$/; +} + ###################################################################### package PGML::Parse; @@ -43,12 +48,12 @@ package PGML::Parse; my $align = '>> *| *<<'; my $code = '```'; my $pre = ': '; -my $quoted = '[$@%]q[qr]?|\bq[qr]?\s+(?=.)|\bq[qr]?(?=\W)'; +my $quoted = '[$@%]q[qr]?|\bq[qr]?\s+(?:#.*?(?:\n\s*)+)?(?!=>)(?=.)|\bq[qr]?(?!=>)(?=\W)'; my $emphasis = '\*+|_+'; my $chars = '\\\\.|[{}[\]()\'"]'; my $ansrule = '\[(?:_+|[ox^])\]\*?'; -my $open = '\[(?:[!<%@$]|::?:?|``?`?|\|+ ?)'; -my $close = '(?:[!>%@$]|::?:?|``?`?| ?\|+)\]'; +my $open = '\[(?:[!<%@$#.]|::?:?|``?`?|\|+ ?)'; +my $close = '(?:[!>%@$#.]|::?:?|``?`?| ?\|+)\]'; my $noop = '\[\]'; my $splitPattern = @@ -94,7 +99,7 @@ sub Unwind { $self->{block}->popItem; $self->Text($block->{token}); $self->{block}->pushItem(@{ $block->{stack} }); - $self->Text($block->{terminator}) if $block->{terminator} && ref($block->{terminator}) ne 'Regexp'; + $self->Text($block->{terminator}) if $block->{terminator} && !PGML::isRegexp($block->{terminator}); $self->{atBlockStart} = 0; } @@ -151,7 +156,8 @@ sub Parse { my $token = $self->{split}[ ($self->{i})++ ]; next unless defined $token && $token ne ''; for ($token) { - $block->{terminator} && /^$block->{terminator}\z/ && do { $self->Terminate($token); last }; + $block->{terminator} && /^$block->{terminator}\z/ && do { $self->Terminate($token); last }; + $block->{containerEnd} && /^$block->{containerEnd}\z/ && do { $self->EndContainer($token); last }; /^\[[@\$]/ && ($block->{parseAll} || $block->{parseSubstitutions}) && do { $self->Begin($token); last }; /^\[%/ && ($block->{parseAll} || $block->{parseComments}) && do { $self->Begin($token); last }; /^\\./ && ($block->{parseAll} || $block->{parseSlashes}) && do { $self->Slash($token); last }; @@ -208,8 +214,17 @@ sub Begin { my $options = shift || {}; my $def = { %{ $BlockDefs{$id} }, %$options, token => $token }; my $type = $def->{type}; + my $class = $def->{class} || 'PGML::Block'; delete $def->{type}; - my $block = PGML::Block->new($type, $def); + delete $def->{class}; + my $block = $class->new($type, $def); + my $end = $self->{block}{isContainer} ? $self->{block}{terminator} : $self->{block}{containerEnd}; + $block->{containerEnd} = $end if $end; + + if ($block->{container} && $self->{block}{type} ne $block->{container}) { + PGML::Warning "A $type must appear in a $block->{container}"; + $block->{hasWarning} = 1; + } $self->{block}->pushItem($block); $block->{prev} = $self->{block}; $self->{block} = $block; @@ -222,9 +237,10 @@ sub End { my $action = shift || "paragraph ends"; my $endAt = shift; my $block = $self->{block}; + return if $block->{isContainer}; $block->popItem if $block->topItem->{type} eq 'break' && $block->{type} ne 'align'; while ($block->{type} ne 'root') { - if (ref($block->{terminator}) eq 'Regexp' || $block->{cancelPar}) { + if (PGML::isRegexp($block->{terminator}) || $block->{cancelPar}) { $self->blockError("'%s' was not closed before $action"); } else { $self->Terminate; @@ -247,7 +263,7 @@ sub Terminate { foreach my $field ( "prev", "parseComments", "parseSubstitutions", "parseSlashes", "parseAll", "cancelUnbalanced", "cancelNL", "cancelPar", - "balance", "terminateMethod", "noIndent" + "balance", "terminateMethod", "noIndent", "ignoreIndent" ) { delete $block->{$field}; @@ -259,6 +275,15 @@ sub Terminate { } } +sub EndContainer { + my $self = shift; + my $token = shift; + while (!$self->{block}{isContainer}) { + $self->Terminate($token); + } + $self->Terminate($token); +} + sub Unbalanced { my $self = shift; my $token = shift; @@ -334,6 +359,7 @@ sub Par { sub Indent { my $self = shift; my $token = shift; + return if $self->{block}{ignoreIndent}; if ($self->{atLineStart}) { my $tabs = $token; $tabs =~ s/ /\t/g; # turn spaces into tabs @@ -397,7 +423,7 @@ sub Emphasis { } $block = $block->{prev}; } - if ($self->nextChar(' ') !~ m/\s/ && $self->prevChar(' ') !~ m/[a-z0-9]/i) { + if ($self->nextChar(' ') !~ m/\s/ && ($type eq 'bold' || $self->prevChar(' ') !~ m/[a-z0-9]/i)) { $self->Begin($token, substr($token, 0, 1)); } else { $self->Text($token); @@ -416,8 +442,8 @@ sub Rule { my $self = shift; my $token = shift; if ($self->{atLineStart}) { -### check for line end or braces - $self->Item("rule", $token, { options => [ "width", "size" ] }); + # check for line end or braces + $self->Item("rule", $token, { options => [ "width", "height", "size" ] }); $self->{ignoreNL} = 1; } else { $self->Text($token); @@ -522,13 +548,13 @@ sub Preformatted { sub Quoted { my $self = shift; my $token = shift; - my $next = $self->{split}[ $self->{i} ]; + my $next = $self->{split}[ $self->{i} ] || $self->{split}[ ++$self->{i} ]; my $quote = substr($next, 0, 1); $self->{split}[ $self->{i} ] = substr($next, 1); my $pcount = 0; - my $open = ($quote =~ m/[({[]/ ? $quote : ''); + my $open = ($quote =~ m/[({[<]/ ? $quote : ''); my $close = $open || $quote; - $close =~ tr/({[/)}]/; + $close =~ tr/({[/; my $qclose = "\\$close"; $self->Text($token . $quote); @@ -538,11 +564,12 @@ sub Quoted { $pcount++; } elsif ($open && $text eq $close && $pcount > 0) { $pcount--; - } elsif (!$open || ($text ne $qclose && $pcount == 0)) { + } elsif ($pcount == 0 && $text ne $qclose) { my $i = index($text, $close); if ($i > -1) { $self->Text(substr($text, 0, $i + 1)); - $text = $self->{split}[ $self->{i} ] = substr($text, $i + 1); + $self->{split}[ $self->{i} ] = substr($text, $i + 1); + $self->Text($self->{split}[ $self->{i}++ ]) if $self->{i} % 2; return; } } @@ -567,6 +594,31 @@ sub NOOP { my $balanceAll = qr/[\{\[\'\"]/; %BlockDefs = ( + "[#" => { + type => 'table', + class => 'PGML::Block::Table', + parseAll => 1, + ignoreIndent => 1, + allowPar => 1, + terminator => qr/#\]/, + allowStar => 1, + options => [ qw( + center caption horizontalrules texalignment align Xratio encase rowheaders headerrules + valign padding tablecss captioncss columnscss datacss headercss allcellcss booktabs + ) ] + }, + "[." => { + type => 'table-cell', + parseAll => 1, + isContainer => 1, + container => 'table', + terminator => qr/\.\]/, + allowStar => 1, + options => [ qw( + halign header color bgcolor b i m noencase colspan top bottom + cellcss texpre texpost texencase rowcolor rowcss headerrow rowtop rowbottom valign rows + ) ] + }, "[:" => { type => 'math', parseComments => 1, @@ -635,16 +687,14 @@ sub NOOP { terminator => qr/!\]/, terminateMethod => 'terminateGetString', cancelNL => 1, - options => [ "source", "width", "height" ] + options => [ "source", "width", "height", "image_options" ] }, "[<" => { - type => 'link', - parseComments => 1, - parseSubstitutions => 1, - terminator => qr/>\]/, - terminateMethod => 'terminateGetString', - cancelNL => 1, - options => [ "text", "title" ] + type => 'tag', + parseAll => 1, + isContainer => 1, + terminator => qr/>\]/, + options => [qw(html tex ptx)] }, "[%" => { type => 'comment', parseComments => 1, terminator => qr/%\]/, allowPar => 1 }, "[\@" => { @@ -654,7 +704,7 @@ sub NOOP { parseQuoted => 1, terminator => qr/@\]/, terminateMethod => 'terminateGetString', - balance => qr/[\'\"]/, + balance => qr/[\{\'\"]/, allowStar => 1, allowDblStar => 1, allowTriStar => 1 @@ -887,9 +937,8 @@ sub replaceVariable { my ($result, $error) = PGML::Eval($var); PGML::Warning "Error evaluating variable \$$item->{text}: $error" if $error; $result = "" unless defined $result; - if ($block->{type} eq 'math' && Value::isValue($result)) { - if ($block->{parsed}) { $result = $result->string } - else { $result = '{' . $result->TeX . '}' } + if (Value::isValue($result)) { + $result = ($block->{type} eq 'math' && !$block->{parsed} ? '{' . $result->TeX . '}' : $result->string); } return $result; } @@ -1128,6 +1177,27 @@ sub pushItem { } } +###################################################################### + +package PGML::Block::Table; +our @ISA = ('PGML::Block'); + +sub pushItem { + my $self = shift; + my $item; + while ($item = shift) { + if ($item->{type} eq 'text') { + my $text = join('', @{ $item->{stack} }); + PGML::Warning 'Table text must be in cells' unless $text =~ m/^\s*$/; + } elsif ($item->{type} eq 'table-cell' || $item->{type} eq 'options') { + $self->SUPER::pushItem($item); + } elsif ($item->{type} eq 'comment') { + } else { + PGML::Warning 'Tables can contain only table cells'; + } + } +} + ###################################################################### ###################################################################### @@ -1186,7 +1256,6 @@ sub string { foreach my $item (@{ $block->{stack} }) { $self->{item} = $item; $self->{nl} = (!defined($strings[-1]) || $strings[-1] =~ m/\n$/ ? "" : "\n"); - # warn "type: $item->{type}"; for ($item->{type}) { /indent/ && do { $string = $self->Indent($item); last }; /align/ && do { $string = $self->Align($item); last }; @@ -1210,9 +1279,11 @@ sub string { /break/ && do { $string = $self->Break($item); last }; /forced/ && do { $string = $self->Forced($item); last }; /comment/ && do { $string = $self->Comment($item); last }; + /table/ && do { $string = $self->Table($item); last }; + /tag/ && do { $string = $self->Tag($item); last }; PGML::Warning "Warning: unknown block type '$item->{type}' in " . ref($self) . "::format\n"; } - push(@strings, $string) unless $string eq ''; + push(@strings, $string) unless (!defined $string || $string eq ''); } $self->{nl} = (!defined($strings[-1]) || $strings[-1] =~ m/\n$/ ? "" : "\n"); return join('', @strings); @@ -1244,6 +1315,33 @@ sub nl { sub Forced { return "" } sub Comment { return "" } +sub Table { + my $self = shift; + my $item = shift; + return "[misplaced $item->{type}]" if $item->{hasWarning}; + my @options; + foreach my $option (@{ $item->{options} || [] }) { + push(@options, $option => $item->{$option}) if defined($item->{$option}); + } + return [ $self->string($item), @options ] if $item->{type} eq 'table-cell'; + my $table = []; + my $row = []; + for my $cell (@{ $item->{stack} }) { + push(@$row, $self->Table($cell)); + if ($cell->{hasStar}) { + push(@$table, $row); + $row = []; + } + } + push(@$table, $row) if @$row; + return ($item->{hasStar} ? main::LayoutTable($table, @options) : main::DataTable($table, @options)); +} + +sub Tag { + my ($self, $item) = @_; + return $self->string($item); +} + sub Math { my $self = shift; my $item = shift; @@ -1260,13 +1358,13 @@ sub Math { my $obj = Parser::Formula($context, $math); if ($context->{error}{flag}) { PGML::Warning "Error parsing mathematics: $context->{error}{message}"; - return "\\text{math error}"; + return ("\\text{math error}", 'inline'); } $obj = $obj->reduce if $item->{reduced}; $math = $obj->TeX; } $math = "\\displaystyle{$math}" if $item->{displaystyle}; - my $mathmode = ($item->{display}) ? 'display' : 'inline'; + my $mathmode = $item->{display} ? 'display' : 'inline'; return ($math, $mathmode); } @@ -1312,7 +1410,15 @@ sub Answer { } $rule = $ans->$method(@options); $rule = PGML::LaTeX($rule); - if (!(ref($ans) eq 'parser::MultiAnswer' && $ans->{part} > 1)) { + my $isMultiAnswer = ref($ans) eq 'parser::MultiAnswer'; + if ($isMultiAnswer) { + $ans->{pgml_cmp} = { cmp => [ $ans->cmp(%{ $item->{cmp_options} }) ] } unless defined $ans->{pgml_cmp}; + if ($ans->{namedRules}) { + main::NAMED_ANS(shift(@{ $ans->{pgml_cmp}{cmp} }), shift(@{ $ans->{pgml_cmp}{cmp} })); + } else { + main::ANS(shift(@{ $ans->{pgml_cmp}{cmp} })) if @{ $ans->{pgml_cmp}{cmp} }; + } + } else { my @cmp = ref($item->{answer}) eq 'AnswerEvaluator' ? $item->{answer} : $ans->cmp(%{ $item->{cmp_options} }); if (defined($item->{name})) { @@ -1361,11 +1467,12 @@ sub Text { sub Image { my ($self, $item) = @_; - my $text = $item->{text}; - my $source = $item->{source}; - my $width = $item->{width} || ''; - my $height = $item->{height} || ''; - return (main::image($source, alt => $text, width => $width, height => $height)); + my $text = $item->{text}; + my $source = $item->{source}; + my $width = $item->{width} || ''; + my $height = $item->{height} || ''; + my $image_options = $item->{image_options} || {}; + return (main::image($source, alt => $text, width => $width, height => $height, %$image_options)); } ###################################################################### @@ -1382,6 +1489,9 @@ sub Escape { $string =~ s//>/g; $string =~ s/"/"/g; + + # Wrap the characters \, `, and $ in span tags to prevent MathJax from processing them. + $string =~ s/([\\`\$]+)/$1<\/span>/g; return $string; } @@ -1498,21 +1608,32 @@ sub Quote { } sub Rule { - my $self = shift; - my $item = shift; - my $width = " width:100%; "; - my $size = ""; - $width = ' width:' . $item->{width} . '; ' if defined $item->{width}; - $size = ' size="' . $item->{size} . '"' if defined $item->{size}; - my $html = ''; - $html = '

                      ' - . '' - . $html - . '' - . '
                      '; # if $width ne '' && $item->{width} !~ m/%/; - return $self->nl . $html . "\n"; + my $self = shift; + my $item = shift; + my $width = '100%;'; + my $height = '1px'; + if (defined $item->{width}) { + $width = $item->{width}; + $width .= 'px' if ($width =~ /^\d*\.?\d+$/); + } + if (defined $item->{height}) { + $height = $item->{height}; + $height .= 'px' if ($height =~ /^\d*\.?\d+$/); + } elsif (defined $item->{size}) { + $height = $item->{size}; + $height .= 'px' if ($height =~ /^\d*\.?\d+$/); + } + return $self->nl + . main::tag( + 'div', + main::tag( + 'span', + style => "width:$width; display:inline-block; margin:0.3em auto", + main::tag( + 'hr', style => "width:$width; height:$height; background-color:currentColor; margin:0.3em auto;" + ) + ) + ); } sub Verbatim { @@ -1528,6 +1649,27 @@ sub Math { return main::general_math_ev3($self->SUPER::Math(@_)); } +sub Tag { + my ($self, $item) = @_; + my %whitelist = (div => 1, span => 1); + my @attributes = ref($item->{html}) eq 'ARRAY' ? @{ $item->{html} } : $item->{html}; + my $tag = @attributes % 2 ? (shift @attributes // 'div') : 'div'; + unless ($whitelist{$tag}) { + PGML::Warning qq{The tag "$tag" is not allowed}; + return $self->string($item); + } + if ($tag eq 'span') { + for my $subblock (@{ $item->{stack} }) { + if ($subblock->{type} =~ /^(indent|align|par|list|bullet|answer|heading|rule|code|pre|verbatim|table|tag)$/) + { + PGML::Warning qq{A "span" tag may not contain a $subblock->{type}}; + return $self->string($item); + } + } + } + return main::tag($tag, @attributes, $self->string($item)); +} + ###################################################################### ###################################################################### @@ -1645,17 +1787,18 @@ sub Quote { } sub Rule { - my $self = shift; - my $item = shift; - my $width = "100%"; - my $size = "1"; - $width = $item->{width} if defined $item->{width}; - $size = $item->{size} if defined $item->{size}; - $width =~ s/%/\\pgmlPercent/; - $size =~ s/%/\\pgmlPercent/; - $width .= "\\pgmlPixels" if $width =~ m/^\d+$/; - $size .= "\\pgmlPixels" if $size =~ m/^\d+$/; - return $self->nl . "\\pgmlRule{$width}{$size}%\n"; + my $self = shift; + my $item = shift; + my $width = "100%"; + my $height = "1"; + $width = $item->{width} if defined $item->{width}; + $height = $item->{size} if defined $item->{size}; + $height = $item->{height} if defined $item->{height}; + $width =~ s/%/\\pgmlPercent/; + $height =~ s/%/\\pgmlPercent/; + $width .= "\\pgmlPixels" if $width =~ m/^\d+$/; + $height .= "\\pgmlPixels" if $height =~ m/^\d+$/; + return $self->nl . "\\pgmlRule{$width}{$height}%\n"; } sub Verbatim { @@ -1671,6 +1814,17 @@ sub Math { return main::general_math_ev3($self->SUPER::Math(@_)); } +sub Tag { + my ($self, $item) = @_; + my ($tex_begin, $tex_end); + if (ref($item->{tex}) eq 'ARRAY') { + ($tex_begin, $tex_end) = @{ $item->{tex} }; + } elsif ($item->{tex}) { + ($tex_begin, $tex_end) = ("\\begin{$item->{tex}}", "\\end{$item->{tex}}"); + } + return ($tex_begin // '') . $self->string($item) . ($tex_end // ''); +} + ###################################################################### ###################################################################### @@ -1818,6 +1972,15 @@ sub Math { return main::general_math_ev3($self->SUPER::Math(@_)); } +sub Tag { + my ($self, $item) = @_; + my @args = ref($item->{ptx}) eq 'ARRAY' ? @{ $item->{ptx} } : $item->{ptx}; + if (my $tag = shift @args) { + return NiceTables::tag($self->string($item), $tag, @args); + } + return $self->string($item); +} + ###################################################################### ###################################################################### @@ -1867,7 +2030,7 @@ package main; sub _PGML_init { PG_restricted_eval('sub PGML {PGML::Format2(@_)}'); - loadMacros("MathObjects.pl"); + loadMacros("MathObjects.pl", "niceTables.pl"); my $context = Context(); # prevent Typeset context from becoming active loadMacros("contextTypeset.pl"); Context($context); diff --git a/macros/core/PGanswermacros.pl b/macros/core/PGanswermacros.pl index 75fc99f3ff..c2dbb469b9 100644 --- a/macros/core/PGanswermacros.pl +++ b/macros/core/PGanswermacros.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -129,8 +129,7 @@ =head1 DESCRIPTION =cut -# ^uses be_strict -BEGIN { be_strict() } +BEGIN { strict->import; } # Until we get the PG cacheing business sorted out, we need to use # PG_restricted_eval to get the correct values for some(?) PG environment diff --git a/macros/core/PGauxiliaryFunctions.pl b/macros/core/PGauxiliaryFunctions.pl index 3292aca603..2295621547 100644 --- a/macros/core/PGauxiliaryFunctions.pl +++ b/macros/core/PGauxiliaryFunctions.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -37,6 +37,7 @@ =head1 DESCRIPTION random_pairwise_coprime($ar1, $ar2, ... ) random_coprime($ar1, $ar2, ... ) random_subset($n, @set) + repeated(@list) =cut # ^uses loadMacros @@ -555,5 +556,32 @@ sub random_subset { return wantarray ? @out : \@out; } +=head2 repeated function + +Usage: C + +This function returns a list of every repeated elements in @list. Comparison is made +using ==, so two elements may be considered 'repeated' even when they are not literally +equal. + +Note that the function will return () if there are no repeated elements, which +is false as a boolean. So !repeated(@list) is a way to check if the list has no repeated +values. + +Also note that generally if two items are equivalent, both will be in the returned list. +However occasionally with MathObjects, x == y is true while y == x is false, and then +only x (the one that makes the relation true when it is on the left) will be included +in the returned list. + +=cut + +sub repeated { + my @return; + for my $x (@_) { + push(@return, $x) if (grep { $_ == $x } (@_)) > 1; + } + return @return; +} + # return 1 so that this file can be included with require 1 diff --git a/macros/core/PGbasicmacros.pl b/macros/core/PGbasicmacros.pl index 793e7b0cbe..3da4761827 100644 --- a/macros/core/PGbasicmacros.pl +++ b/macros/core/PGbasicmacros.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -29,9 +29,7 @@ =head1 DESCRIPTION # this is equivalent to use strict, but can be used within the Safe compartment -BEGIN { - be_strict; -} +BEGIN { strict->import; } my $displayMode; @@ -187,206 +185,189 @@ sub _PGbasicmacros_init { =head2 Answer blank macros: -These produce answer blanks of various sizes or pop up lists or radio answer buttons. -The names for the answer blanks are -generated implicitly. +These produce answer blanks of various sizes or pop up lists or radio answer +buttons. The names for the answer blanks are generated and implicitly +associated with answer evaluators via the C method. - ans_rule( width ) - tex_ans_rule( width ) - ans_radio_buttons(value1 => label1, value2, label2 => value3, label3 => ...) - pop_up_list(@list) # list consists of (value => label, PR => "Product rule", ...) - pop_up_list([@list]) # list consists of values + ans_rule(width) + ans_radio_buttons(value1 => name1, value2, name2 => value3, name3 => ...) + pop_up_list(@list) # list consists of (value => label, PR => "Product rule", ...) + pop_up_list([@list]) # list consists of values In the last case, one can use C to produce a pop-up list containing the three strings listed, and then use str_cmp to check the answer. -To indicate the checked position of radio buttons put a '%' in front of the value: C -will have 'No' checked. C works inside math equations in C mode. It does not work in C mode since this mode produces gif pictures. +To indicate the checked position of radio buttons put a '%' in front of the +value: C will have 'No' checked. -The following method is defined in F for entering the answer evaluators corresponding -to answer rules with automatically generated names. The answer evaluators are matched with the -answer rules in the order in which they appear on the page. +The following method is defined in F for entering the answer evaluators +corresponding to answer rules with automatically generated names. The answer +evaluators are matched with the answer rules in the order in which they appear +on the page. ANS(ans_evaluator1, ans_evaluator2, ...); -These are more primitive macros which produce answer blanks for specialized cases when complete -control over the matching of answers blanks and answer evaluators is desired. -The names of the answer blanks must be generated manually, and it is best if they do NOT begin -with the default answer prefix (currently AnSwEr). - - labeled_ans_rule(name, width) # an alias for NAMED_ANS_RULE where width defaults to 20 if omitted. +These are more primitive macros which produce answer blanks for specialized +cases when complete control over the matching of answers blanks and answer +evaluators is desired. The names of the answer blanks must be generated +manually, and it is best if they do NOT begin with the default answer prefix +(currently AnSwEr). NAMED_ANS_RULE(name, width) - NAMED_ANS_BOX(name, rows, cols) - NAMED_ANS_RADIO(name, value, label) - NAMED_ANS_RADIO_EXTENSION(name, value, label) - NAMED_ANS_RADIO_BUTTONS(name, value1, label1, value2, label2, ...) - check_box('-name' => answer5, '-value' => 'statement3', '-label' => 'I loved this course!') - NAMED_POP_UP_LIST($name, @list) # list consists of (value => tag, PR => "Product rule", ...) - NAMED_POP_UP_LIST($name, [@list]) # list consists of a list of values (and each tag will be set to the corresponding value) - -(Name is the name of the variable, value is the value given to the variable when this option is selected, -and label is the text printed next to the button or check box. Check box variables can have multiple values.) - -NAMED_ANS_RADIO_BUTTONS creates a sequence of NAMED_ANS_RADIO and NAMED_ANS_RADIO_EXTENSION items which -are output either as an array or, in scalar context, as the array glued together with spaces. It is -usually easier to use this than to manually construct the radio buttons by hand. However, sometimes - extra flexibility is desiredin which case: - -When entering radio buttons using the "NAMED" format, you should use NAMED_ANS_RADIO button for the first button -and then use NAMED_ANS_RADIO_EXTENSION for the remaining buttons. NAMED_ANS_RADIO requires a matching answer evalutor, -while NAMED_ANS_RADIO_EXTENSION does not. The name used for NAMED_ANS_RADIO_EXTENSION should match the name -used for NAMED_ANS_RADIO (and the associated answer evaluator). - -The following method is defined in F for entering the answer evaluators corresponding -to answer rules with automatically generated names. The answer evaluators are matched with the -answer rules in the order in which they appear on the page. + labeled_ans_rule(name, width) # alias for NAMED_ANS_RULE + NAMED_ANS_BOX(name, rows, cols) + NAMED_ANS_RADIO(name, value, name) + NAMED_ANS_RADIO_EXTENSION(name, value, name) + NAMED_ANS_RADIO_BUTTONS(name, value1, name1, value2, name2, ...) + NAMED_POP_UP_LIST($name, @list) # list consists of (value => tag, PR => "Product rule", ...) + NAMED_POP_UP_LIST($name, [@list]) # list consists of a list of values + # (and each tag will be set to the corresponding value) + +(Name is the name of the input, value is the value given to the input when +this option is selected, and label is the text printed next to the button or +check box. Check box variables can have multiple values.) + +NAMED_ANS_RADIO_BUTTONS creates a sequence of NAMED_ANS_RADIO and +NAMED_ANS_RADIO_EXTENSION items which are output either as an array or, in +scalar context, as the array glued together with spaces. It is usually easier +to use this than to manually construct the radio buttons by hand. However, +sometimes extra flexibility is desiredin which case: + +When entering radio buttons using the "NAMED" format, you should use +NAMED_ANS_RADIO button for the first button and then use +NAMED_ANS_RADIO_EXTENSION for the remaining buttons. NAMED_ANS_RADIO requires a +matching answer evalutor, while NAMED_ANS_RADIO_EXTENSION does not. The name +used for NAMED_ANS_RADIO_EXTENSION should match the name used for +NAMED_ANS_RADIO (and the associated answer evaluator). + +The following method is defined for entering the answer evaluators corresponding +to answer rules. The answer evaluators are matched with the answer rules in the +order in which they appear on the page. NAMED_ANS(name1 => ans_evaluator1, name2 => ans_evaluator2, ...); -These auxiliary macros are defined in PG.pl +Auxiliary macros defined in PG.pl: + +=over + +=item NEW_ANS_NAME() + +Produces a new anonymous answer blank name by appending a number to the prefix +(AnSwEr). + +=item ANS_NUM_TO_NAME(number) + +Prepends the prefix (AnSwEr) to the number, but does nothing else. - NEW_ANS_NAME( ); # produces a new anonymous answer blank name by appending a number to the prefix (AnSwEr) - # and registers this name as an implicitly labeled answer - # Its use is paired with each answer evaluator being entered using ANS() +=item RECORD_ANS_NAME(name) - ANS_NUM_TO_NAME(number); # prepends the prefix (AnSwEr) to the number, but does nothing else. +Records the order in which the answer blank is rendered. All answer rules must +be recorded by this method. All named answer rule methods in this macro do this. +Most answer rules created elsewhere call a named answer rule method in this +macro to handle this. - RECORD_ANS_NAME( name ); # records the order in which the answer blank is rendered - # This is called by all of the constructs above, but must - # be called explicitly if an input blank is constructed explictly - # using HTML code. +=item RECORD_IMPLICIT_ANS_NAME(name) -These are legacy macros: +Records answer names which are to be implicitly associated with an answer This +is called by the internal answer rule methods, but must be called for all answer +rules constructed elsewhere as well. After this is called C +must be called as well. Usually the appropriate named answer rule method should +be called which will do this. - ANS_RULE( number, width ); # equivalent to NAMED_ANS_RULE( NEW_ANS_NAME( ), width) - ANS_BOX( question_number, height, width ); # equivalent to NAMED_ANS_BOX( NEW_ANS_NAME( ), height, width) - ANS_RADIO( question_number, value, tag ); # equivalent to NAMED_ANS_RADIO( NEW_ANS_NAME( ), value, tag) - ANS_RADIO_OPTION( question_number, value, tag ); # equivalent to NAMED_ANS_RADIO_EXTENSION( ANS_NUM_TO_NAME(number), value, tag) +=back + +Deprecated macro (still used by many problems): + + ANS_RULE(number, width); # equivalent to ans_rule(width) -- number is ignored =cut -sub labeled_ans_rule { # syntactic sugar for NAMED_ANS_RULE - my ($name, $col) = @_; - $col = 20 unless not_null($col); - NAMED_ANS_RULE($name, $col); +# Alias for NAMED_ANS_RULE +sub labeled_ans_rule { + my ($name, $col, %options) = @_; + return NAMED_ANS_RULE($name, $col, %options); } sub NAMED_ANS_RULE { - my $name = shift; - my $col = shift; - my %options = @_; - $col = 20 unless not_null($col); - my $answer_value = ''; - $answer_value = ${$inputs_ref}{$name} if defined(${$inputs_ref}{$name}); - - #FIXME -- code factoring needed - if ($answer_value =~ /\0/) { - my @answers = split("\0", $answer_value); - $answer_value = shift(@answers); # use up the first answer - $rh_sticky_answers->{$name} = \@answers; - # store the rest -- beacuse this stores to a main:: variable - # it must be evaluated at run time - $answer_value = '' unless defined($answer_value); - } elsif (ref($answer_value) eq 'ARRAY') { - my @answers = @{$answer_value}; - $answer_value = shift(@answers); # use up the first answer - $rh_sticky_answers->{$name} = \@answers; - # store the rest -- because this stores to a main:: variable - # it must be evaluated at run time - $answer_value = '' unless defined($answer_value); + my ($name, $col, %options) = @_; + $col ||= 20; + my $answer_value = $inputs_ref->{$name} // ''; + $answer_value = [ split("\0", $answer_value) ] if $answer_value =~ /\0/; + + if (ref($answer_value) eq 'ARRAY') { + my @answers = @$answer_value; + $answer_value = shift(@answers) // ''; # Use up the first answer. + $rh_sticky_answers->{$name} = \@answers; # Store the rest. } - $answer_value =~ s/\s+/ /g; ## remove excessive whitespace from student answer - $name = RECORD_ANS_NAME($name, $answer_value); - $answer_value = encode_pg_and_html($answer_value); + $name = RECORD_ANS_NAME($name, $answer_value); my $previous_name = "previous_$name"; $name = ($envir{use_opaque_prefix}) ? "%%IDPREFIX%%$name" : $name; $previous_name = ($envir{use_opaque_prefix}) ? "%%IDPREFIX%%$previous_name" : $previous_name; - my $label; - if (defined($options{aria_label})) { - $label = $options{aria_label}; - } else { - $label = generate_aria_label($name); - } - - my $tcol = $col / 2 > 3 ? $col / 2 : 3; ## get max - $tcol = $tcol < 40 ? $tcol : 40; ## get min + my $tcol = $col / 2 > 3 ? $col / 2 : 3; # get max + $tcol = $tcol < 40 ? $tcol : 40; # get min - MODES( - TeX => "\\relax{\\answerRule[$name]{$tcol}}", - Latex2HTML => qq!\\begin{rawhtml}\\end{rawhtml}!, - - # Note: codeshard is used in the css to identify input elements that come from pg - HTML => qq!! - . qq!!, + return MODES( + TeX => "{\\answerRule[$name]{$tcol}}", + # Note: codeshard is used in the css to identify input elements that come from pg. + HTML => tag( + 'div', + class => 'text-nowrap d-inline', + tag( + 'input', + type => 'text', + class => 'codeshard', + size => $col, + name => $name, + id => $name, + aria_label => $options{aria_label} // generate_aria_label($name), + dir => 'auto', + autocomplete => 'off', + autocapitalize => 'off', + spellcheck => 'false', + value => $answer_value + ) + ) + . tag('input', type => 'hidden', name => $previous_name, value => $answer_value), PTX => qq!! ); } sub NAMED_HIDDEN_ANS_RULE { - # this is used to hold information being passed into and out of applets - # -- preserves state -- identical to NAMED_ANS_RULE except input type "hidden" my ($name, $col) = @_; - $col = 20 unless not_null($col); - my $answer_value = ''; - $answer_value = ${$inputs_ref}{$name} if defined(${$inputs_ref}{$name}); - if ($answer_value =~ /\0/) { - my @answers = split("\0", $answer_value); - $answer_value = shift(@answers); # use up the first answer - $rh_sticky_answers->{$name} = \@answers; - # store the rest -- beacuse this stores to a main:: variable - # it must be evaluated at run time - $answer_value = '' unless defined($answer_value); - } elsif (ref($answer_value) eq 'ARRAY') { - my @answers = @{$answer_value}; - $answer_value = shift(@answers); # use up the first answer - $rh_sticky_answers->{$name} = \@answers; - # store the rest -- beacuse this stores to a main:: variable - # it must be evaluated at run time - $answer_value = '' unless defined($answer_value); + $col ||= 20; + my $answer_value = $inputs_ref->{$name} // ''; + $answer_value = [ split("\0", $answer_value) ] if $answer_value =~ /\0/; + + if (ref($answer_value) eq 'ARRAY') { + my @answers = @$answer_value; + $answer_value = shift(@answers) // ''; # Use up the first answer. + $rh_sticky_answers->{$name} = \@answers; # Store the rest. } - $answer_value =~ s/\s+/ /g; ## remove excessive whitespace from student answer + $answer_value =~ s/\s+/ /g; # Remove excessive whitespace from student answer. - $name = RECORD_ANS_NAME($name, $answer_value); - $answer_value = encode_pg_and_html($answer_value); + $name = RECORD_ANS_NAME($name, $answer_value); - my $tcol = $col / 2 > 3 ? $col / 2 : 3; ## get max - $tcol = $tcol < 40 ? $tcol : 40; ## get min + my $tcol = $col / 2 > 3 ? $col / 2 : 3; # get max + $tcol = $tcol < 40 ? $tcol : 40; # get min - MODES( - TeX => "\\relax{\\answerRule[$name]{$tcol}}", - Latex2HTML => qq!\\begin{rawhtml}\\end{rawhtml}!, - HTML => qq!! - . qq!!, + return MODES( + TeX => "{\\answerRule[$name]{$tcol}}", + HTML => tag('input', type => 'hidden', name => $name, id => $name, value => $answer_value) + . tag('input', type => 'hidden', name => "previous_$name", id => "previous_$name", value => $answer_value), PTX => '', ); } -sub NAMED_ANS_RULE_OPTION { # deprecated - &NAMED_ANS_RULE_EXTENSION; -} - sub NAMED_ANS_RULE_EXTENSION { - my $name = shift; # this is the name of the response item - my $col = shift; - my %options = @_; + my ($name, $col, %options) = @_; - my $label; - if (defined($options{aria_label})) { - $label = $options{aria_label}; - } else { - $label = generate_aria_label($name); - } # $answer_group_name is the name of the parent answer group # the group name is usually the same as the answer blank name # when there is only one answer blank. - my $answer_group_name = $options{answer_group_name} // ''; unless ($answer_group_name) { WARN_MESSAGE( @@ -395,78 +376,76 @@ sub NAMED_ANS_RULE_EXTENSION { usually the same as the answer blank name. Answer blank name: $name" ); } - # warn "from named answer rule extension in PGbasic answer_group_name: |$answer_group_name|"; - my $answer_value = ''; - $answer_value = ${$inputs_ref}{$name} if defined(${$inputs_ref}{$name}); - if (defined($rh_sticky_answers->{$name})) { - $answer_value = shift(@{ $rh_sticky_answers->{$name} }); - $answer_value = '' unless defined($answer_value); + + my $answer_value = $inputs_ref->{$name} // ''; + if (defined $rh_sticky_answers->{$name}) { + $answer_value = shift(@{ $rh_sticky_answers->{$name} }) // ''; } - $answer_value =~ s/\s+/ /g; ## remove excessive whitespace from student answer - # warn "from NAMED_ANSWER_RULE_EXTENSION in PGbasic: - # answer_group_name: |$answer_group_name| name: |$name| answer value: |$answer_value|"; - INSERT_RESPONSE($answer_group_name, $name, $answer_value) - ; #FIXME hack -- this needs more work to decide how to make it work - $answer_value = encode_pg_and_html($answer_value); + $answer_value =~ s/\s+/ /g; # remove excessive whitespace from student answer - my $tcol = $col / 2 > 3 ? $col / 2 : 3; ## get max - $tcol = $tcol < 40 ? $tcol : 40; ## get min - MODES( - TeX => "\\relax{\\answerRule[$name]{$tcol}}", - Latex2HTML => - qq!\\begin{rawhtml}\n\n\\end{rawhtml}\n!, - HTML => qq!! - . qq!!, + INSERT_RESPONSE($answer_group_name, $name, $answer_value); + + my $tcol = $col / 2 > 3 ? $col / 2 : 3; # get max + $tcol = $tcol < 40 ? $tcol : 40; # get min + + return MODES( + TeX => "{\\answerRule[$name]{$tcol}}", + HTML => tag( + 'input', + type => 'text', + class => 'codeshard', + size => $col, + name => $name, + id => $name, + aria_label => $options{aria_label} // generate_aria_label($name), + dir => 'auto', + autocomplete => 'off', + autocapitalize => 'off', + spellcheck => 'false', + value => $answer_value + ) + . tag('input', type => 'hidden', name => "previous_$name", id => "previous_$name", value => $answer_value), PTX => qq!!, ); } -sub ANS_RULE { #deprecated +# Deprecated +sub ANS_RULE { my ($number, $col) = @_; - my $name = NEW_ANS_NAME($number); - NAMED_ANS_RULE($name, $col); + my $name = NEW_ANS_NAME(); + RECORD_IMPLICIT_ANS_NAME($name); + return NAMED_ANS_RULE($name, $col); } sub NAMED_ANS_BOX { - my $name = shift; - my $row = shift; - my $col = shift; - my %options = @_; - - $row = 10 unless defined($row); - $col = 80 unless defined($col); + my ($name, $row, $col, %options) = @_; + $row //= 10; + $col //= 80; my $height = .07 * $row; - my $answer_value = ''; - $answer_value = $inputs_ref->{$name} if defined($inputs_ref->{$name}); - $name = RECORD_ANS_NAME($name, $answer_value); - my $label; - if (defined($options{aria_label})) { - $label = $options{aria_label}; - } else { - $label = generate_aria_label($name); - } - # try to escape HTML entities to deal with xss stuff - $answer_value = encode_pg_and_html($answer_value); - my $out = MODES( - TeX => qq!\\vskip $height in \\hrulefill\\quad !, - Latex2HTML => qq!\\begin{rawhtml}\\end{rawhtml}!, - HTML => qq!! - . qq!!, + my $answer_value = $inputs_ref->{$name} // ''; + $name = RECORD_ANS_NAME($name, $answer_value); + my $label = $options{aria_label} // generate_aria_label($name); + + return MODES( + TeX => qq!\\vskip $height in \\hrulefill\\quad !, + HTML => tag( + 'div', + class => 'text-nowrap d-inline', + tag( + 'textarea', + name => $name, + id => $name, + rows => $row, + cols => $col, + aria_label => $label, + encode_pg_and_html($answer_value) + ) + ) + . tag('input', type => 'hidden', name => "previous_$name", value => $answer_value), PTX => '', ); - $out; -} - -sub ANS_BOX { #deprecated - my ($number, $row, $col) = @_; - my $name = NEW_ANS_NAME(); - NAMED_ANS_BOX($name, $row, $col); } sub NAMED_ANS_RADIO { @@ -483,9 +462,7 @@ sub NAMED_ANS_RADIO { INSERT_RESPONSE($options{answer_group_name}, $name, { $value => $checked }) if $extend; return MODES( - TeX => qq!\\item{$tag}\n!, - Latex2HTML => - qq!\\begin{rawhtml}\n\\end{rawhtml}$tag!, + TeX => qq!\\item{$tag}\n!, HTML => tag( 'label', tag( @@ -502,11 +479,6 @@ sub NAMED_ANS_RADIO { ), PTX => '
                    • ' . "$tag" . '
                    • ' . "\n", ); - -} - -sub NAMED_ANS_RADIO_OPTION { #deprecated - &NAMED_ANS_RADIO_EXTENSION; } sub NAMED_ANS_RADIO_EXTENSION { @@ -522,9 +494,7 @@ sub NAMED_ANS_RADIO_EXTENSION { EXTEND_RESPONSE($options{answer_group_name} // $name, $name, $value, $checked); return MODES( - TeX => qq!\\item{$tag}\n!, - Latex2HTML => - qq!\\begin{rawhtml}\n\\end{rawhtml}$tag!, + TeX => qq!\\item{$tag}\n!, HTML => tag( 'label', tag( @@ -541,63 +511,27 @@ sub NAMED_ANS_RADIO_EXTENSION { ), PTX => '
                    • ' . "$tag" . '
                    • ' . "\n", ); - } sub NAMED_ANS_RADIO_BUTTONS { - my $name = shift; - my $value = shift; - my $tag = shift; + my ($name, $value, $tag, @buttons) = @_; - my @out = (); + my @out; push(@out, NAMED_ANS_RADIO($name, $value, $tag)); - my @buttons = @_; - my $label = generate_aria_label($name); - my $count = 2; + my $label = generate_aria_label($name); + my $count = 2; while (@buttons) { $value = shift @buttons; $tag = shift @buttons; - push(@out, NAMED_ANS_RADIO_OPTION($name, $value, $tag, aria_label => $label . "option $count ")); + push(@out, NAMED_ANS_RADIO_EXTENSION($name, $value, $tag, aria_label => $label . "option $count ")); $count++; } - (wantarray) ? @out : join(" ", @out); -} - -sub ANS_RADIO { - my $number = shift; - my $value = shift; - my $tag = shift; - my $name = NEW_ANS_NAME(); - NAMED_ANS_RADIO($name, $value, $tag); -} - -sub ANS_RADIO_OPTION { - my $number = shift; - my $value = shift; - my $tag = shift; - my $name = ANS_NUM_TO_NAME($number); - NAMED_ANS_RADIO_OPTION($name, $value, $tag); -} - -sub ANS_RADIO_BUTTONS { - my $number = shift; - my $value = shift; - my $tag = shift; - - my @out = (); - push(@out, ANS_RADIO($number, $value, $tag)); - my @buttons = @_; - while (@buttons) { - $value = shift @buttons; - $tag = shift @buttons; - push(@out, ANS_RADIO_OPTION($number, $value, $tag)); - } - (wantarray) ? @out : join(" ", @out); + return wantarray ? @out : join(" ", @out); } ############################################## # generate_aria_label( $name ) -# takes the name of an ANS_RULE or ANS_BOX and generates an appropriate +# takes the name of an ANS_RULE and generates an appropriate # aria label for screen readers ############################################## @@ -606,7 +540,7 @@ sub generate_aria_label { my $label = ''; # if we dont have an AnSwEr type name then we do the best we can - if ($name !~ /AnSwEr/) { + if ($name !~ /AnSwEr\d+/) { return maketext('answer') . ' ' . $name; } @@ -682,10 +616,7 @@ sub NAMED_ANS_CHECKBOX { INSERT_RESPONSE($options{answer_group_name}, $name, { $value => $checked }) if $extend; return MODES( - TeX => qq!\\item{$tag}\n!, - Latex2HTML => qq!\\begin{rawhtml}\n! - . qq!! - . qq!\\end{rawhtml}$tag!, + TeX => qq!\\item{$tag}\n!, HTML => tag( 'label', tag( @@ -702,7 +633,6 @@ sub NAMED_ANS_CHECKBOX { ), PTX => "
                    • $tag
                    • \n", ); - } sub NAMED_ANS_CHECKBOX_OPTION { @@ -718,10 +648,7 @@ sub NAMED_ANS_CHECKBOX_OPTION { EXTEND_RESPONSE($options{answer_group_name} // $name, $name, $value, $checked); return MODES( - TeX => qq!\\item{$tag}\n!, - Latex2HTML => qq!\\begin{rawhtml}\n! - . qq!! - . qq!\\end{rawhtml}$tag!, + TeX => qq!\\item{$tag}\n!, HTML => tag( 'label', tag( @@ -741,15 +668,12 @@ sub NAMED_ANS_CHECKBOX_OPTION { } sub NAMED_ANS_CHECKBOX_BUTTONS { - my $name = shift; - my $value = shift; - my $tag = shift; + my ($name, $value, $tag, @buttons) = @_; - my @out = (); + my @out; push(@out, NAMED_ANS_CHECKBOX($name, $value, $tag)); - my $label = generate_aria_label($name); - my $count = 2; - my @buttons = @_; + my $label = generate_aria_label($name); + my $count = 2; while (@buttons) { $value = shift @buttons; $tag = shift @buttons; @@ -757,62 +681,19 @@ sub NAMED_ANS_CHECKBOX_BUTTONS { $count++; } - (wantarray) ? @out : join(" ", @out); -} - -sub ANS_CHECKBOX { - my $number = shift; - my $value = shift; - my $tag = shift; - my $name = NEW_ANS_NAME(); - - NAMED_ANS_CHECKBOX($name, $value, $tag); -} - -sub ANS_CHECKBOX_OPTION { - my $number = shift; - my $value = shift; - my $tag = shift; - my $name = ANS_NUM_TO_NAME($number); - - NAMED_ANS_CHECKBOX_OPTION($name, $value, $tag); -} - -sub ANS_CHECKBOX_BUTTONS { - my $number = shift; - my $value = shift; - my $tag = shift; - - my @out = (); - push(@out, ANS_CHECKBOX($number, $value, $tag)); - - my @buttons = @_; - while (@buttons) { - $value = shift @buttons; - $tag = shift @buttons; - push(@out, ANS_CHECKBOX_OPTION($number, $value, $tag)); - } - - (wantarray) ? @out : join(" ", @out); + return wantarray ? @out : join(" ", @out); } sub ans_rule { - my $len = shift; # gives the optional length of the answer blank - $len = 20 unless $len; - my $name = NEW_ANS_NAME(); # increment is done internally - NAMED_ANS_RULE($name, $len); -} - -sub ans_rule_extension { - my $len = shift; - $len = 20 unless $len; - # warn "ans_rule_extension may be misnumbering the answers"; - my $name = NEW_ANS_NAME($$r_ans_rule_count); # don't update the answer name - NAMED_ANS_RULE($name, $len); + my $len = shift; + my $name = NEW_ANS_NAME(); + RECORD_IMPLICIT_ANS_NAME($name); + return NAMED_ANS_RULE($name, $len || 20); } sub ans_radio_buttons { - my $name = NEW_ANS_NAME(); + my $name = NEW_ANS_NAME(); + RECORD_IMPLICIT_ANS_NAME($name); my @radio_buttons = NAMED_ANS_RADIO_BUTTONS($name, @_); if ($displayMode eq 'TeX') { @@ -821,14 +702,20 @@ sub ans_radio_buttons { } elsif ($displayMode eq 'PTX') { $radio_buttons[0] = '' . "\n" . $radio_buttons[0]; $radio_buttons[$#radio_buttons] .= ''; + } else { + $radio_buttons[0] = + qq{
                      $radio_buttons[0]}; + $radio_buttons[-1] .= "
                      "; } - (wantarray) ? @radio_buttons : join(" ", @radio_buttons); + return wantarray ? @radio_buttons : join(" ", @radio_buttons); } -#added 6/14/2000 by David Etlinger sub ans_checkbox { - my $name = NEW_ANS_NAME(); + my $name = NEW_ANS_NAME(); + RECORD_IMPLICIT_ANS_NAME($name); my @checkboxes = NAMED_ANS_CHECKBOX_BUTTONS($name, @_); if ($displayMode eq 'TeX') { @@ -837,92 +724,22 @@ sub ans_checkbox { } elsif ($displayMode eq 'PTX') { $checkboxes[0] = '' . "\n" . $checkboxes[0]; $checkboxes[$#checkboxes] .= ''; + } else { + $checkboxes[0] = + qq{
                      $checkboxes[0]}; + $checkboxes[-1] .= '
                      '; } - (wantarray) ? @checkboxes : join(" ", @checkboxes); -} - -## define a version of ans_rule which will work inside TeX math mode or display math mode -- at least for tth mode. -## This is great for displayed fractions. -## This will not work with latex2HTML mode since it creates gif equations. - -sub tex_ans_rule { - my $len = shift; - $len = 20 unless $len; - my $name = NEW_ANS_NAME(); - my $answer_rule = NAMED_ANS_RULE($name, $len); # we don't want to create three answer rules in different modes. - my $out = MODES( - 'TeX' => $answer_rule, - 'Latex2HTML' => '\\fbox{Answer boxes cannot be placed inside typeset equations}', - 'HTML_tth' => '\\begin{rawhtml} ' . $answer_rule . '\\end{rawhtml}', - 'HTML_dpng' => '\\fbox{Answer boxes cannot be placed inside typeset equations}', - 'HTML' => $answer_rule, - 'PTX' => 'Answer boxes cannot be placed inside typeset equations', - ); - - $out; -} - -sub tex_ans_rule_extension { - my $len = shift; - $len = 20 unless $len; - # warn "tex_ans_rule_extension may be missnumbering the answer"; - my $name = NEW_ANS_NAME($$r_ans_rule_count); - my $answer_rule = NAMED_ANS_RULE($name, $len); # we don't want to create three answer rules in different modes. - my $out = MODES( - 'TeX' => $answer_rule, - 'Latex2HTML' => '\fbox{Answer boxes cannot be placed inside typeset equations}', - 'HTML_tth' => '\\begin{rawhtml} ' . $answer_rule . '\\end{rawhtml}', - 'HTML_dpng' => '\fbox{Answer boxes cannot be placed inside typeset equations}', - 'HTML' => $answer_rule, - 'PTX' => 'Answer boxes cannot be placed inside typeset equations', - ); - - $out; -} -# still needs some cleanup. -sub NAMED_TEX_ANS_RULE { - my $name = shift; - my $len = shift; - $len = 20 unless $len; - my $answer_rule = NAMED_ANS_RULE($name, $len); # we don't want to create three answer rules in different modes. - my $out = MODES( - 'TeX' => $answer_rule, - 'Latex2HTML' => '\\fbox{Answer boxes cannot be placed inside typeset equations}', - 'HTML_tth' => '\\begin{rawhtml} ' . $answer_rule . '\\end{rawhtml}', - 'HTML_dpng' => '\\fbox{Answer boxes cannot be placed inside typeset equations}', - 'HTML' => $answer_rule, - 'PTX' => 'Answer boxes cannot be placed inside typeset equations', - ); - - $out; -} - -sub NAMED_TEX_ANS_RULE_EXTENSION { - my $name = shift; - my $len = shift; - $len = 20 unless $len; - my $answer_rule = - NAMED_ANS_RULE_EXTENSION($name, $len); # we don't want to create three answer rules in different modes. - my $out = MODES( - 'TeX' => $answer_rule, - 'Latex2HTML' => '\fbox{Answer boxes cannot be placed inside typeset equations}', - 'HTML_tth' => '\\begin{rawhtml} ' . $answer_rule . '\\end{rawhtml}', - 'HTML_dpng' => '\fbox{Answer boxes cannot be placed inside typeset equations}', - 'HTML' => $answer_rule, - 'PTX' => 'Answer boxes cannot be placed inside typeset equations', - ); - - $out; + return wantarray ? @checkboxes : join(" ", @checkboxes); } sub ans_box { - my $row = shift; - my $col = shift; - $row = 5 unless $row; - $col = 80 unless $col; + my ($row, $col) = @_; my $name = NEW_ANS_NAME(); - NAMED_ANS_BOX($name, $row, $col); + RECORD_IMPLICIT_ANS_NAME($name); + return NAMED_ANS_BOX($name, $row || 5, $col || 80); } # this is legacy code; use ans_checkbox instead @@ -932,60 +749,48 @@ sub checkbox { } sub NAMED_POP_UP_LIST { - my $name = shift; - my @list = @_; - if (ref($list[0]) eq 'ARRAY') { - my @list1 = @{ $list[0] }; - @list = map { $_ => $_ } @list1; - } - my $moodle_prefix = ($envir{use_opaque_prefix}) ? "%%IDPREFIX%%" : ''; + my ($name, @list) = @_; + + my %options = ref($list[0]) eq 'ARRAY' ? (map { $_ => $_ } @{ $list[0] }) : @list; + + my $moodle_prefix = $envir{use_opaque_prefix} ? '%%IDPREFIX%%' : ''; + + my $answer_value = $inputs_ref->{$name} // ''; + $name = RECORD_ANS_NAME($name, $answer_value); - my $answer_value = ''; - $answer_value = ${$inputs_ref}{$name} if defined(${$inputs_ref}{$name}); - my $out = ""; if ($displayMode eq 'HTML_MathJax' || $displayMode eq 'HTML_dpng' || $displayMode eq 'HTML' - || $displayMode eq 'HTML_tth' - || $displayMode eq 'HTML_jsMath' - || $displayMode eq 'HTML_asciimath' - || $displayMode eq 'HTML_LaTeXMathML' - || $displayMode eq 'HTML_img') + || $displayMode eq 'HTML_tth') { - $out = qq!\n"; - } elsif ($displayMode eq "Latex2HTML") { - $out = qq! \\begin{rawhtml}\\end{rawhtml}\n"; + return tag( + 'div', + class => 'text-nowrap d-inline', + tag( + 'select', + class => 'pg-select', + name => "$moodle_prefix$name", + id => "$moodle_prefix$name", + size => 1, + join( + '', + map { tag('option', value => $_, $_ eq $answer_value ? (selected => undef) : (), $options{$_}) } + keys %options + ) + ) + ); } elsif ($displayMode eq "TeX") { - $out .= "\\fbox{?}"; + return "\\fbox{?}"; } elsif ($displayMode eq "PTX") { - $out = '' . "\n"; - my $i; - foreach ($i = 0; $i < @list; $i = $i + 2) { - $out .= '
                    • ' . $list[ $i + 1 ] . '
                    • ' . "\n"; - } - $out .= '
                      '; + return '' . "\n" . join('', map {"
                    • $options{$_}
                    • \n"} keys %options) . '
                      '; } - $name = RECORD_ANS_NAME($name, $answer_value); # record answer name - $out; } sub pop_up_list { my @list = @_; - my $name = NEW_ANS_NAME(); # get new answer name - NAMED_POP_UP_LIST($name, @list); + my $name = NEW_ANS_NAME(); + RECORD_IMPLICIT_ANS_NAME($name); + return NAMED_POP_UP_LIST($name, @list); } =head2 answer_matrix @@ -1007,55 +812,42 @@ =head2 answer_matrix =cut sub answer_matrix { - my $m = shift; - my $n = shift; - my $width = shift; - my @options = @_; - my @array = (); + my ($m, $n, $width, @options) = @_; + my @array; for (my $i = 0; $i < $m; $i += 1) { - my @row_array = (); + my @row_array; for (my $i = 0; $i < $n; $i += 1) { push @row_array, ans_rule($width); } - my $r_row_array = \@row_array; - push @array, $r_row_array; + push @array, \@row_array; } # display_matrix hasn't been loaded into the cache safe compartment # so we need to refer to the subroutine in this way to make # sure that main is defined correctly. my $ra_local_display_matrix = PG_restricted_eval(q!\&main::display_matrix!); - &$ra_local_display_matrix(\@array, @options); + return &$ra_local_display_matrix(\@array, @options); } sub NAMED_ANS_ARRAY_EXTENSION { - my $name = shift; - my $col = shift; - my %options = @_; - $col = 20 unless $col; - my $answer_value = ''; + my ($name, $col, %options) = @_; + $col ||= 20; - $answer_value = ${$inputs_ref}{$name} if defined(${$inputs_ref}{$name}); - if ($answer_value =~ /\0/) { - my @answers = split("\0", $answer_value); - $answer_value = shift(@answers); - $answer_value = '' unless defined($answer_value); - } elsif (ref($answer_value) eq 'ARRAY') { - my @answers = @{$answer_value}; + my $answer_value = $inputs_ref->{$name} // ''; + $answer_value = [ split("\0", $answer_value) ] if $answer_value =~ /\0/; + + if (ref($answer_value) eq 'ARRAY') { + my @answers = @$answer_value; $answer_value = shift(@answers); $answer_value = '' unless defined($answer_value); } - my $label; - if (defined($options{aria_label})) { - $label = $options{aria_label}; - } else { - $label = generate_aria_label($name); - } + my $label = $options{aria_label} // generate_aria_label($name); + + # the name of the answer evaluator controlling this collection of responses. + my $answer_group_name; - # warn "ans_label $options{ans_label} $name $answer_value"; - my $answer_group_name; # the name of the answer evaluator controlling this collection of responses. - # catch deprecated use of ans_label to pass answer_group_name + # catch deprecated use of ans_label to pass answer_group_name if (defined($options{ans_label})) { WARN_MESSAGE( "Error in NAMED_ANS_ARRAY_EXTENSION: the answer group name should be passed in ", @@ -1068,76 +860,60 @@ sub NAMED_ANS_ARRAY_EXTENSION { if (defined($options{answer_group_name})) { $answer_group_name = $options{answer_group_name}; } + if ($answer_group_name) { INSERT_RESPONSE($options{answer_group_name}, $name, $answer_value); } else { WARN_MESSAGE("Error: answer_group_name must be defined for $name"); } - $answer_value = encode_pg_and_html($answer_value); - my $tcol = $col / 2 > 3 ? $col / 2 : 3; ## get max - $tcol = $tcol < 40 ? $tcol : 40; ## get min + my $tcol = $col / 2 > 3 ? $col / 2 : 3; # get max + $tcol = $tcol < 40 ? $tcol : 40; # get min - MODES( - TeX => "\\relax{\\answerRule[$name]{$tcol}}", - Latex2HTML => - qq!\\begin{rawhtml}\n\n\\end{rawhtml}\n!, - HTML => qq!!, + return MODES( + TeX => "{\\answerRule[$name]{$tcol}}", + HTML => tag( + 'input', + type => 'text', + size => $col, + name => $name, + id => $name, + class => 'codeshard', + aria_label => $label, + autocomplete => 'off', + autocapitalize => 'off', + spellcheck => 'false', + value => $answer_value + ), PTX => qq!!, ); } sub ans_array { - my $m = shift; - my $n = shift; - my $col = shift; - $col = 20 unless $col; - my $ans_label = NEW_ANS_NAME(); - my $num = ans_rule_count(); - my @options = @_; - my @array = (); - my $answer_value = ""; - my @response_list = (); - my $name; + my ($m, $n, $col, @options) = @_; + $col ||= 20; + + my $ans_label = NEW_ANS_NAME(); + RECORD_IMPLICIT_ANS_NAME($ans_label); + $main::vecnum = -1; CLEAR_RESPONSES($ans_label); + my @array; for (my $i = 0; $i < $n; $i += 1) { - $name = NEW_ANS_ARRAY_NAME_EXTENSION($num, 0, $i); + my $name = NEW_ANS_ARRAY_NAME_EXTENSION(0, $i); $array[0][$i] = NAMED_ANS_ARRAY_EXTENSION($name, $col, ans_label => $ans_label); } for (my $j = 1; $j < $m; $j += 1) { for (my $i = 0; $i < $n; $i += 1) { - $name = NEW_ANS_ARRAY_NAME_EXTENSION($num, $j, $i); + my $name = NEW_ANS_ARRAY_NAME_EXTENSION($j, $i); $array[$j][$i] = NAMED_ANS_ARRAY_EXTENSION($name, $col, ans_label => $ans_label); } } my $ra_local_display_matrix = PG_restricted_eval(q!\&main::display_matrix!); - &$ra_local_display_matrix(\@array, @options); -} - -sub ans_array_extension { - my $m = shift; - my $n = shift; - my $col = shift; - $col = 20 unless $col; - my $num = ans_rule_count(); #hack -- ans_rule_count is updated after being used - my @options = @_; - my @response_list = (); - my $name; - my @array = (); - my $ans_label = $main::PG->new_label($num); - - for (my $j = 0; $j < $m; $j += 1) { - for (my $i = 0; $i < $n; $i += 1) { - $name = NEW_ANS_ARRAY_NAME_EXTENSION($num, $j, $i); - $array[$j][$i] = NAMED_ANS_ARRAY_EXTENSION($name, $col, answer_group_name => $ans_label, @options); - } - } - my $ra_local_display_matrix = PG_restricted_eval(q!\&main::display_matrix!); - &$ra_local_display_matrix(\@array, @options); + + return $ra_local_display_matrix->(\@array, @options); } # end answer blank macros @@ -1182,8 +958,24 @@ sub SOLUTION { return "" if $solution_body eq ""; if ($displayMode =~ /HTML/) { - TEXT('
                      ', - knowlLink(SOLUTION_HEADING(), value => $solution_body, type => 'solution'), '
                      '); + TEXT(tag( + 'div', + class => 'solution accordion my-3', + tag( + 'details', + class => 'accordion-item', + tag( + 'summary', + class => 'accordion-button collapsed text-primary fw-bold py-2', + tag('span', class => 'accordion-header user-select-none', SOLUTION_HEADING()) + ) + . tag( + 'div', + class => 'accordion-collapse collapse', + tag('div', class => 'accordion-body', $solution_body) + ) + ) + )); } elsif ($displayMode =~ /TeX/) { TEXT( "\n%%% BEGIN SOLUTION\n" @@ -1208,7 +1000,24 @@ sub HINT { my $hint_body = hint(@_); return unless $hint_body; if ($displayMode =~ /HTML/) { - TEXT('
                      ', knowlLink(HINT_HEADING(), value => $hint_body, type => 'hint'), '
                      '); + TEXT(tag( + 'div', + class => 'hint accordion my-3', + tag( + 'details', + class => 'accordion-item', + tag( + 'summary', + class => 'accordion-button collapsed text-primary fw-bold py-2', + tag('span', class => 'accordion-header user-select-none', HINT_HEADING()) + ) + . tag( + 'div', + class => 'accordion-collapse collapse', + tag('div', class => 'accordion-body', $hint_body) + ) + ) + )); } elsif ($displayMode =~ /TeX/) { TEXT( "\n%%% BEGIN HINT\n" @@ -1442,7 +1251,7 @@ sub PAR { MODES( TeX => '\\vskip\\baselineskip ', Latex2HTML => '\\begin{rawhtml}

                      \\end{rawhtml}', - HTML => '

                      ', + HTML => '

                      ', PTX => "\n\n" ); } @@ -1505,19 +1314,17 @@ sub END_ONE_COLUMN { # deprecated sub SOLUTION_HEADING { MODES( - TeX => '{\\bf ' . maketext('Solution: ') . ' }', - Latex2HTML => '\\par {\\bf ' . maketext('Solution:') . ' }', - HTML => '' . maketext('Solution:') . ' ', - PTX => '' + TeX => '{\\bf ' . maketext('Solution:') . ' }', + HTML => maketext('Solution'), + PTX => '' ); } sub HINT_HEADING { MODES( - TeX => "{\\bf " . maketext('Hint: ') . "}", - Latex2HTML => "\\par {\\bf " . maketext('Hint:') . " }", - HTML => "" . maketext('Hint:') . " ", - PTX => '' + TeX => '{\\bf ' . maketext('Hint:') . ' }', + HTML => maketext('Hint'), + PTX => '' ); } sub US { MODES(TeX => '\\_', Latex2HTML => '\\_', HTML => '_', PTX => '_'); }; # underscore, e.g. file${US}name @@ -2070,25 +1877,26 @@ sub safe_ev { ($out, $PG_eval_errors, $PG_full_error_report); } +sub safe_evp { + my @result = &safe_ev; + $result[0] = '${__blank__}' . $result[0] . '${__blank__}'; + return @result; +} + sub old_safe_ev { my $in = shift; - my ($out, $PG_eval_errors, $PG_full_error_report) = PG_restricted_eval("$in;"); - # the addition of the ; seems to provide better error reporting + my ($out, $PG_eval_errors, $PG_full_error_report) = PG_restricted_eval($in); if ($PG_eval_errors) { my @errorLines = split("\n", $PG_eval_errors); -#$out = "
                      $PAR % ERROR in $0:old_safe_ev, PGbasicmacros.pl: $PAR % There is an error occuring inside evaluation brackets \\{ ...code... \\} $BR % somewhere in an EV2 or EV3 or BEGIN_TEXT block. $BR % Code evaluated:$BR $in $BR % $BR % $errorLines[0]\n % $errorLines[1]$BR % $BR % $BR 
                      "; - warn " ERROR in old_safe_ev, PGbasicmacros.pl:
                      -		## There is an error occuring inside evaluation brackets \\{ ...code... \\}
                      -		## somewhere in an EV2 or EV3 or BEGIN_TEXT block.
                      -		## Code evaluated:
                      -		## $in
                      -		##" . join("\n     ", @errorLines) . "
                      -		##
                      $BR - "; - $out = "$PAR $BBOLD $in $EBOLD $PAR"; + warn "There is an error occuring inside evaluation brackets \\{ ...code... \\}\n" + . "somewhere in an EV2, EV3, or BEGIN_TEXT block.\n" + . "Code evaluated:\n$in\n" + . "Errors:\n" + . join("\n", @errorLines) . "\n"; + $out = "$BBOLD$in$EBOLD"; } - ($out, $PG_eval_errors, $PG_full_error_report); + return ($out, $PG_eval_errors, $PG_full_error_report); } sub FEQ { # Format EQuations @@ -2160,11 +1968,26 @@ sub general_math_ev3 { $out = "`$in`" if $mode eq "inline"; $out = '
                      `' . $in . '`
                      ' if $mode eq "display"; } elsif ($displayMode eq "PTX") { - #protect XML control characters + # protect XML control characters $in =~ s/\&(?!([\w#]+;))/\\amp /g; $in =~ s/' . "$in" . '' if $mode eq "inline"; - $out = '' . "$in" . '' if $mode eq "display"; + # attempt to parse align|alignat|gather into complete md/mrow structure, otherwise use me + if ($mode eq 'inline') { + $out = "$in"; + } elsif ($mode eq 'display' && $in =~ /^\s*\\begin\{(align|alignat|gather)}((?!\\end\{\1}).)*\\end\{\1}\s*$/s) { + my $alignment = $1; + my $lines = + ($in =~ s/^\s*\\begin\{$alignment}\s*(((?!\\end\{$alignment}).)*)\s*\\end\{$alignment}\s*$/$1/sr); + $lines =~ s/^\{\d+\}// if ($alignment eq 'alignat'); + my @lines = split(/\\\\\n?/, $lines); + @lines = map { $_ =~ s/^\s+|\s+$//r } @lines; + my @rows = map {"$_"} @lines; + my $rows = join("\n", @rows); + $alignment = ($alignment eq 'align') ? '' : " alignment=\"$alignment\""; + $out = "\n$rows\n"; + } elsif ($mode eq 'display') { + $out = "$in"; + } } elsif ($displayMode eq "HTML_LaTeXMathML") { $in = HTML::Entities::encode_entities($in); $in = '{' . $in . '}'; @@ -2294,12 +2117,13 @@ sub EV3P { %{$option_ref}, ); my $string = join(" ", @_); - $string = ev_substring($string, "\\\\{", "\\\\}", \&safe_ev) if $options{processCommands}; + $string = ev_substring($string, "\\\\{", "\\\\}", $options{processVariables} ? \&safe_evp : \&safe_ev) + if $options{processCommands}; if ($options{processVariables}) { my $eval_string = $string; $eval_string =~ s/\$(?![a-z\{])/\${DOLLAR}/gi if $options{fixDollars}; - my ($evaluated_string, $PG_eval_errors, $PG_full_errors) = - PG_restricted_eval("< ..., value => ..., type => ...]) +=head2 knowlLink + +Inserts a knowl link into the problem. Usually you should not call this method +directly. Instead use C below. + +Usage: C + +C<$display_text> is the text that will be shown for the link. + +The following options may be included in C<%options>. Note that one of C +or C is required. + +=over + +=item url + +A URL whose contents will be shown in a modal dialog when the knowl link is +clicked. These contents will be fetched by JavaScript and injected into the +knowl modal dialog. + +=item value + +The direct contents that will be shown in a modal dialog when the knowl link is +clicked. + +=item title + +A string that will be used for the title of the modal dialog that opens when the +knowl link is clicked. If this is not provided, then C<$display_text> will be +used for the title. + +=item type + +A string that will be set as the data-type attribute of the knowl link. This is +only used by PreTeXt. + +=back + +Example usage: + + knowlLink('Click Me', title => 'Fascinating Contents', value => 'Here are my facinating contents.'); + knowlLink('Help Me', title => 'Help Contents', url => 'https://my.domain.edu/helpfile-contents'); + +=cut + sub knowlLink { - my $display_text = shift; + my ($display_text, %options) = @_; - # Check that there are an even number of inputs WARN_MESSAGE( - 'usage: knowlLink($display_text, [url => $url, value => $helpMessage, type => "help/hint/solution/..."]);' - . qq!after the display_text the information requires key/value pairs. - Received @_ !, scalar(@_) % 2 - ) if scalar(@_) % 2; - - my %options = @_; + 'usage: knowlLink($display_text, [url => $url, value => $contents, title => $title, type => "help"]);', + 'One of "url => $url" or "value => $contents" is required.') + unless $options{value} || $options{url}; - my $properties = ''; - if ($options{value}) { - $properties = - 'data-knowl-contents="' - . encode_pg_and_html($options{value}) - . ($options{base64} ? '" data-base64="1"' : '"'); - } elsif ($options{url}) { - $properties = qq!data-knowl-url="$options{url}"!; + if ($displayMode eq 'TeX') { + return "{\\bf\\underline{$display_text}}"; + } elsif ($displayMode eq 'PTX') { + return ($options{type} && $options{type} eq 'help') + ? '' + : '$display_text"; } else { - WARN_MESSAGE( - 'usage: knowlLink($display_text, [url => $url, value => $helpMessage, type => "help/hint/solution/..."]);' - ); - } - - $properties .= qq! data-type="$options{type}"! if $options{type}; + my %properties; + if ($options{value}) { + $properties{data_knowl_contents} = + $options{base64} ? $main::PG->decode_base64($options{value}) : $options{value}; + } elsif ($options{url}) { + $properties{data_knowl_url} = $options{url}; + } - MODES( - TeX => "{\\bf \\underline{$display_text}}", - HTML => qq!$display_text!, - PTX => '' . $display_text . '', - ); + $properties{data_knowl_title} = $options{title} if $options{title}; + $properties{data_type} = $options{type} if $options{type}; + return tag('button', type => 'button', class => 'knowl', %properties, $display_text); + } } sub iframe { - my $url = shift; - my %options = @_; # keys: height, width, id, name - my $formatted_options = join(" ", map {qq!$_ = "$options{$_}"!} (keys %options)); - return "$BBOLD\[ broken link: $url \] $EBOLD" unless defined($url); + my ($url, %attributes) = @_; MODES( TeX => "\\framebox{" . protect_underbar($url) . "}\n", - HTML => qq!\n \n!, + HTML => tag('iframe', src => $url, %attributes), PTX => '', ); } -=head2 helpLink($type, $display_text, $helpurl) +=head2 helpLink + +Usage: C Creates links for students to help documentation on formatting answers and allows for custom help links. @@ -2940,7 +2801,7 @@ =head2 Macros for displaying images Usage: - image($image, width => 200, height => 200, tex_size => 800, alt => 'alt text', extra_html_tags => 'style="border:solid black 1pt"'); + image($image, width => 200, height => 200, tex_size => 800, valign => 'middle', alt => 'alt text', extra_html_tags => 'style="border:solid black 1pt"'); where C<$image> can be a local file path, URL, WWPlot object, PGlateximage object, PGtikz object, or parser::GraphTool object. @@ -2957,6 +2818,9 @@ =head2 Macros for displaying images HTML version with a 600 pixel wide reading area. If C is missing and C is declared, we presume this is a wide image and then C defaults to 800. +C can be 'top', 'middle', or 'bottom'. This aligns the image relative to +the surrounding line of text. + image([$image1,$image2], width => 200, height => 200, tex_size => 800, alt => ['alt text 1','alt text 2'], extra_html_tags => 'style="border:solid black 1pt"'); image([$image1,$image2], width => 200, height => 200, tex_size => 800, alt => 'common alt text', extra_html_tags => 'style="border:solid black 1pt"'); @@ -2976,6 +2840,7 @@ sub image { width => '', height => '', tex_size => '', + valign => 'middle', # default value for alt is undef, since an empty string is the explicit indicator of a decorative image alt => undef, extra_html_tags => '', @@ -3004,6 +2869,9 @@ sub image { my $width_ratio = $tex_size * (.001); my @image_list = (); my @alt_list = (); + my $valign = 'middle'; + $valign = 'top' if ($out_options{valign} eq 'top'); + $valign = 'bottom' if ($out_options{valign} eq 'bottom'); # if width and/or height are explicit, create string for attribute to be used in HTML, LaTeX2HTML my $width_attrib = ($width) ? qq{ width="$width"} : ''; @@ -3043,10 +2911,18 @@ sub image { if ($displayMode eq 'TeX') { my $imagePath = $imageURL; # in TeX mode, alias gives us a path, not a URL - # We're going to create PDF files with our TeX (using pdflatex), so + # We're going to create PDF files with our TeX (using LaTeX), so # alias should have given us the path to a PNG image. if ($imagePath) { - $out = "\\includegraphics[width=$width_ratio\\linewidth]{$imagePath}\n"; + if ($valign eq 'top') { + $out = '\settoheight{\strutheight}{\strut}' + . "\\raisebox{-\\height + \\strutheight}{\\includegraphics[width=$width_ratio\\linewidth]{$imagePath}}\n"; + } elsif ($valign eq 'bottom') { + $out = "\\includegraphics[width=$width_ratio\\linewidth]{$imagePath}\n"; + } else { + $out = '\settoheight{\strutheight}{\strut}' + . "\\raisebox{-0.5\\height + 0.5\\strutheight}{\\includegraphics[width=$width_ratio\\linewidth]{$imagePath}}\n"; + } } else { $out = ""; } @@ -3067,7 +2943,7 @@ sub image { my $altattrib = ''; if (defined $alt_list[0]) { $altattrib = 'alt="' . encode_pg_and_html(shift @alt_list) . '"' } $out = - qq!!; + qq!!; } elsif ($displayMode eq 'PTX') { my $ptxwidth = ($width ? int($width / 6) : 80); if (defined $alt) { diff --git a/macros/core/PGessaymacros.pl b/macros/core/PGessaymacros.pl index 453e1ce8af..793aa74f95 100644 --- a/macros/core/PGessaymacros.pl +++ b/macros/core/PGessaymacros.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -13,9 +13,6 @@ # Artistic License for more details. ################################################################################ -# FIXME TODO: -# Document and maybe split out: filters, graders, utilities - =head1 NAME PGessaymacros.pl - Macros for building answer evaluators. @@ -26,20 +23,23 @@ =head2 SYNPOSIS essay_cmp() -Answer Boxes +Answer Boxes: essay_box() -To use essay answers just put an C into your problem file wherever you want the -input box to go and then use C for the corresponding checker. You will then need -grade the problem manually. The grader can be found in the "Detail Set List". +To use essay answers call C in your problem file wherever you want +the input box to go, and then use C for the corresponding checker. +You will then need to grade the problem manually. explanation_box() -Like an C, except can be turned off at a configuration level. Intended for two-part -questions where the first answer is automatically assessible, and the second part is an explanation -or "showing your work". An instructor may want to turn these off to use the problem but without the -manual grading component. These necessarily supply their own C. +Like an C, except that it can be turned off at a configuration +level. This is intended for two-part questions where the first answer is +automatically assessible, and the second part is an explanation or "show your +work" type answer. An instructor may want to turn these off to use the problem +but without the manual grading component. These necessarily supply their own +C. + =cut sub _PGessaymacros_init { @@ -47,113 +47,132 @@ sub _PGessaymacros_init { } sub essay_cmp { - - my $self = shift; - my $ans = new AnswerEvaluator; + my (%options) = @_; + my $ans = AnswerEvaluator->new; $ans->ans_hash( - type => "essay", - correct_ans => "Undefined", - correct_value => $self, - @_, + type => 'essay', + correct_ans => 'Undefined', + correct_value => '', + scaffold_force => 1, + feedback_options => sub { + my ($ansHash, $options) = @_; + + $options->{manuallyGraded} = 1; + + if ($envir{needs_grading} + || !defined $ansHash->{ans_label} + || !defined $inputs_ref->{"previous_$ansHash->{ans_label}"} + || $inputs_ref->{ $ansHash->{ans_label} } ne $inputs_ref->{"previous_$ansHash->{ans_label}"}) + { + $options->{needsGrading} = 1; + $options->{resultTitle} = maketext('Ungraded'); + } else { + $options->{resultTitle} = maketext('Graded'); + $ansHash->{ans_message} = ''; + } + + $options->{resultClass} = ''; + $options->{insertMethod} = 'append_content'; + $options->{btnClass} = 'btn-info'; + $options->{btnAddClass} = ''; + $options->{wrapPreviewInTex} = 0; + $options->{showEntered} = 0; # Suppress output of the feedback entered answer. + $options->{showCorrect} = 0; # Suppress output of the feedback correct answer. + }, + %options, ); $ans->install_evaluator(sub { - my $student = shift; - my %response_options = @_; + my $ans_hash = shift; - $student->{original_student_ans} = - (defined $student->{original_student_ans}) ? $student->{original_student_ans} : ''; + $ans_hash->{original_student_ans} //= ''; + $ans_hash->{_filter_name} = 'Essay Check'; + $ans_hash->{score} = 0; + $ans_hash->{ans_message} = maketext('This answer will be graded at a later time.'); + $ans_hash->{preview_text_string} = ''; - my $answer_value = $student->{original_student_ans}; - - # always returns false but stuff should check for the essay flag and avoid the red highlighting - loadMacros("contextTypeset.pl"); + loadMacros('contextTypeset.pl'); my $oldContext = Context(); - Context("Typeset"); - $answer_value = EV3P({ processCommands => 0, processVariables => 0 }, text2PG($answer_value)); - + Context('Typeset'); + $ans_hash->{preview_latex_string} = + EV3P({ processCommands => 0, processVariables => 0 }, text2PG($ans_hash->{original_student_ans})); Context($oldContext); - my $ans_hash = new AnswerHash( - 'score' => "0", - 'correct_ans' => "Undefined", - # 'student_ans'=>$student->{student_ans}, - 'student_ans' => '', #supresses output to original answer field - 'original_student_ans' => $student->{original_student_ans}, - 'type' => 'essay', - 'ans_message' => 'This answer will be graded at a later time.', - 'preview_text_string' => '', - 'preview_latex_string' => $answer_value, - ); return $ans_hash; }); - $ans->install_pre_filter('erase') if $self->{ans_name}; - return $ans; } sub NAMED_ESSAY_BOX { my ($name, $row, $col) = @_; - $row = 8 unless defined($row); - $col = 75 unless defined($col); + $row //= 8; + $col //= 75; my $height = .07 * $row; - my $answer_value = ''; - $answer_value = $inputs_ref->{$name} if defined($inputs_ref->{$name}); - $name = RECORD_ANS_NAME($name, $answer_value); + my $answer_value = $inputs_ref->{$name} // ''; + $name = RECORD_ANS_NAME($name, $answer_value); - my $label = generate_aria_label($name); - # $answer_value =~ tr/$@//d; #`## make sure student answers can not be interpolated by e.g. EV3 - - #### Answer Value needs to have special characters replaced by the html codes - $answer_value = encode_pg_and_html($answer_value); - - # Get rid of tabs since they mess up the past answer db + # Get rid of tabs since they mess up the past answer db. + # FIXME: This fails because this only modifies the value for the next submission. + # It doesn't change the value in the already submitted form. $answer_value =~ s/\t/\ \ \ \ \ /; - #INSERT_RESPONSE($name,$name,$answer_value); # no longer needed? - my $out = MODES( - TeX => qq!\\vskip $height in \\hrulefill\\quad !, - Latex2HTML => - qq!\\begin{rawhtml}\\end{rawhtml}!, - HTML => qq! - - - !, + return MODES( + TeX => qq!\\vskip $height in \\hrulefill\\quad !, + HTML => tag( + 'textarea', + name => $name, + id => $name, + aria_label => generate_aria_label($name), + rows => $row, + cols => $col, + class => 'latexentryfield', + title => 'Enclose math expressions with backticks or use LaTeX.', + # Answer Value needs to have special characters replaced by the html codes + encode_pg_and_html($answer_value) + ) + . tag( + 'div', + class => 'latexentry-button-container d-flex gap-2 mt-2', + id => "$name-latexentry-button-container", + data_feedback_insert_element => $name, + tag( + 'button', + class => 'latexentry-preview btn btn-secondary btn-sm', + type => 'button', + maketext('Preview') + ) + ) + . tag('input', type => 'hidden', name => "previous_$name", value => $answer_value), PTX => '', ); - - $out; } sub essay_help { - - my $out = MODES( - TeX => '', - Latex2HTML => '', - HTML => qq! -

                      This is an essay answer text box. You can type your answer in here and, after you hit submit, - it will be saved so that your instructor can grade it at a later date. If your instructor makes - any comments on your answer those comments will appear on this page after the question has been - graded. You can use LaTeX to make your math equations look pretty. - LaTeX expressions should be enclosed using the parenthesis notation and not dollar signs. -

                      - !, + return MODES( + TeX => '', + HTML => tag( + 'p', + maketext( + 'This is an essay answer text box. You can type your answer in here and, after you hit submit, ' + . 'it will be saved so that your instructor can grade it at a later date. If your instructor ' + . 'makes any comments on your answer those comments will appear on this page after the question ' + . 'has been graded. You can use LaTeX to make your math equations look pretty. ' + . 'LaTeX expressions should be enclosed using the parenthesis notation and not dollar signs.' + ) + ), PTX => '', ); - - $out; } sub essay_box { - my $row = shift; - my $col = shift; - $row = 8 unless $row; - $col = 75 unless $col; + my ($row, $col) = @_; + $row ||= 8; + $col ||= 75; my $name = NEW_ANS_NAME(); + main::RECORD_IMPLICIT_ANS_NAME($name); NAMED_ESSAY_BOX($name, $row, $col); } @@ -167,20 +186,16 @@ sub essay_box { # help: boolean for whether to display the essay help message; default is true sub explanation_box { my %options = @_; - my $row = 8; - my $col = 75; - $row = $options{height} if defined $options{height}; - $col = $options{width} if defined $options{width}; - $row = $options{row} if defined $options{row}; - $col = $options{col} if defined $options{col}; - my $message = 'Explain.'; - if (defined $options{message}) { $message = $options{message} } - my $help = 1; - $help = $options{help} if defined $options{help}; - if ($envir{waiveExplanations}) { } - else { + + if ($envir{waiveExplanations}) { + return ''; + } else { ANS(essay_cmp()); - return $message . $PAR . essay_box($row, $col) . ($help ? essay_help() : ''); + return + ($options{message} // 'Explain.') + . $PAR + . essay_box($options{row} // $options{height} // 8, $options{col} // $options{width} // 75) + . (($options{help} // 1) ? essay_help() : ''); } } diff --git a/macros/core/Parser.pl b/macros/core/Parser.pl index 0847c5df65..134aeabe59 100644 --- a/macros/core/Parser.pl +++ b/macros/core/Parser.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -93,7 +93,15 @@ =head2 Compute # ^uses Formula # ^uses Value::contextSet sub Compute { - my $string = shift; + my $string = shift; + if ($string =~ m/^\s*-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]\d+)\s*$/ + && Value::matchNumber($string) + && ($string ^ $string) eq '0') + { + warn "Compute() called with ambiguous value: $string\n" + . "-- use Real() for scientific notation or Formula() for Euler's number e\n"; + $string = uc($string); + } my $formula = Formula($string); $formula = $formula->{tree}->Compute if $formula->{tree}{canCompute}; my $context = $formula->context; diff --git a/macros/core/externalData.pl b/macros/core/externalData.pl index 53f113cfb5..15c24b3c2f 100644 --- a/macros/core/externalData.pl +++ b/macros/core/externalData.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/core/sage.pl b/macros/core/sage.pl index 3932ec04f3..40c270b1b4 100644 --- a/macros/core/sage.pl +++ b/macros/core/sage.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/core/scaffold.pl b/macros/core/scaffold.pl index bce37b3525..029a102ba6 100644 --- a/macros/core/scaffold.pl +++ b/macros/core/scaffold.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -44,17 +44,15 @@ =head1 DESCRIPTION Scaffold::Begin(); Section::Begin("Part 1: The first part"); - BEGIN_TEXT - This is the text for part 1. \(1+1\) = \{ans_rule\} - END_TEXT - ANS(Real(2)->cmp); + BEGIN_PGML + This is the text for part 1. [`1+1 =`] [_]{Real(2)} + END_PGML Section::End(); Section::Begin("Part 2: The second part"); - BEGIN_TEXT - This is text for the second part. \(2*2\) = \{ans_rule\} - END_TEXT - ANS(Real(4)->cmp); + BEGIN_PGML + This is text for the second part. [`2*2 =`] [_]{Real(4)} + END_PGML Section::End(); Scaffold::End(); @@ -75,7 +73,7 @@ =head1 DESCRIPTION answer blank to be considered correct by using the C option on the answer checker. For example: - ANS(Real(123)->cmp(scaffold_force => 1)); + [_]{ Real(123)->cmp(scaffold_force => 1) }; would mean that this answer would not have to be correct for the section to be considered correct. @@ -85,7 +83,7 @@ =head1 DESCRIPTION last one, if you wish. That material would always be showing, regardless of which sections are open. So, for example, you could put a data table in the area before the first section, so that it would be -visible throughtout the problem, no matter which section the student +visible through out the problem, no matter which section the student is working on. The C function accepts optional parameters that @@ -260,15 +258,15 @@ =head1 DESCRIPTION =cut +BEGIN { strict->import } + sub _scaffold_init { # Load style and javascript for opening and closing the scaffolds. ADD_CSS_FILE("js/Scaffold/scaffold.css"); ADD_JS_FILE("js/Scaffold/scaffold.js", 0, { defer => undef }); + return; } -# -# The Scaffoling package -# package Scaffold; our $forceOpen = $main::envir{forceScaffoldsOpen}; @@ -277,31 +275,30 @@ package Scaffold; our $isPTX = $main::displayMode eq "PTX"; our $afterAnswerDate = $main::envir{answersAvailable}; -our $scaffold; # the active scaffold (set by Begin() below) -my @scaffolds = (); # array of nested scaffolds -my $scaffold_no = 0; # each scaffold gets a unique number -my $scaffold_depth = 1; # each scaffold has a nesting depth +our $scaffold; # the active scaffold (set by Begin() below) +my @scaffolds; # array of nested scaffolds +my $scaffold_no = 0; # each scaffold gets a unique number +my $scaffold_depth = 1; # each scaffold has a nesting depth our $PG_ANSWERS_HASH = $main::PG->{PG_ANSWERS_HASH}; # where PG stores answer evaluators our $PG_OUTPUT = $main::PG->{OUTPUT_ARRAY}; # where PG stores the TEXT() output our $PREFIX = "$main::envir{QUIZ_PREFIX}Prob-$main::envir{probNum}"; +# Scaffold::Begin() is used to start a new scaffold section, passing +# it any options that need to be overridden (e.g. is_open, can_open, +# open_first_section, etc). # -# Scaffold::Begin() is used to start a new scaffold section, passing -# it any options that need to be overriden (e.g. is_open, can_open, -# open_first_section, etc). -# -# Problems can include more than one scaffold, if desired, -# and they can be nested. -# -# We save the current PG_OUTPUT, which will be put back durring -# the Scaffold::End() call. The sections use PG_OUTPUT to create -# their own text, which is added to the $scaffold->{output} -# during the Section::End() call. +# Problems can include more than one scaffold, if desired, +# and they can be nested. # +# We save the current PG_OUTPUT, which will be put back during +# the Scaffold::End() call. The sections use PG_OUTPUT to create +# their own text, which is added to the $scaffold->{output} +# during the Section::End() call. sub Begin { - my $self = Scaffold->new(@_); + my %options = @_; + my $self = Scaffold->new(%options); unshift(@scaffolds, $self); $scaffold = $self; $scaffold_depth++; @@ -311,47 +308,42 @@ sub Begin { return $self; } +# Scaffold::End() is used to end the scaffold. # -# Scaffold::End() is used to end the scaffold. -# -# This puts the scaffold into the page output -# and opens the sections that should be open. -# Then the next nested scaffold (if any) is poped off -# the stack and returned. -# +# This puts the scaffold into the page output and opens the sections that should be open. +# Then the next nested scaffold (if any) is popped off the stack and returned. sub End { Scaffold->Error("Scaffold::End() without a corresponding Scaffold::Begin") unless @scaffolds; - Scaffold->Error("Scaffold ended with section was still open") if $self->{current_section}; + Scaffold->Error("Scaffold ended with section was still open") if $scaffold->{current_section}; my $self = $scaffold; - push(@{ $self->{output} }, splice(@$PG_OUTPUT, 0)); # collect any final non-section output - $self->hide_other_results(@{ $self->{open} }); # hide results of unnopened sections in the results table - push(@$PG_OUTPUT, @{ $self->{previous_output} }, @{ $self->{output} }) - ; # put back original output and scaffold output + + # collect any final non-section output + push(@{ $self->{output} }, splice(@$PG_OUTPUT, 0)); + + # put back original output and scaffold output + push(@$PG_OUTPUT, @{ $self->{previous_output} }, @{ $self->{output} }); + delete $self->{previous_output}; - delete $self->{output}; # don't need these any more + delete $self->{output}; shift(@scaffolds); $scaffold = $scaffolds[0]; $scaffold_depth--; return $scaffold; } -# -# Report an error and die -# +# Report an error and die sub Error { my $self = shift; my $error = shift; die $error; } +# Create a new Scaffold object. # -# Create a new Scaffold object. -# -# Set the defaults for can_open, is_open, etc., but allow -# the author to override them. -# +# Set the defaults for can_open, is_open, etc., but allow +# the author to override them. sub new { - my $class = shift; + my ($class, %options) = @_; $class = ref($class) if ref($class); my $self = bless { can_open => "when_previous_correct", @@ -361,7 +353,7 @@ sub new { hardcopy_is_open => "always", # open all possible sections in hardcopy open_first_section => 1, # 0 means don't open any sections initially numbered => 0, # 1 means sections will be printed with their number - @_, + %options, number => ++$scaffold_no, # the number for this scaffold depth => $scaffold_depth, # the nesting depth for this scaffold sections => {}, # the sections within this scaffold @@ -373,11 +365,9 @@ sub new { return $self; } -# -# Add a section to the scaffold and give it a unique number (within -# the scaffold). Determine its label and save it as current_section -# so that we know which section is active. -# +# Add a section to the scaffold and give it a unique number (within +# the scaffold). Determine its label and save it as current_section +# so that we know which section is active. sub start_section { my $self = shift; my $section = shift; @@ -389,127 +379,54 @@ sub start_section { return $section; } -# -# Add the content from the current section into the scaffold's output -# and remove the current_section (so we can tell that no sectionis open). -# +# Add the content from the current section into the scaffold's output +# and remove the current_section (so we can tell that no section is open). sub end_section { my $self = shift; push(@{ $self->{output} }, splice(@{$PG_OUTPUT}, 0)); # save the section output delete $self->{current_section}; + return; } -# -# Record the answers for a section, and evaluate them, if non-empty, -# keeping the scores for future reference. -# +# Record the answers for a section. +# Scores are obtained when post processing is done. sub section_answers { - my $self = shift; - my %answers; - # - # MultiAnswer objects can set the answer hash score when the last answer is evaluated, - # so save the hashes and look up the scores after they have all been called. - # Essay answers never return as correct, so special case them, and provide a - # "scaffold_force" option in the AnswerHash that can be used to force Scaffold - # to consider the score to be 1 (bug in PGessaymacros.pl prevents us from using - # it for essay_cmp(), though). - # - push(@{ $self->{ans_names} }, @_); - foreach my $name (@_) { - my $input = $main::inputs_ref->{$name}; - my $evaluator = $PG_ANSWERS_HASH->{$name}->ans_eval; - Parser::Eval(sub { $answers{$name} = $evaluator->evaluate($input) }) if defined($input) && $input ne ""; - $answers{$name}{score} = 1 - if $answers{$name} && (($answers{$name}{type} || "") eq "essay" || $answers{$name}{"scaffold_force"}); - $evaluator->{rh_ans}{ans_message} = ""; - delete $evaluator->{rh_ans}{error_message}; - } - foreach my $name (@_) { $self->{scores}{$name} = $answers{$name}{score} if $answers{$name} } + my ($self, @section_answers) = @_; + push(@{ $self->{ans_names} }, @section_answers); + return; } -# -# Add the given sections to the list of sections to be openned -# for this scaffold -# +# Add the given sections to the list of sections to be opened for this scaffold. sub is_open { - my $self = shift; - push(@{ $self->{open} }, map { $_->{number} } @_) if @_; + my ($self, @open_sections) = @_; + push(@{ $self->{open} }, map { $_->{number} } @open_sections) if @open_sections; return $self->{open}; } -# -# Add CSS to dim the rows of the table that are not in the open -# section. (When a section is marked correct, the next section will -# be opened, so the correct answers will be dimmed, and the new -# section's blank rows will be active. That may be a downside to the -# dimming.) -# -sub hide_other_results { - my $self = shift; - # - # Record the row for each answer evaluator, and - # mark which sections to show - # - my %row; - my $i = 2; - foreach my $name (keys %{$PG_ANSWERS_HASH}) { $row{$name} = $i; $i++ }; # record the rows for all answers - my %show; - map { $show{$_} = 1 } @_; - # - # Get the row numbers for the answers from OTHER sections - # - my @hide = (); - foreach $i (keys %{ $self->{sections} }) { - push(@hide, map { $row{$_} } @{ $self->{sections}{$i}{ans_names} }) if !$show{$i}; - } - # - # Add styles that dim the hidden rows - # (the other possibility would be to use display:none) - # - if (@hide) { - my @styles = (map {".attemptResults > tbody > tr:nth-child($_) {opacity:.5}"} @hide); - main::HEADER_TEXT(''); - } -} - -# -# Check if a scaffold is completely correct. -# (Must be called after the last section is ended.) -# -sub is_correct { - my $self = shift; - my $scores = $self->{scores}; - foreach my $name (@{ $self->{ans_names} }) { return 0 unless ($scores->{$name} || 0) >= 1 } - return 1; -} - package Section; -# -# Shortcuts for Scaffold data -# +# Shortcuts for Scaffold data $PG_ANSWERS_HASH = $Scaffold::PG_ANSWERS_HASH; $PG_OUTPUT = $Scaffold::PG_OUTPUT; +# Section::Begin() is used to start a section in the scaffolding, +# passing it the name of the section and any options (e.g., can_open, +# is_open, etc.). # -# Section::Begin() is used to start a section in the scaffolding, -# passing it the name of the section and any options (e.g., can_open, -# is_open, etc.). -# -# The section is added to the scaffold, and the names of the answer -# blanks for previous sections are recorded, along with information -# about the answer blanks that have evaluators assigned (so we can -# see which answers belong to this section when it closes). -# +# The section is added to the scaffold, and the names of the answer +# blanks for previous sections are recorded, along with information +# about the answer blanks that have evaluators assigned (so we can +# see which answers belong to this section when it closes). sub Begin { + my ($section_name, %options) = @_; my $scaffold = $Scaffold::scaffold; Scaffold->Error("Sections must appear within a Scaffold") unless $scaffold; Scaffold->Error("Section::Begin() while a section is already open") if $scaffold->{current_section}; - my $self = $scaffold->start_section(Section->new(@_)); + my $self = $scaffold->start_section(Section->new($section_name, %options)); my $number = $self->{number}; my $number_at_depth = $number; # Convert the number (e.g. 2) into a depth-styled version (e.g. b, ii, B) - # Supports numbers up to 99 and depth up to 3 but then leaves in arabic + # Supports numbers up to 99 and depth up to 3 but then leaves in Arabic if ($scaffold->{depth} == 1 && $number <= 99) { $number_at_depth = ('a' .. 'cu')[ $number - 1 ]; } elsif ($scaffold->{depth} == 2 && $number <= 99) { @@ -538,19 +455,17 @@ sub Begin { $number_at_depth = ('A' .. 'CU')[ $number - 1 ]; } $self->{number_at_depth} = $number_at_depth; - $self->{previous_ans} = [ @{ $scaffold->{ans_names} } ], # copy of current list of answers in the scaffold - $self->{assigned_ans} = [ $self->assigned_ans ], # array indicating which answers have evaluators - return $self; + $self->{previous_ans} = [ @{ $scaffold->{ans_names} } ]; # copy of current list of answers in the scaffold + $self->{assigned_ans} = [ $self->assigned_ans ]; # array indicating which answers have evaluators + return $self; } +# Section::End() is used to end the active section. # -# Section::End() is used to end the active section. -# -# We get the names of the answer blanks that are in this section, -# then add the HTML around the section that is used by jQuery -# for showing/hiding the section, and finally tell the scaffold -# that the section is complete (it adds the content to its output). -# +# We get the names of the answer blanks that are in this section, +# then add the HTML around the section that is used by JavaScript +# for showing/hiding the section, and finally tell the scaffold +# that the section is complete (it adds the content to its output). sub End { my $scaffold = $Scaffold::scaffold; Scaffold->Error("Sections must appear within a Scaffold") unless $scaffold; @@ -560,19 +475,17 @@ sub End { $scaffold->section_answers(@{ $self->{ans_names} }); $self->add_container(); $scaffold->end_section(); + return; } +# Create a new Section object. # -# Create a new Section object. -# -# It takes default values for can_open, is_open, etc. -# from the active scaffold. These can be overridden -# by the author. -# +# It takes default values for can_open, is_open, etc. +# from the active scaffold. These can be overridden +# by the author. sub new { - my $class = shift; + my ($class, $name, %options) = @_; $class = ref($class) if ref($class); - my $name = shift; my $scaffold = $Scaffold::scaffold; my $self = bless { name => $name, @@ -581,23 +494,16 @@ sub new { after_AnswerDate_can_open => $scaffold->{after_AnswerDate_can_open}, is_open => $scaffold->{is_open}, hardcopy_is_open => $scaffold->{hardcopy_is_open}, - @_, + %options }, $class; return $self; } -# -# Adds the necessary HTML around the content of the section. -# -# First, determine the is_correct and can_open status and save them. -# Then check if the section is to be openned, and if so, add it -# to the open list of the scaffold. -# -# The $PG_OUTPUT variable holds just the contents of this section, -# so we unshift the openning tags onto the front, and push -# the closing tags onto the back. (This is added to the scaffold -# output when $scaffold->end_section() is called.) -# +# Adds the necessary HTML around the content of the section. Initially a temporary "scaffold-section" tag is added that +# wraps the content, and that is replaced with the correct HTML in post processing. The content is also removed in post +# processing if the scaffold can not be opened and is not correct. The $PG_OUTPUT variable holds just the contents of +# this section, so unshift the opening tags onto the front, and push the closing tags onto the back. (This is added to +# the scaffold output when $scaffold->end_section() is called.) sub add_container { my $self = shift; my $scaffold = $Scaffold::scaffold; @@ -605,63 +511,125 @@ sub add_container { my $name = $self->{name} // ''; my $title = ($name || $scaffold->{numbered}) ? $name : "Part $self->{number}:"; my $number = ($scaffold->{numbered} ? $self->{number_at_depth} . '.' : ''); - my ($iscorrect, $canopen, $isopen); - $iscorrect = $self->{is_correct} = $self->is_correct; - $canopen = $self->{can_open} = $self->can_open; - $isopen = $self->is_open; - - $scaffold->is_open($self) if $isopen; - splice(@$PG_OUTPUT, 0, scalar(@$PG_OUTPUT)) - if !($canopen || $iscorrect || $Scaffold::isPTX) || (!$isopen && $Scaffold::isHardcopy); unshift( @$PG_OUTPUT, - @{ - main::MODES( - HTML => [ - '
                      ', - '
                      ', - '
                      ', - '', - '
                      ', - qq{
                      }, - '
                      ' - ], - TeX => ["\\par{\\bf $number $title}\\addtolength{\\leftskip}{15pt}\\par "], - PTX => $name ? [ "\n", "$name\n" ] : ["\n"], - ) - } + main::MODES( + HTML => qq{}, + TeX => + "\\par{\\bf $number $title}\\addtolength{\\leftskip}{15pt}\\par\n%scaffold-section-$label-start\n", + PTX => $name ? "\n$name\n" : "\n", + ) ); push( @$PG_OUTPUT, main::MODES( - HTML => '
                      ', - TeX => "\\addtolength{\\leftskip}{-15pt}\\par ", + HTML => '', + TeX => "%scaffold-section-$label-end\n\\addtolength{\\leftskip}{-15pt}\\par ", PTX => "<\/task>\n", ) ); + + main::add_content_post_processor(sub { + my $problemContents = shift; + + # Nothing needs to be done for the PTX display mode. + return if $Scaffold::isPTX; + + # Provide a "scaffold_force" option in the AnswerHash that can be used to force + # Scaffold to consider the score to be 1. This is used by PGessaymacros.pl. + for (@{ $self->{ans_names} }) { + next unless defined $PG_ANSWERS_HASH->{$_}; + $scaffold->{scores}{$_} = + $PG_ANSWERS_HASH->{$_}{ans_eval}{rh_ans}{scaffold_force} + ? 1 + : $PG_ANSWERS_HASH->{$_}{ans_eval}{rh_ans}{score}; + } + + # Set the active scaffold to the scaffold for this section so that is_correct, can_open, + # and is_open methods use the correct one. + $Scaffold::scaffold = $scaffold; + + # Now, determine the is_correct and can_open status and save them. + # Then check if the section is to be opened, and if so, add it + # to the open list of the scaffold. + my $iscorrect = $self->{is_correct} = $self->is_correct; + my $canopen = $self->{can_open} = $self->can_open; + my $isopen = $self->is_open; + + $scaffold->is_open($self) if $isopen; + + if ($Scaffold::isHardcopy) { + $$problemContents =~ s/%scaffold-section-$label-start.*%scaffold-section-$label-end//ms + if !($canopen || $iscorrect) || !$isopen; + } else { + my $sectionElt = $problemContents->at(qq{scaffold-section[id="$label"]}); + return unless $sectionElt; + + $sectionElt->tag('div'); + delete $sectionElt->attr->{id}; + $sectionElt->attr(class => 'accordion'); + + $sectionElt->content('') if !($canopen || $iscorrect); + + $sectionElt->wrap_content( + Mojo::DOM->new_tag( + 'div', + class => 'accordion-item section-div', + sub { + Mojo::DOM->new_tag( + 'div', + id => $label, + class => 'accordion-collapse collapse' . ($isopen ? ' show' : ''), + 'aria-labelledby' => "$label-header", + sub { Mojo::DOM->new_tag('div', class => 'accordion-body') } + ); + } + )->to_string + ); + + $sectionElt->at('.accordion-item.section-div')->prepend_content( + Mojo::DOM->new_tag( + 'div', + class => 'accordion-header' + . ( + $iscorrect && ($main::envir{showFeedback} || $main::envir{forceShowAttemptResults}) + ? ' iscorrect' + : ' iswrong' + ) + . ($canopen ? ' canopen' : ' cannotopen'), + id => "$label-header", + sub { + Mojo::DOM->new_tag( + 'button', + class => 'accordion-button' . ($isopen ? '' : ' collapsed'), + type => 'button', + ( + $canopen + ? ( + data => { bs_target => "#$label", bs_toggle => 'collapse' }, + 'aria-controls' => $label + ) + : (tabindex => '-1') + ), + aria_expanded => ($isopen ? 'true' : 'false'), + sub { + Mojo::DOM->new_tag('span', class => 'section-number', $number) + . Mojo::DOM->new_tag('span', class => 'section-title', $title); + } + ); + } + )->to_string + ); + } + + return; + }); + + return; } -# -# Check if all the answers for this section are correct -# +# Check if all the answers for this section are correct sub is_correct { my $self = shift; my $scores = $Scaffold::scaffold->{scores}; @@ -669,55 +637,48 @@ sub is_correct { return 1; } -# -# Perform the can_open check for this section: -# If the author supplied code, use it, otherwise use the routine from Section::can_open. -# +# Perform the can_open check for this section: +# If the author supplied code, use it, otherwise use the routine from Section::can_open. sub can_open { my $self = shift; return 1 if $Scaffold::forceOpen; my $method = ($Scaffold::isInstructor ? $self->{instructor_can_open} : $self->{can_open}); $method = $self->{after_AnswerDate_can_open} if $Scaffold::afterAnswerDate; - $method = "Section::can_open::" . $method unless ref($method) eq 'CODE'; - return &{$method}($self); + return $method->($self) if ref($method) eq 'CODE'; + $method = "Section::can_open::" . $method; + return $self->$method; } -# -# Peform the is_open check for this section: -# If the author supplied code, use it, otherwise use the routine from Section::is_open. -# +# Perform the is_open check for this section: +# If the author supplied code, use it, otherwise use the routine from Section::is_open. sub is_open { my $self = shift; return 1 if $Scaffold::forceOpen; return 0 unless $self->{can_open}; # only open ones that are allowed to be open my $method = $self->{is_open}; $method = $self->{hardcopy_is_open} if $Scaffold::isHardcopy; - $method = "Section::is_open::" . $method unless ref($method) eq 'CODE'; - return &{$method}($self); + return $method->($self) if ref($method) eq 'CODE'; + $method = "Section::is_open::" . $method; + return $self->$method; } -# -# Return a boolean array where a 1 means that answer blank has -# an answer evaluator assigned to it and 0 means not. -# +# Return a boolean array where a 1 means that answer blank has +# an answer evaluator assigned to it and 0 means not. sub assigned_ans { - my $self = shift; - my @answers = (); + my $self = shift; + my @answers; foreach my $name (keys %{$PG_ANSWERS_HASH}) { push(@answers, $PG_ANSWERS_HASH->{$name}->ans_eval ? 1 : 0); } return @answers; } -# -# Get the names of any of the original answer blanks that now have -# evaluators attached. -# +# Get the names of any of the original answer blanks that now have evaluators attached. sub new_answers { my $self = shift; my @assigned = @{ $self->{assigned_ans} }; # 0 if previously unassigned, 1 if assigned - my @answers = (); - my $i = 0; + my @answers; + my $i = 0; foreach my $name (keys %{$PG_ANSWERS_HASH}) { push(@answers, $name) if $PG_ANSWERS_HASH->{$name}->ans_eval && !$assigned[$i]; $i++; @@ -727,60 +688,49 @@ sub new_answers { } ######################################################################## -# -# Implements the possible values for the can_open option for scaffolds -# and sections -# +# Implements the possible values for the can_open option for scaffolds +# and sections + package Section::can_open; -# -# Always can be openned -# +# Always can be opened sub always { return 1 } -# -# Can be openned when all the answers from previous sections are correct -# + +# Can be opened when all the answers from previous sections are correct sub when_previous_correct { my $section = shift; my $scores = $Scaffold::scaffold->{scores}; foreach my $name (@{ $section->{previous_ans} }) { return 0 unless ($scores->{$name} || 0) >= 1 } return 1; } -# -# Can open when previous are correct but this one is not -# + +# Can open when previous are correct but this one is not sub first_incorrect { my $section = shift; return when_previous_correct($section) && !$section->{is_correct}; } -# -# Can open when incorrect -# + +# Can open when incorrect sub incorrect { my $section = shift; return !$section->{is_correct}; } -# -# Never can be openned -# + +# Never can be opened sub never { return 0 } ######################################################################## -# -# Implements the possible values for the is_open option for scaffolds -# and sections -# +# Implements the possible values for the is_open option for scaffolds +# and sections + package Section::is_open; -# -# Every section is open that can be -# +# Every section is open that can be sub always { return 1 } -# -# Every incorrect section is open that can be -# (unless it is the first one, and everything is blank, and -# the scaffold doesn't have open_first_section set) -# + +# Every incorrect section is open that can be +# (unless it is the first one, and everything is blank, and +# the scaffold doesn't have open_first_section set) sub incorrect { my $section = shift; my $scaffold = $Scaffold::scaffold; @@ -793,24 +743,21 @@ sub incorrect { } return 1; } -# -# The first incorrect section is open that can be -# + +# The first incorrect section is open that can be sub first_incorrect { my $section = shift; return Section::is_open::incorrect($section) && Section::can_open::when_previous_correct($section); } -# -# All correct sections and the first incorrect section -# are open (that are allowed to be open) -# + +# All correct sections and the first incorrect section +# are open (that are allowed to be open) sub correct_or_first_incorrect { my $section = shift; - return 1 if $section->{is_correct} || Section::is_open::first_incorrect($section); + return $section->{is_correct} || Section::is_open::first_incorrect($section); } -# -# No sections are open -# + +# No sections are open sub never { return 0 } 1; diff --git a/macros/deprecated/Dartmouthmacros.pl b/macros/deprecated/Dartmouthmacros.pl index 09678528b2..5358d21549 100755 --- a/macros/deprecated/Dartmouthmacros.pl +++ b/macros/deprecated/Dartmouthmacros.pl @@ -1,9 +1,7 @@ #!/usr/bin/perl # this is equivalent to use strict, but can be used within the Safe compartment. -BEGIN { - be_strict; -} +BEGIN { strict->import; } ## Some local macros diff --git a/macros/deprecated/PGtextevaluators.pl b/macros/deprecated/PGtextevaluators.pl index f5609fe715..d56fcb7451 100644 --- a/macros/deprecated/PGtextevaluators.pl +++ b/macros/deprecated/PGtextevaluators.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -41,7 +41,7 @@ =head1 DESCRIPTION =cut -BEGIN { be_strict() } +BEGIN { strict->import; } # Until we get the PG cacheing business sorted out, we need to use # PG_restricted_eval to get the correct values for some(?) PG environment diff --git a/macros/core/compoundProblem.pl b/macros/deprecated/compoundProblem.pl similarity index 100% rename from macros/core/compoundProblem.pl rename to macros/deprecated/compoundProblem.pl diff --git a/macros/core/compoundProblem2.pl b/macros/deprecated/compoundProblem2.pl similarity index 100% rename from macros/core/compoundProblem2.pl rename to macros/deprecated/compoundProblem2.pl diff --git a/macros/core/compoundProblem5.pl b/macros/deprecated/compoundProblem5.pl similarity index 99% rename from macros/core/compoundProblem5.pl rename to macros/deprecated/compoundProblem5.pl index b81bd243f1..6b17841d39 100644 --- a/macros/core/compoundProblem5.pl +++ b/macros/deprecated/compoundProblem5.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -313,9 +313,7 @@ sub ans_evaluators { my $self = shift; my $section = $self->{sections}{ $self->{current_section} }; if ($section->{section_answers}) { - my $count = $main::PG->{unlabeled_answer_eval_count}; # Pitty that we have to grab this by hand - foreach my $evaluator (@_) { - my $name = main::ANS_NUM_TO_NAME(++$count); + foreach my $name (@{ $main::PG->{unlabeled_answer_name_stack} }) { push(@{ $self->{ans_names} }, $name); push(@{ $section->{section_answers} }, $name); } @@ -725,4 +723,3 @@ package main; sub INITIALIZE_SCAFFOLD { $Scaffold::scaffold->{oldstyle} = 1 } # backward compatibility 1; - diff --git a/macros/deprecated/problemPreserveAnswers.pl b/macros/deprecated/problemPreserveAnswers.pl index e2f1a27539..ab1bd44a34 100644 --- a/macros/deprecated/problemPreserveAnswers.pl +++ b/macros/deprecated/problemPreserveAnswers.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/deprecated/problemRandomize.pl b/macros/deprecated/problemRandomize.pl index 59698c755a..b563618a89 100644 --- a/macros/deprecated/problemRandomize.pl +++ b/macros/deprecated/problemRandomize.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/graph/AppletObjects.pl b/macros/graph/AppletObjects.pl index 68ce9a57d3..5af4c3df47 100644 --- a/macros/graph/AppletObjects.pl +++ b/macros/graph/AppletObjects.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/graph/LiveGraphics3D.pl b/macros/graph/LiveGraphics3D.pl index fee16e8d95..2d7fa3d24d 100644 --- a/macros/graph/LiveGraphics3D.pl +++ b/macros/graph/LiveGraphics3D.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -19,193 +19,166 @@ =head1 NAME =head1 DESCRIPTION +Macros for handling interactive 3D graphics. -Macros for handling interactive 3D graphics via the LiveGraphics3D -Java applet. The applet needs to be in the course html directory. -(If it is in the system html area, you will need to change the -default below or supply the jar option explicitly). +This parses LiveGraphics3D data into L +traces. See L for +information about the Mathematica syntax of the LiveGraphics3D format. Note that +not all of the syntax is supported by this macro. Instead of creating this data +directly, it is recommended to use one of the other LiveGraphics PG macros that +generate this data. See L, +L, L, +L, L, and +L. -The LiveGraphics3D applet displays a mathematica Graphics3D object -that is stored in a .m file (or a compressed one). Use Mathematica -to create one. (In the future, I plan to write a perl class that -will create these for you on the fly. -- DPVC) +=head1 METHODS -The main routines are +The following methods are provided. + +=head2 LiveGraphics3D + +Usage: C + +The following options can be given. =over -=item * C +=item * C<< file => name >> + +Name of the C<.m> file to load. -load a data file +=item * C<< archive => name >> -=item * C +Name of a C<.zip> file to load. If this is set, then the C option must +also be given, and must be set to the name of the file in the zip archive that +contains the data. -load raw Graphics3D data +=item * C<< input => 3Ddata >> -=item * C +String containing Graphics3D data to be displayed by the applet. -Options are from: +=item * C<< size => [w, h] >> - file => name name of .m file to load +Width and height of applet. - archive => name name of a .zip file to load +=item * C<< max_ticks => n >> - input => 3Ddata string containing Graphics3D data to - be displayed by the applet +Maximum number of ticks to show on the C, C, and C axes. This can be +given as a single positive integer, or can be a reference to an array of three +positive integers. - size => [w,h] width and height of applet +=item * C<< vars => [vars] >> - vars => [vars] hash of variables to pass as independent - variables to the applet, togther with - their initial values - e.g., vars => [a=>1,b=>1] +Hash of variables to pass as independent variables to the applet, together with +their initial values, e.g., C<< vars => [ a => 1, b => 1 ] >>. - depend => [list] list of dependent variables to pass to - the applet with their replacement strings - (see LiveGraphics3D documentation) +=item * C<< background => "#RRGGBB" >> - background=>"#RRGGBB" the background color to use (default is white) +The background color to use (default is white). - scale => n scaling factor for applet (default is 1.) +=item * C<< image => file >> - image => file a file containing an image to use in TeX mode - or when Java is disabled +A file containing an image to use in TeX mode. - tex_size => ratio a scaling factor for the TeX image (as a portion - of the line width). - 1000 is 100%, 500 is 50%, etc. +=item * C<< tex_size => ratio >> - tex_center => 0 or 1 center the image in TeX mode or not +A scaling factor for the TeX image (as a portion of the line width). 1000 is +100%, 500 is 50%, etc. - Live3D => [params] hash of additional parameters to pass to - the Live3D applet. - e.g. Live3D => [VISIBLE_FACES => "FRONT"] +=item * C<< tex_center => 0 or 1 >> + +Whether to center the image in TeX mode. =back +=head2 Live3Dfile + +Usage: C<< Live3Dfile($file, options) >> + +Load a data file. This just calls C with the C option set +to C<$file>. All other options supported by C can also be +given. + +=head2 Live3Ddata + +Usage: C<< Live3Ddata($input, options) >> + +Load raw Graphics3D data. This just calls C with the C +option set to C<$input>. All other options supported by C can +also be given. + =cut sub _LiveGraphics3D_init { - ADD_JS_FILE('node_modules/x3dom/x3dom.js'); - ADD_JS_FILE('node_modules/jszip/dist/jszip.min.js'); - ADD_JS_FILE('node_modules/jszip-utils/dist/jszip-utils.min.js'); - ADD_JS_FILE('js/LiveGraphics/liveGraphics.js'); - ADD_CSS_FILE('node_modules/x3dom/x3dom.css'); + ADD_JS_FILE('node_modules/plotly.js-dist-min/plotly.min.js', 0, { defer => undef }); + ADD_JS_FILE('node_modules/jszip/dist/jszip.min.js', 0, { defer => undef }); + ADD_JS_FILE('node_modules/jszip-utils/dist/jszip-utils.min.js', 0, { defer => undef }); + ADD_JS_FILE('js/LiveGraphics/liveGraphics.js', 0, { defer => undef }); + return; } sub LiveGraphics3D { my %options = ( size => [ 250, 250 ], - background => "#FFFFFF", - scale => 1., + background => '#FFFFFF', tex_size => 500, tex_center => 0, + max_ticks => 6, @_ ); - my $out = ""; - my $p; - my %pval; - my $ratio = $options{tex_size} * (.001); if ($main::displayMode eq "TeX") { - # - # In TeX mode, include the image, if there is one, or - # else give the user a message about using it on line - # + # In TeX mode, include the image, if there is one, or + # else give the user a message about using it on line. if ($options{image}) { - $out = "\\includegraphics[width=$ratio\\linewidth]{$options{image}}"; + my $ratio = $options{tex_size} * 0.001; + my $out = "\\includegraphics[width=$ratio\\linewidth]{$options{image}}"; $out = "\\centerline{$out}" if $options{tex_center}; $out .= "\n"; + return $out; } else { - $out = "\\vbox{ - \\hbox{[ This image is created by} - \\hbox{\\quad an interactive applet;} - \\hbox{you must view it on line ]} - }"; + return "[ This image is created by an interactive applet. You must view it on line. ]\n"; } - # In html mode check to see if we use javascript or not } else { my ($w, $h) = @{ $options{size} }; - $out .= $bHTML if ($main::displayMode eq "Latex2HTML"); - # - # Put the js in a table - # - $out .= qq{\n\n}; - $out .= qq{\n
                      }; - - $archive_input = $options{archive} // ''; - $file_input = $options{file} // ''; - $direct_input = $options{input} // ''; - - $direct_input =~ s/\n//g; - # - # include any independent variables - # - $ind_vars = '{}'; - - if ($options{vars}) { - $ind_vars = "{"; - %vars = @{ $options{vars} }; - - foreach $var (keys %vars) { - $ind_vars .= "\"$var\":\"" . $vars{$var} . "\","; - } - - $ind_vars .= "}"; - } - - $out .= < - var thisTD = jQuery('script:last').parent(); - var options = { width : $w, - height : $h, - file : '$file_input', - input : '$direct_input', - archive : '$archive_input', - vars : $ind_vars, - }; - - if (typeof LiveGraphics3D !== 'undefined') { - var graph = new LiveGraphics3D(thisTD[0],options); - } - - -EOS - - $out .= "
                      \n"; - $out .= $eHTML if ($main::displayMode eq "Latex2HTML"); - # otherwise use the applet + # Include independent variables. + my %vars; + %vars = @{ $options{vars} } if $options{vars}; + + return tag( + 'div', + class => 'live-graphics-3d-container', + style => "width:${w}px;height:${h}px;border:1px solid black;", + data_options => JSON->new->encode({ + width => $w - 2, + height => $h - 2, + maxTicks => $options{max_ticks}, + file => $options{file} // '', + input => ($options{input} // '') =~ s/\n//gr, + archive => $options{archive} // '', + vars => \%vars + }) + ); } - - return $out; } -# -# Syntactic sugar to make it easier to pass files and data to -# LiveGraphics3D. -# +# Syntactic sugar to make it easier to pass files and data to LiveGraphics3D. sub Live3Dfile { - my $file = shift; - LiveGraphics3D(file => $file, @_); + my ($file, %options) = @_; + return LiveGraphics3D(file => $file, %options); } -# -# Syntactic sugar to make it easier to pass raw Graohics3D data -# to LiveGraphics3D. -# +# Syntactic sugar to make it easier to pass raw Graohics3D data to LiveGraphics3D. sub Live3Ddata { - my $data = shift; - LiveGraphics3D(input => $data, @_); + my ($data, %options) = @_; + return LiveGraphics3D(input => $data, %options); } -# -# A message you can use for a caption under a graph -# -$LIVEMESSAGE = MODES( - TeX => '', - Latex2HTML => $BCENTER . "Drag the surface to rotate it" . $ECENTER, - HTML => $BCENTER . "Drag the surface to rotate it" . $ECENTER +# A message you can use for a caption under a graph. +$main::LIVEMESSAGE = MODES( + TeX => '', + HTML => $BCENTER . "Drag the surface to rotate it" . $ECENTER ); 1; diff --git a/macros/graph/LiveGraphicsCylindricalPlot3D.pl b/macros/graph/LiveGraphicsCylindricalPlot3D.pl index f15b2e9e98..02b9868b43 100644 --- a/macros/graph/LiveGraphicsCylindricalPlot3D.pl +++ b/macros/graph/LiveGraphicsCylindricalPlot3D.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -15,81 +15,133 @@ =head1 NAME -LiveGraphicsCylindricalPlot3D.pl - provide an interactive plot on a rectangular cylinder. +LiveGraphicsCylindricalPlot3D.pl - provide an interactive plot on a rectangular +cylinder. =head1 DESCRIPTION -C provides a macros for creating an -interactive plot of a function of two variables C -(where C is the radius and Cis the angle) via the C -javascript applet. The routine CC takes a C Formula -of two variables defined over an annular domain and some plot options -as input and returns a string of plot data that can be displayed -using the C routine of the C macro. +This macro provides the C method for creating an interactive +plot of a function of two variables C (where C is the radius and +C is the angle) via the C JavaScript applet. The method +takes a C Formula of two variables defined over an annular domain +and some plot options as input and returns a string of plot data that can be +displayed using the C routine of the L macro. -=head1 USAGE +=head1 METHODS - CylindricalPlot3D(options) +=head2 CylindricalPlot3D -Options are: +Usage: C - function => $f, $f is a MathObjects Formula - For example, in the setup section define +The available options are as follows. - Context("Numeric"); - Context()->variables->add(r=>"Real",t=>"Real"); - $a = random(1,3,1); - $f = Formula("$a*r + t"); # use double quotes! +=over - before calling CylindricalPlot3D() +=item C<< function => $f >> - rvar => "r", independent variable name, default "r" - tvar => "t", independent variable name, default "t" +C<$f> is a MathObject Formula. For example, in the setup section define - rmin => 0, domain for rvar - rmax => 3, + Context()->variables->are(r => 'Real', t => 'Real'); + $a = random(1, 3); + $f = Formula("$a * r + t"); # Use double quotes! - tmin => -3, domain for tvar - tmax => 3, +before calling C. - rsamples => 20, deltar = (rmax - rmin) / rsamples - tsamples => 20, deltat = (tmax - tmin) / tsamples +=item C<< rvar => 'r' >> - axesframed => 1, 1 displays framed axes, 0 hides framed axes +Radial independent variable name, default 'r'. This must correspond to the +radial variable used in the C. - xaxislabel => "X", Capital letters may be easier to read - yaxislabel => "Y", - zaxislabel => "Z", +=item C<< tvar => 't' >> - outputtype => 1, return string of only polygons (or mesh) - 2, return string of only plotoptions - 3, return string of polygons (or mesh) and plotoptions - 4, return complete plot +Angular independent variable name, default 't'. This must correspond to the +angular variable used in the C. +=item C<< rmin => -3 >> -=cut +Lower bound for the domain of the radial variable. -sub _LiveGraphicsCylindricalPlot3D_init { }; # don't reload this file +=item C<< rmax => 3 >> -loadMacros("MathObjects.pl", "LiveGraphics3D.pl"); +Upper bound for the domain of the radial variable. -$beginplot = "Graphics3D["; -$endplot = "]"; +=item C<< tmin => -3 >> -############################################# -# Begin CylindricalPlot3D +Lower bound for the domain of the angular variable. -sub CylindricalPlot3D { +=item C<< tmax => 3 >> + +Upper bound for the domain of the angular variable. + +=item C<< rsamples => 20 >> + +The number of sample values for the radial variable in the interval from C +to C to use. + +=item C<< tsamples => 20 >> + +The number of sample values for the angular variable in the interval from +C to C to use. + +=item C<< axesframed => 1 >> + +If set to 1 then the framed axes are displayed. If set to 0, the the framed +axes are not shown. This is 1 by default. + +=item C<< xaxislabel => 'x' >> + +Label for the axis corresponding to the first independent variable. + +=item C<< yaxislabel => 'y' >> + +Label for the axis corresponding to the second independent variable. + +=item C<< zaxislabel => 'z' >> + +Label for the axis corresponding to the dependent variable. + +=item C<< outputtype => 1 >> + +This determines what is contained in the string that the method returns. The +values of 1 through 4 are accepted, and have the following meaning. + +=over + +=item 1. + +Return a string of only polygons (or edge mesh). -############################################# - # - # Set default options - # +=item 2. +Return a string of only plot options. + +=item 3. + +Return a string of polygons (or edge mesh) and plot options. + +=item 4. + +Return the complete plot to be passed directly to the C method. + +=back + +=back + +=cut + +sub _LiveGraphicsCylindricalPlot3D_init { } + +loadMacros('MathObjects.pl', 'LiveGraphics3D.pl'); + +$main::beginplot = 'Graphics3D['; +$main::endplot = ']'; + +sub CylindricalPlot3D { + # Set default options. my %options = ( - function => Formula("1"), - rvar => "r", - tvar => "t", + function => Formula('1'), + rvar => 'r', + tvar => 't', rmin => 0.001, rmax => 3, tmin => 0, @@ -97,119 +149,70 @@ sub CylindricalPlot3D { rsamples => 20, tsamples => 20, axesframed => 1, - xaxislabel => "X", - yaxislabel => "Y", - zaxislabel => "Z", + xaxislabel => 'x', + yaxislabel => 'y', + zaxislabel => 'z', outputtype => 4, @_ ); - my $fsubroutine; - $options{function}->perlFunction('fsubroutine', [ "$options{rvar}", "$options{tvar}" ]); + $options{function}->perlFunction('fsubroutine', [ $options{rvar}, $options{tvar} ]); -###################################################### - # - # Generate a plotdata array, which has two indices - # - - my $rsamples1 = $options{rsamples} - 1; - my $tsamples1 = $options{tsamples} - 1; + # Generate a plotdata array, which has two indices. my $dr = ($options{rmax} - $options{rmin}) / $options{rsamples}; my $dt = ($options{tmax} - $options{tmin}) / $options{tsamples}; - my $r; - my $t; - - my $x; - my $y; - my $z; - - foreach my $i (0 .. $options{tsamples}) { - $t[$i] = $options{tmin} + $i * $dt; - foreach my $j (0 .. $options{rsamples}) { - $r[$j] = $options{rmin} + $j * $dr; - $x[$i][$j] = $r[$j] * cos($t[$i]); - $y[$i][$j] = $r[$j] * sin($t[$i]); - $z[$i][$j] = sprintf("%.3f", fsubroutine($r[$j], $t[$i])->value); - $x[$i][$j] = sprintf("%.3f", $x[$i][$j]); - $y[$i][$j] = sprintf("%.3f", $y[$i][$j]); + my (@x, @y, @z); + + for my $i (0 .. $options{tsamples}) { + my $t = $options{tmin} + $i * $dt; + for my $j (0 .. $options{rsamples}) { + my $r = $options{rmin} + $j * $dr; + $x[$i][$j] = $r * cos($t); + $y[$i][$j] = $r * sin($t); + $z[$i][$j] = sprintf('%.3f', fsubroutine($r, $t)->value); + $x[$i][$j] = sprintf('%.3f', $x[$i][$j]); + $y[$i][$j] = sprintf('%.3f', $y[$i][$j]); } } -########################################################################### - # - # Generate a plotstring from the plotdata. - # - # The plotstring is a list of polygons that - # LiveGraphics3D reads as input. - # - # For more information on the format of the plotstring, see - # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html - # http://www.vis.uni-stuttgart.de/~kraus/LiveGraphics3D/documentation.html - # -########################################### - # - # Generate the polygons in the plotstring - # - - my $plotstructure = "{"; - - foreach my $i (0 .. $tsamples1) { - foreach my $j (0 .. $rsamples1) { - - $plotstructure = - $plotstructure - . "Polygon[{" - . "{$x[$i][$j],$y[$i][$j],$z[$i][$j]}," - . "{$x[$i+1][$j],$y[$i+1][$j],$z[$i+1][$j]}," - . "{$x[$i+1][$j+1],$y[$i+1][$j+1],$z[$i+1][$j+1]}," - . "{$x[$i][$j+1],$y[$i][$j+1],$z[$i][$j+1]}" . "}]"; - - if (($i < $tsamples1) || ($j < $rsamples1)) { - $plotstructure = $plotstructure . ","; - } - + # Generate a plotstring from the plotdata. This is a list of polygons that LiveGraphics3D reads as input. + # For more information on the format of the plotstring, see + # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html + + # Generate the polygons in the plotstring. + my @polygons; + for my $i (0 .. $options{tsamples} - 1) { + for my $j (0 .. $options{rsamples} - 1) { + push(@polygons, + 'Polygon[{' + . "{$x[$i][$j],$y[$i][$j],$z[$i][$j]}," + . "{$x[$i+1][$j],$y[$i+1][$j],$z[$i+1][$j]}," + . "{$x[$i+1][$j+1],$y[$i+1][$j+1],$z[$i+1][$j+1]}," + . "{$x[$i][$j+1],$y[$i][$j+1],$z[$i][$j+1]}" + . '}]'); } } - $plotstructure = $plotstructure . "}"; + my $plotstructure = '{' . join(',', @polygons) . '}'; -############################################## - # - # Add plot options to the plotoptions string - # - - my $plotoptions = ""; - - if (($options{outputtype} > 1) || ($options{axesframed} == 1)) { - $plotoptions = - $plotoptions - . "Axes->True,AxesLabel->" - . "{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}"; - } - -#################################################### - # - # Return only the plotstring (if outputtype=>1), - # or only plotoptions (if outputtype=>2), - # or plotstring, plotoptions (if outputtype=>2), - # or the entire plot (default) (if outputtype=>4) + my $plotoptions = + $options{outputtype} > 1 && $options{axesframed} == 1 + ? "Axes->True,AxesLabel->{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}" + : ''; if ($options{outputtype} == 1) { return $plotstructure; } elsif ($options{outputtype} == 2) { return $plotoptions; } elsif ($options{outputtype} == 3) { - return "{" . $plotstructure . "," . $plotoptions . "}"; + return "{$plotstructure,$plotoptions}"; } elsif ($options{outputtype} == 4) { - return $beginplot . $plotstructure . "," . $plotoptions . $endplot; + return "${main::beginplot}${plotstructure},${plotoptions}${main::endplot}"; } else { - return "Invalid outputtype (outputtype should be a number 1 through 4)."; + return 'Invalid outputtype (outputtype should be a number 1 through 4).'; } - -} # End CylindricalPlot3D -##################################################### -##################################################### +} 1; diff --git a/macros/graph/LiveGraphicsParametricCurve3D.pl b/macros/graph/LiveGraphicsParametricCurve3D.pl index 9f5b72cfc4..862ac8157d 100644 --- a/macros/graph/LiveGraphicsParametricCurve3D.pl +++ b/macros/graph/LiveGraphicsParametricCurve3D.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -19,185 +19,179 @@ =head1 NAME =head1 DESCRIPTION -C provides a macros for creating an -interactive plot of a vector field via the C Javascript applet. -The routine C takes three C Formulas of -3 variables as input and returns a string of plot data that can be -displayed using the C routine of the C macro. +This macro provides the C method for creating an interactive +plot of a vector field via the C JavaScript applet. The method +takes three C Formulas in one variable as input and returns a string +of plot data that can be displayed using the C routine of the +L macro. -=head1 USAGE +=head1 METHODS - ParametricCurve3D(options); +=head2 ParametricCurve3D -Options are: +Usage: C - Fx => Formula("t*cos(t)"), F = < Fx, Fy, Fz > where Fx, Fy, Fz are each - Fy => Formula("t*sin(t)"), functions of tvar - Fz => Formula("t"), +The available options are as follows. - tvar => "t", independent variable name, default "t" - tmin => -3, domain for tvar - tmax => 3, - tsamples => 3, deltat = (tmax - tmin) / tsamples +=over - axesframed => 1, 1 displays framed axes, 0 hides framed axes +=item C<< Fx => Formula('t * cos(t)') >> - xaxislabel => "X", Capital letters may be easier to read - yaxislabel => "Y", - zaxislabel => "Z", +Parametric function for the C-coordinate. - orientation => 0, do not show any orientation arrows - => 1, show only one arrow in the middle - => 2, make the curve entirely of arrows +=item C<< Fy => Formula('t * sin(t)') >> - curvecolor => "RGBColor[1.0,0.0,0.0]", - curvethickness => 0.001, +Parametric function for the C-coordinate. - outputtype => 1, return string of only polygons (or mesh) - 2, return string of only plotoptions - 3, return string of polygons (or mesh) and plotoptions - 4, return complete plot +=item C<< Fz => Formula('t') >> -=cut +Parametric function for the C-coordinate. -sub _LiveGraphicsParametricCurve3D_init { }; # don't reload this file +=item C<< tvar => 't' >> -loadMacros("MathObjects.pl", "LiveGraphics3D.pl"); +Parameter name, default 't'. This must correspond to the parameter used in +C, C, and C. -$beginplot = "Graphics3D["; -$endplot = "]"; +=item C<< tmin => -3 >> -########################################### -########################################### -# Begin ParametricCurve3D +Lower bound for the domain of the parameter. -sub ParametricCurve3D { +=item C<< tmax => 3 >> + +Upper bound for the domain of the parameter. + +=item C<< tsamples => 3 >> + +The number of sample values for the parameter in the interval from C to +C to use. + +=item C<< axesframed => 1 >> + +If set to 1 then the framed axes are displayed. If set to 0, the the framed +axes are not shown. This is 1 by default. + +=item C<< xaxislabel => 'x' >> + +Label for the axis corresponding to the first independent variable. + +=item C<< yaxislabel => 'y' >> + +Label for the axis corresponding to the second independent variable. + +=item C<< zaxislabel => 'z' >> + +Label for the axis corresponding to the dependent variable. + +=item C<< curvecolor => 'RGBColor[1.0, 0.0, 0.0]' >> + +The color of the curve. + +=item C<< curvethickness => 0.001 >> + +The curve thickness. + +=item C<< outputtype => 1 >> + +This determines what is contained in the string that the method returns. The +values of 1 through 4 are accepted, and have the following meaning. + +=over + +=item 1. + +Return a string of only polygons (or edge mesh). + +=item 2. + +Return a string of only plot options. + +=item 3. + +Return a string of polygons (or edge mesh) and plot options. -########################################### - # - # Set default options - # +=item 4. +Return the complete plot to be passed directly to the C method. + +=back + +=back + +=cut + +sub _LiveGraphicsParametricCurve3D_init { } + +loadMacros('MathObjects.pl', 'LiveGraphics3D.pl'); + +$main::beginplot = 'Graphics3D['; +$main::endplot = ']'; + +sub ParametricCurve3D { + # Set default options. my %options = ( - Fx => Formula("1"), - Fy => Formula("1"), - Fz => Formula("1"), + Fx => Formula('1'), + Fy => Formula('1'), + Fz => Formula('1'), tvar => 't', tmin => -3, tmax => 3, tsamples => 20, orientation => 0, axesframed => 1, - xaxislabel => "X", - yaxislabel => "Y", - zaxislabel => "Z", - curvecolor => "RGBColor[1.0,0.0,0.0]", + xaxislabel => 'x', + yaxislabel => 'y', + zaxislabel => 'z', + curvecolor => 'RGBColor[1.0,0.0,0.0]', curvethickness => 0.001, outputtype => 4, @_ ); - my $Fxsubroutine; - my $Fysubroutine; - my $Fzsubroutine; - - $options{Fx}->perlFunction('Fxsubroutine', ["$options{tvar}"]); - $options{Fy}->perlFunction('Fysubroutine', ["$options{tvar}"]); - $options{Fz}->perlFunction('Fzsubroutine', ["$options{tvar}"]); + $options{Fx}->perlFunction('Fxsubroutine', [ $options{tvar} ]); + $options{Fy}->perlFunction('Fysubroutine', [ $options{tvar} ]); + $options{Fz}->perlFunction('Fzsubroutine', [ $options{tvar} ]); -###################################################### - # - # Generate plot data - # + # Generate plot data. my $dt = ($options{tmax} - $options{tmin}) / $options{tsamples}; - my $t; + my (@Fx, @Fy, @Fz); # The curve data - foreach my $i (0 .. $options{tsamples}) { - $t[$i] = $options{tmin} + $i * $dt; - - $FX[$i] = sprintf("%.3f", (Fxsubroutine($t[$i])->value)); - $FY[$i] = sprintf("%.3f", (Fysubroutine($t[$i])->value)); - $FZ[$i] = sprintf("%.3f", (Fzsubroutine($t[$i])->value)); - + for my $i (0 .. $options{tsamples}) { + my $t = $options{tmin} + $i * $dt; + $Fx[$i] = sprintf('%.3f', Fxsubroutine($t)->value); + $Fy[$i] = sprintf('%.3f', Fysubroutine($t)->value); + $Fz[$i] = sprintf('%.3f', Fzsubroutine($t)->value); } - if ($options{orientation} > 0) { - # - # The arrow head data - # - my $tmidindex = sprintf("%.0f", $options{tsamples} / 2); + # Generate plotstructure from the plotdata. This is a list of lines that LiveGraphics3D reads as input. For more + # information on the format of the plotstructure, see http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html. + # Generate the line segments in the plotstructure. + my @lines; + for my $i (0 .. $options{tsamples} - 1) { + push(@lines, "Line[{{$Fx[$i],$Fy[$i],$Fz[$i]},{$Fx[$i+1],$Fy[$i+1],$Fz[$i+1]}}]"); } -########################################################################### - # - # Generate plotstructure from the plotdata. - # - # The plotstucture is a list of arrows (made of lines) that - # LiveGraphics3D reads as input. - # - # For more information on the format of the plotstructure, see - # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html - # http://www.vis.uni-stuttgart.de/~kraus/LiveGraphics3D/documentation.html - # -########################################### - # - # Generate the line segments in the plotstructure - # - - my $plotstructure = "{$options{curvecolor},Thickness[$options{curvethickness}],"; + my $plotstructure = "{$options{curvecolor},Thickness[$options{curvethickness}]," . join(',', @lines) . '}'; - my $tsamples1 = $options{tsamples} - 1; - - foreach my $i (0 .. $tsamples1) { - - $plotstructure = - $plotstructure . "Line[{" . "{$FX[$i],$FY[$i],$FZ[$i]}," . "{$FX[$i+1],$FY[$i+1],$FZ[$i+1]}" . "}]"; - - if ($i < $tsamples1) { $plotstructure = $plotstructure . "," } - - } - - $plotstructure = $plotstructure . "}"; - -############################################## - # - # Add plot options to the plotoptions string - # - - my $plotoptions = ""; - - if (($options{outputtype} > 1) || ($options{axesframed} == 1)) { - $plotoptions = - $plotoptions - . "Axes->True,AxesLabel->" - . "{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}"; - } - -#################################################### - # - # Return only the plotstring (if outputtype=>1), - # or only plotoptions (if outputtype=>2), - # or plotstring, plotoptions (if outputtype=>2), - # or the entire plot (default) (if outputtype=>4) + my $plotoptions = + $options{outputtype} > 1 && $options{axesframed} == 1 + ? "Axes->True,AxesLabel->{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}" + : ''; if ($options{outputtype} == 1) { return $plotstructure; } elsif ($options{outputtype} == 2) { return $plotoptions; } elsif ($options{outputtype} == 3) { - return "{" . $plotstructure . "," . $plotoptions . "}"; + return "{$plotstructure,$plotoptions}"; } elsif ($options{outputtype} == 4) { - return $beginplot . $plotstructure . "," . $plotoptions . $endplot; + return "${main::beginplot}${plotstructure},${plotoptions}${main::endplot}"; } else { - return "Invalid outputtype (outputtype should be a number 1 through 4)."; + return 'Invalid outputtype (outputtype should be a number 1 through 4).'; } - -} # End ParametricCurve3D -############################################## -############################################## +} 1; diff --git a/macros/graph/LiveGraphicsParametricSurface3D.pl b/macros/graph/LiveGraphicsParametricSurface3D.pl index 185c49cd80..e93b54b8e6 100644 --- a/macros/graph/LiveGraphicsParametricSurface3D.pl +++ b/macros/graph/LiveGraphicsParametricSurface3D.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -15,83 +15,164 @@ =head1 NAME -LiveGraphicsParametricSurface3D.pl - provide an interactive plot of a parametric surface. +LiveGraphicsParametricSurface3D.pl - provide an interactive plot of a parametric +surface. =head1 DESCRIPTION -C provides a macro for creating an -interactive plot of a parametric surface via the C Javascript applet. -The routine C takes three C Formulas of -2 variables as input and returns a string of plot data that can be -displayed using the C routine of the C macro. +This macro provides the C method for creating an +interactive plot of a parametric surface via the C JavaScript +applet. The method takes three C Formulas in two variables as input +and returns a string of plot data that can be displayed using the C +routine of the L macro. -=head1 USAGE +=head1 Methods - ParametricSurface3D(options); +=head2 ParametricSurface3D -Options are: +Usage: C - Fx => Formula("cos(u)*cos(v)"), x-coordinate function - Fy => Formula("sin(u)*cos(v)"), y-coordinate function - Fz => Formula("sin(v)"), z-coordinate function - F(u,v) = < Fx, Fy, Fz > - = < Fx(u,v), Fy(u,v), Fz(u,v) > +The available options are as follows. - uvar => "u", parameter name, default "u" - vvar => "v", parameter name, default "v" +=over - umin => -3, domain for uvar - umax => 3, +=item C<< Fx => Formula('cos(u) * cos(v)') >> - vmin => -3, domain for vvar - vmax => 3, +Parametric function for the C-coordinate. - usamples => 3, deltau = (umax - umin) / usamples - vsamples => 3, deltav = (vmax - vmin) / vsamples +=item C<< Fy => Formula('sin(u) * cos(v)') >> - axesframed => 1, 1 displays framed axes, 0 hides framed axes +Parametric function for the C-coordinate. - xaxislabel => "X", Capital letters may be easier to read - yaxislabel => "Y", - zaxislabel => "Z", +=item C<< Fz => Formula('sin(v)') >> - edges => 0, 1 displays edges of polygons, 0 hides them - edgecolor => "RGBColor[0.2,0.2,0.2]", - edgethickness => "Thickness[0.001]", +Parametric function for the C-coordinate. - mesh => 0, 1 displays open mesh, 0 displays filled polygons - meshcolor => "RGBColor[0.7,0.7,0.7]", three values between 0 and 1 - meshthickness => 0.001, +=item C<< uvar => 'u' >> - outputtype => 1, return string of only polygons (or mesh) - 2, return string of only plotoptions - 3, return string of polygons (or mesh) and plotoptions - 4, return complete plot +The first parameter, default 'u'. This must correspond to the first parameter +used in C, C, and C. -=cut +=item C<< vvar => 'v' >> -sub _LiveGraphicsParametricSurface3D_init { }; # don't reload this file +The second parameter, default 'v'. This must correspond to the second parameter +used in C, C, and C. -loadMacros("MathObjects.pl", "LiveGraphics3D.pl"); +=item C<< umin => -3 >> -$beginplot = "Graphics3D["; -$endplot = "]"; +Lower bound for the domain of the first parameter. -########################################### -########################################### -# Begin ParametricSurface3D +=item C<< umax => 3 >> -sub ParametricSurface3D { +Upper bound for the domain of the first parameter. + +=item C<< vmin => -3 >> + +Lower bound for the domain of the second parameter. + +=item C<< vmax => 3 >> + +Upper bound for the domain of the second parameter. + +=item C<< usamples => 3 >> + +The number of sample values for the first parameter in the interval from C +to C to use. + +=item C<< vsamples => 3 >> + +The number of sample values for the second parameter in the interval from +C to C to use. + +=item C<< axesframed => 1 >> + +If set to 1 then the framed axes are displayed. If set to 0, the the framed +axes are not shown. This is 1 by default. + +=item C<< xaxislabel => 'x' >> + +Label for the axis corresponding to the first independent variable. + +=item C<< yaxislabel => 'y' >> + +Label for the axis corresponding to the second independent variable. + +=item C<< zaxislabel => 'z' >> + +Label for the axis corresponding to the dependent variable. + +=item C<< edges => 1 >> + +If set to 1, then the edges of the polygons are shown. If set to 0, then the +edges are not shown. This is 1 by default. + +=item C<< edgecolor => 'RGBColor[0.2, 0.2, 0.2]' >> + +The color of the edges if C is 1. + +=item C<< edgethickness => 'Thickness[0.001]' >> + +The thickness of the edges if C is 1. + +=item C<< mesh => 0 >> + +If set to 1, then the the edge mesh is shown and the polygons for the surface +are not filled. If set to 0, then the polygons for the surface are filled. The +edge mesh can also be shown in this case by setting C to 1. This is 0 by +default. + +=item C<< meshcolor => 'RGBColor[0.7, 0.7, 0.7]' >> + +The red, green, and blue colors each from 0 to 1 to be combined to form the +color of the mesh. If this is set and C is 1, then this will be the color +of the mesh edges. + +=item C<< meshthickness => 0.001 >> + +The thickness of the mesh edges if C is 1. + +=item C<< outputtype => 1 >> + +This determines what is contained in the string that the method returns. The +values of 1 through 4 are accepted, and have the following meaning. + +=over + +=item 1. + +Return a string of only polygons (or edge mesh). -########################################### - # - # Set default options - # +=item 2. +Return a string of only plot options. + +=item 3. + +Return a string of polygons (or edge mesh) and plot options. + +=item 4. + +Return the complete plot to be passed directly to the C method. + +=back + +=back + +=cut + +sub _LiveGraphicsParametricSurface3D_init { } + +loadMacros('MathObjects.pl', 'LiveGraphics3D.pl'); + +$main::beginplot = 'Graphics3D['; +$main::endplot = ']'; + +sub ParametricSurface3D { + # Set default options. my %options = ( - Fx => Formula("1"), - Fy => Formula("1"), - Fz => Formula("1"), + Fx => Formula('1'), + Fy => Formula('1'), + Fz => Formula('1'), uvar => 'u', vvar => 'v', umin => -3, @@ -101,166 +182,103 @@ sub ParametricSurface3D { usamples => 20, vsamples => 20, axesframed => 1, - xaxislabel => "X", - yaxislabel => "Y", - zaxislabel => "Z", - edges => 0, - edgecolor => "RGBColor[0.2,0.2,0.2]", - edgethickness => "Thickness[0.001]", + xaxislabel => 'x', + yaxislabel => 'y', + zaxislabel => 'z', + edges => 1, + edgecolor => 'RGBColor[0.2,0.2,0.2]', + edgethickness => 'Thickness[0.001]', mesh => 0, - meshcolor => "RGBColor[0.7,0.7,0.7]", + meshcolor => 'RGBColor[0.7,0.7,0.7]', meshthickness => 0.001, outputtype => 4, @_ ); - my $Fxsubroutine; - my $Fysubroutine; - my $Fzsubroutine; + $options{Fx}->perlFunction('Fxsubroutine', [ $options{uvar}, $options{vvar} ]); + $options{Fy}->perlFunction('Fysubroutine', [ $options{uvar}, $options{vvar} ]); + $options{Fz}->perlFunction('Fzsubroutine', [ $options{uvar}, $options{vvar} ]); - $options{Fx}->perlFunction('Fxsubroutine', [ "$options{uvar}", "$options{vvar}" ]); - $options{Fy}->perlFunction('Fysubroutine', [ "$options{uvar}", "$options{vvar}" ]); - $options{Fz}->perlFunction('Fzsubroutine', [ "$options{uvar}", "$options{vvar}" ]); - -###################################################### - # - # Generate plot data - # + # Generate plot data. my $du = ($options{umax} - $options{umin}) / $options{usamples}; my $dv = ($options{vmax} - $options{vmin}) / $options{vsamples}; - my $u; - my $v; - - foreach my $i (0 .. $options{usamples}) { - $u[$i] = $options{umin} + $i * $du; - foreach my $j (0 .. $options{vsamples}) { - $v[$j] = $options{vmin} + $j * $dv; + my (@Fx, @Fy, @Fz); - $FX[$i][$j] = sprintf("%.3f", (Fxsubroutine($u[$i], $v[$j])->value)); - $FY[$i][$j] = sprintf("%.3f", (Fysubroutine($u[$i], $v[$j])->value)); - $FZ[$i][$j] = sprintf("%.3f", (Fzsubroutine($u[$i], $v[$j])->value)); - - $u[$i] = sprintf("%.3f", $u[$i]); - $v[$j] = sprintf("%.3f", $v[$j]); + for my $i (0 .. $options{usamples}) { + my $u = $options{umin} + $i * $du; + for my $j (0 .. $options{vsamples}) { + my $v = $options{vmin} + $j * $dv; + $Fx[$i][$j] = sprintf('%.3f', Fxsubroutine($u, $v)->value); + $Fy[$i][$j] = sprintf('%.3f', Fysubroutine($u, $v)->value); + $Fz[$i][$j] = sprintf('%.3f', Fzsubroutine($u, $v)->value); } } -########################################################################### - # - # Generate plotstructure from the plotdata. - # - # The plotstucture is a list of arrows (made of lines) that - # LiveGraphics3D reads as input. - # - # For more information on the format of the plotstructure, see - # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html - # http://www.vis.uni-stuttgart.de/~kraus/LiveGraphics3D/documentation.html - # -########################################### - # - # Generate the polygons in the plotstructure - # - - my $plotstructure = "{"; + # Generate plotstructure from the plotdata. This is a list of arrows (made of lines) that LiveGraphics3D reads as + # input. For more information on the format of the plotstructure, see + # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html + + my $plotstructure = '{'; if ($options{edges} == 0 && $options{mesh} == 0) { - $plotstructure = $plotstructure . "EdgeForm[],"; + $plotstructure .= 'EdgeForm[],'; } elsif ($options{edges} == 1 && $options{mesh} == 0) { - $plotstructure = $plotstructure . "EdgeForm[{$options{edgecolor},$options{edgethickness}}],"; + $plotstructure .= "EdgeForm[{$options{edgecolor},$options{edgethickness}}],"; } if ($options{mesh} == 1) { - $plotstructure = $plotstructure . "$options{meshcolor},Thickness[$options{meshthickness}],"; + $plotstructure .= "$options{meshcolor},Thickness[$options{meshthickness}],"; } - my $usamples1 = $options{usamples} - 1; - my $vsamples1 = $options{vsamples} - 1; - + # Generate the polygons or lines in the plotstructure. + my @objects; if ($options{mesh} == 0) { - - foreach my $i (0 .. $usamples1) { - foreach my $j (0 .. $vsamples1) { - - $plotstructure = - $plotstructure - . "Polygon[{" - . "{$FX[$i][$j],$FY[$i][$j],$FZ[$i][$j]}," - . "{$FX[$i+1][$j],$FY[$i+1][$j],$FZ[$i+1][$j]}," - . "{$FX[$i+1][$j+1],$FY[$i+1][$j+1],$FZ[$i+1][$j+1]}," - . "{$FX[$i][$j+1],$FY[$i][$j+1],$FZ[$i][$j+1]}" . "}]"; - - if (($i < $usamples1) || ($j < $vsamples1)) { - $plotstructure = $plotstructure . ","; - } - + for my $i (0 .. $options{usamples} - 1) { + for my $j (0 .. $options{vsamples} - 1) { + push(@objects, + 'Polygon[{' + . "{$Fx[$i][$j],$Fy[$i][$j],$Fz[$i][$j]}," + . "{$Fx[$i+1][$j],$Fy[$i+1][$j],$Fz[$i+1][$j]}," + . "{$Fx[$i+1][$j+1],$Fy[$i+1][$j+1],$Fz[$i+1][$j+1]}," + . "{$Fx[$i][$j+1],$Fy[$i][$j+1],$Fz[$i][$j+1]}" + . '}]'); } } - - # end mesh == 0 } else { - # begin mesh == 1 - - foreach my $i (0 .. $usamples1) { - foreach my $j (0 .. $vsamples1) { - - # this could be made more efficient - $plotstructure = - $plotstructure - . "Line[{" - . "{$FX[$i][$j],$FY[$i][$j],$FZ[$i][$j]}," - . "{$FX[$i+1][$j],$FY[$i+1][$j],$FZ[$i+1][$j]}," - . "{$FX[$i+1][$j+1],$FY[$i+1][$j+1],$FZ[$i+1][$j+1]}," - . "{$FX[$i][$j+1],$FY[$i][$j+1],$FZ[$i][$j+1]}," - . "{$FX[$i][$j],$FY[$i][$j],$FZ[$i][$j]}" . "}]"; - - if (($i < $usamples1) || ($j < $vsamples1)) { - $plotstructure = $plotstructure . ","; - } - + for my $i (0 .. $options{usamples} - 1) { + for my $j (0 .. $options{vsamples} - 1) { + push(@objects, + 'Line[{' + . "{$Fx[$i][$j],$Fy[$i][$j],$Fz[$i][$j]}," + . "{$Fx[$i+1][$j],$Fy[$i+1][$j],$Fz[$i+1][$j]}," + . "{$Fx[$i+1][$j+1],$Fy[$i+1][$j+1],$Fz[$i+1][$j+1]}," + . "{$Fx[$i][$j+1],$Fy[$i][$j+1],$Fz[$i][$j+1]}," + . "{$Fx[$i][$j],$Fy[$i][$j],$Fz[$i][$j]}" + . '}]'); } } - - } # end mesh == 1 - - $plotstructure = $plotstructure . "}"; - -############################################## - # - # Add plot options to the plotoptions string - # - - my $plotoptions = ""; - - if (($options{outputtype} > 1) || ($options{axesframed} == 1)) { - $plotoptions = - $plotoptions - . "Axes->True,AxesLabel->" - . "{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}"; } -#################################################### - # - # Return only the plotstring (if outputtype=>1), - # or only plotoptions (if outputtype=>2), - # or plotstring, plotoptions (if outputtype=>2), - # or the entire plot (default) (if outputtype=>4) + $plotstructure .= join(',', @objects) . '}'; + + my $plotoptions = + $options{outputtype} > 1 && $options{axesframed} == 1 + ? "Axes->True,AxesLabel->{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}" + : ''; if ($options{outputtype} == 1) { return $plotstructure; } elsif ($options{outputtype} == 2) { return $plotoptions; } elsif ($options{outputtype} == 3) { - return "{" . $plotstructure . "," . $plotoptions . "}"; + return "{$plotstructure,$plotoptions}"; } elsif ($options{outputtype} == 4) { - return $beginplot . $plotstructure . "," . $plotoptions . $endplot; + return "${main::beginplot}${plotstructure},${plotoptions}${main::endplot}"; } else { - return "Invalid outputtype (outputtype should be a number 1 through 4)."; + return 'Invalid outputtype (outputtype should be a number 1 through 4).'; } - -} # End ParametricSurface3D -############################################## -############################################## +} 1; diff --git a/macros/graph/LiveGraphicsRectangularPlot3D.pl b/macros/graph/LiveGraphicsRectangularPlot3D.pl index 661331eaf6..bb9e40cb77 100644 --- a/macros/graph/LiveGraphicsRectangularPlot3D.pl +++ b/macros/graph/LiveGraphicsRectangularPlot3D.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -19,123 +19,227 @@ =head1 NAME =head1 DESCRIPTION -C provides two macros for creating an -interactive plot of a function of two variables C in -Rectangular (Cartesian) coordinates via the C Javascript applet. -The routine C takes a MathObject Formula of -two variables defined over a rectangular domain and some plot options -as input and returns a string of plot data that can be displayed -using the C routine of the C macro. -The routine C works similarly for a function -C over an annular domain specified in polar by -C>> and C>>. +This macro provides two methods for creating an interactive plot of a function +of two variables C in rectangular (Cartesian) coordinates via the +C JavaScript applet. The routine +L takes a C Formula of two +variables defined over a rectangular domain and some plot options as input and +returns a string of plot data that can be displayed using the C +routine of the L macro. The routine +L works similarly for a function C +over an annular domain specified in polar coordinates by C<< rmin < r < rmax >> +and C<< tmin < theta < tmax >> (polar coordinates are converted to rectangular +for evaluation of the function). -=head1 USAGE +=head1 METHODS + +=head2 RectangularPlot3DRectangularDomain + +Usage: C + +The available options are as follows. =over -=item C +=item C<< function => $f >> + +C<$f> is a MathObject Formula. For example, in the setup section define + + Context()->variables->are(x => 'Real', y => 'Real'); + $a = random(1, 3); + $f = Formula("$a * x^2 - 2 * y"); # Use double quotes! + +before calling C. + +=item C<< xvar => 'x' >> -Options are: +First independent variable name, default 'x'. This must correspond to the first +variable used in the C. - function => $f, $f is a MathObjects Formula - For example, in the setup section define +=item C<< yvar => 'y' >> - Context("Numeric"); - Context()->variables->add(s=>"Real",t=>"Real"); - $a = random(1,3,1); - $f = Formula("$a*s^2-2*t"); # use double quotes! +Second independent variable name, default 'y'. This must correspond to the +second variable used in the C. - before calling RectangularPlot3DRectangularDomain() +=item C<< xmin => -3 >> - xvar => "s", independent variable name, default "x" - yvar => "t", independent variable name, default "y" +Lower bound for the domain of the first independent variable. - xmin => -3, domain for xvar - xmax => 3, +=item C<< xmax => 3 >> - ymin => -3, domain for yvar - ymax => 3, +Upper bound for the domain of the first independent variable. - xsamples => 20, deltax = (xmax - xmin) / xsamples - ysamples => 20, deltay = (ymax - ymin) / ysamples +=item C<< ymin => -3 >> - axesframed => 1, 1 displays framed axes, 0 hides framed axes +Lower bound for the domain of the second independent variable. - xaxislabel => "S", Capital letters may be easier to read - yaxislabel => "T", - zaxislabel => "Z", +=item C<< ymax => 3 >> - outputtype => 1, return string of only polygons (or mesh) - 2, return string of only plotoptions - 3, return string of polygons (or mesh) and plotoptions - 4, return complete plot +Upper bound for the domain of the second independent variable. -=item C +=item C<< xsamples => 20 >> -Options are: +The number of sample values for the first independent variable in the interval +from C to C to use. +=item C<< ysamples => 20 >> - function => $f, $f is a MathObjects Formula - For example, in the setup section define +The number of sample values for the second independent variable in the interval +from C to C to use. - Context("Numeric"); - Context()->variables->add(y=>"Real",r=>"Real",t=>"Real"); - $a = random(1,3,1); - $f = Formula("$a*e^(- x^2 - y^2)"); # use double quotes! +=item C<< axesframed => 1 >> - before calling RectangularPlot3DAnnularDomain() +If set to 1 then the framed axes are displayed. If set to 0, the the framed +axes are not shown. This is 1 by default. - xvar => "x", independent variable name, default "x" - yvar => "y", independent variable name, default "y" +=item C<< xaxislabel => 'x' >> - rvar => "r", independent variable name, default "r" - tvar => "t", independent variable name, default "t" (for theta) +Label for the axis corresponding to the first independent variable. - rmin => -3, domain for rvar - rmax => 3, +=item C<< yaxislabel => 'y' >> - tmin => -3, domain for tvar - tmax => 3, +Label for the axis corresponding to the second independent variable. + +=item C<< zaxislabel => 'z' >> + +Label for the axis corresponding to the dependent variable. + +=item C<< outputtype => 1 >> + +This determines what is contained in the string that the method returns. The +values of 1 through 4 are accepted, and have the following meaning. + +=over - rsamples => 20, deltar = (rmax - rmin) / rsamples - tsamples => 20, deltat = (tmax - tmin) / tsamples +=item 1. - axesframed => 1, 1 displays framed axes, 0 hides framed axes +Return a string of only polygons (or edge mesh). - xaxislabel => "X", Capital letters may be easier to read - yaxislabel => "Y", - zaxislabel => "Z", +=item 2. - outputtype => 1, return string of only polygons (or mesh) - 2, return string of only plotoptions - 3, return string of polygons (or mesh) and plotoptions - 4, return complete plot +Return a string of only plot options. + +=item 3. + +Return a string of polygons (or edge mesh) and plot options. + +=item 4. + +Return the complete plot to be passed directly to the C method. =back -=cut +=back -sub _LiveGraphicsRectangularPlot3D_init { }; # don't reload this file +=head2 RectangularPlot3DAnnularDomain -loadMacros("MathObjects.pl", "LiveGraphics3D.pl"); +Usage: C -$beginplot = "Graphics3D["; -$endplot = "]"; +The available options are as follows. -########################################### -########################################### -# Begin RectangularPlot3DRectangularDomain +=over -sub RectangularPlot3DRectangularDomain { +=item C<< function => $f >> + +C<$f> is a MathObject Formula. For example, in the setup section define + + Context()->variables->are(x => 'Real', y => 'Real'); + $a = random(1, 3); + $f = Formula("$a * e^(-x^2 - y^2)"); # Use double quotes! + +before calling C. + +=item C<< xvar => 'x' >> + +First independent variable name, default 'x'. This must correspond to the first +variable used in the C. + +=item C<< yvar => 'y' >> + +Second independent variable name, default 'y'. This must correspond to the +second variable used in the C. + +=item C<< rmin => -3 >> + +Lower bound for the domain of radial coordinate. + +=item C<< rmax => 3 >> + +Upper bound for the domain of radial coordinate. + +=item C<< tmin => -3 >> + +Lower bound for the domain of angular coordinate. + +=item C<< tmax => 3 >> + +Upper bound for the domain of angular coordinate. + +=item C<< rsamples => 20 >> + +The number of radial values in the interval from C to C to use. + +=item C<< tsamples => 20 >> + +The number of angular values in the interval from C to C to use. + +=item C<< axesframed => 1 >> + +If set to 1 then the frames axes are displayed. If set to 0, the the framed +axes are not shown. This is 1 by default. + +=item C<< xaxislabel => 'x' >> + +Label for the axis corresponding to the first independent variable. + +=item C<< yaxislabel => 'y' >> + +Label for the axis corresponding to the second independent variable. + +=item C<< zaxislabel => 'z' >> + +Label for the axis corresponding to the dependent variable. -########################################### - # - # Set default options - # +=item C<< outputtype => 1 >> +This determines what is contained in the string that the method returns. The +values of 1 through 4 are accepted, and have the following meaning. + +=over + +=item 1. + +Return a string of only polygons (or edge mesh). + +=item 2. + +Return a string of only plot options. + +=item 3. + +Return a string of polygons (or edge mesh) and plot options. + +=item 4. + +Return the complete plot to be passed directly to the C method. + +=back + +=back + +=cut + +sub _LiveGraphicsRectangularPlot3D_init { }; # don't reload this file + +loadMacros('MathObjects.pl', 'LiveGraphics3D.pl'); + +$main::beginplot = 'Graphics3D['; +$main::endplot = ']'; + +sub RectangularPlot3DRectangularDomain { + # Set default options my %options = ( - function => Formula("1"), + function => Formula('1'), xvar => 'x', yvar => 'y', xmin => -3, @@ -145,144 +249,76 @@ sub RectangularPlot3DRectangularDomain { xsamples => 20, ysamples => 20, axesframed => 1, - xaxislabel => "X", - yaxislabel => "Y", - zaxislabel => "Z", + xaxislabel => 'x', + yaxislabel => 'y', + zaxislabel => 'z', outputtype => 4, @_ ); -############################################ - # - # Reset to Context("Numeric") just to be - # sure that everything will work properly. - # - - #Context("Numeric"); - #Context()->variables->are($options{xvar}=>"Real",$options{yvar}=>"Real"); - - my $fsubroutine; - $options{function}->perlFunction('fsubroutine', [ "$options{xvar}", "$options{yvar}" ]); - -###################################################### - # - # Generate a plotdata array, which has two indices - # + $options{function}->perlFunction('fsubroutine', [ $options{xvar}, $options{yvar} ]); - my $xsamples1 = $options{xsamples} - 1; - my $ysamples1 = $options{ysamples} - 1; + # Generate a plotdata array, which has two indices. my $dx = ($options{xmax} - $options{xmin}) / $options{xsamples}; my $dy = ($options{ymax} - $options{ymin}) / $options{ysamples}; - my $x; - my $y; + my (@x, @y, @z); - my $z; - - foreach my $i (0 .. $options{xsamples}) { + for my $i (0 .. $options{xsamples}) { $x[$i] = $options{xmin} + $i * $dx; - foreach my $j (0 .. $options{ysamples}) { - $y[$j] = $options{ymin} + $j * $dy; - # Use sprintf to round to three decimal places - $z[$i][$j] = sprintf("%.3f", fsubroutine($x[$i], $y[$j])->value); - $y[$j] = sprintf("%.3f", $y[$j]); + for my $j (0 .. $options{ysamples}) { + $y[$j] = $options{ymin} + $j * $dy; + $z[$i][$j] = sprintf('%.3f', fsubroutine($x[$i], $y[$j])->value); + $y[$j] = sprintf('%.3f', $y[$j]); } - $x[$i] = sprintf("%.3f", $x[$i]); + $x[$i] = sprintf('%.3f', $x[$i]); } -########################################################################### - # - # Generate a plotstring from the plotdata. - # - # The plotstring is a list of polygons - # LiveGraphics3D reads as input. - # - # For more information on the format of the plotstring, see - # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html - # http://www.vis.uni-stuttgart.de/~kraus/LiveGraphics3D/documentation.html - # -########################################### - # - # Generate the polygons in the plotstring - # - - my $plotstructure = "{"; - - foreach my $i (0 .. $xsamples1) { - foreach my $j (0 .. $ysamples1) { - - $plotstructure = - $plotstructure - . "Polygon[{" - . "{$x[$i],$y[$j],$z[$i][$j]}," - . "{$x[$i+1],$y[$j],$z[$i+1][$j]}," - . "{$x[$i+1],$y[$j+1],$z[$i+1][$j+1]}," - . "{$x[$i],$y[$j+1],$z[$i][$j+1]}" . "}]"; - - if (($i < $xsamples1) || ($j < $ysamples1)) { - $plotstructure = $plotstructure . ","; - } - + # Generate a plotstring from the plotdata. This is a list of polygons LiveGraphics3D reads as input. + # For more information on the format of the plotstring, see + # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html. + + # Generate the polygons in the plotstring. + my @polygons; + for my $i (0 .. $options{xsamples} - 1) { + for my $j (0 .. $options{ysamples} - 1) { + push(@polygons, + 'Polygon[{' + . "{$x[$i],$y[$j],$z[$i][$j]}," + . "{$x[$i+1],$y[$j],$z[$i+1][$j]}," + . "{$x[$i+1],$y[$j+1],$z[$i+1][$j+1]}," + . "{$x[$i],$y[$j+1],$z[$i][$j+1]}" + . '}]'); } } + my $plotstructure = '{' . join(',', @polygons) . '}'; - $plotstructure = $plotstructure . "}"; - -############################################## - # - # Add plot options to the plotoptions string - # - - my $plotoptions = ""; - - if (($options{outputtype} > 1) || ($options{axesframed} == 1)) { - $plotoptions = - $plotoptions - . "Axes->True,AxesLabel->" - . "{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}"; - } - -#################################################### - # - # Return only the plotstring (if outputtype=>1), - # or only plotoptions (if outputtype=>2), - # or plotstring, plotoptions (if outputtype=>2), - # or the entire plot (default) (if outputtype=>4) + my $plotoptions = + $options{outputtype} > 1 && $options{axesframed} == 1 + ? "Axes->True,AxesLabel->{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}" + : ''; if ($options{outputtype} == 1) { return $plotstructure; } elsif ($options{outputtype} == 2) { return $plotoptions; } elsif ($options{outputtype} == 3) { - return "{" . $plotstructure . "," . $plotoptions . "}"; + return "{$plotstructure,$plotoptions}"; } elsif ($options{outputtype} == 4) { - return $beginplot . $plotstructure . "," . $plotoptions . $endplot; + return "${main::beginplot}${plotstructure},${plotoptions}${main::endplot}"; } else { - return "Invalid outputtype (outputtype should be a number 1 through 4)."; + return 'Invalid outputtype (outputtype should be a number 1 through 4).'; } -} # End RectangularPlot3DRectangularDomain -############################################## -############################################## - -############################################# -############################################# -# Begin RectangularPlot3DAnnularDomain +} sub RectangularPlot3DAnnularDomain { - -############################################# - # - # Set default options - # - + # Set default options. my %options = ( - function => Formula("1"), - xvar => "x", - yvar => "y", - rvar => "r", - tvar => "t", + function => Formula('1'), + xvar => 'x', + yvar => 'y', rmin => 0.001, rmax => 3, tmin => 0, @@ -290,134 +326,71 @@ sub RectangularPlot3DAnnularDomain { rsamples => 20, tsamples => 20, axesframed => 1, - xaxislabel => "X", - yaxislabel => "Y", - zaxislabel => "Z", + xaxislabel => 'x', + yaxislabel => 'y', + zaxislabel => 'z', outputtype => 4, @_ ); -############################################ - # - # Reset to Context("Numeric") just to be - # sure that everything will work properly. - # + $options{function}->perlFunction('fsubroutine', [ $options{xvar}, $options{yvar} ]); - #Context("Numeric"); - #Context()->variables->are( - #$options{xvar}=>"Real", - #$options{yvar}=>"Real", - #$options{rvar}=>"Real", - #$options{tvar}=>"Real" - #); + # Generate a plotdata array which has two indices. - my $fsubroutine; - $options{function}->perlFunction('fsubroutine', [ "$options{xvar}", "$options{yvar}" ]); - -###################################################### - # - # Generate a plotdata array, which has two indices - # - - my $rsamples1 = $options{rsamples} - 1; - my $tsamples1 = $options{tsamples} - 1; + my ($rsamples1, $tsamples1) = ($options{rsamples} - 1, $options{tsamples} - 1); my $dr = ($options{rmax} - $options{rmin}) / $options{rsamples}; my $dt = ($options{tmax} - $options{tmin}) / $options{tsamples}; - my $t; - my $r; - - my $x; - my $y; - - my $z; - - foreach my $i (0 .. $options{tsamples}) { - $t[$i] = $options{tmin} + $i * $dt; - foreach my $j (0 .. $options{rsamples}) { - $r[$j] = $options{rmin} + $j * $dr; - $x[$i][$j] = $r[$j] * cos($t[$i]); - $y[$i][$j] = $r[$j] * sin($t[$i]); - $z[$i][$j] = sprintf("%.3f", fsubroutine($x[$i][$j], $y[$i][$j])->value); - $x[$i][$j] = sprintf("%.3f", $x[$i][$j]); - $y[$i][$j] = sprintf("%.3f", $y[$i][$j]); + my (@x, @y, @z); + + for my $i (0 .. $options{tsamples}) { + my $t = $options{tmin} + $i * $dt; + for my $j (0 .. $options{rsamples}) { + my $r = $options{rmin} + $j * $dr; + $x[$i][$j] = $r * cos($t); + $y[$i][$j] = $r * sin($t); + $z[$i][$j] = sprintf('%.3f', fsubroutine($x[$i][$j], $y[$i][$j])->value); + $x[$i][$j] = sprintf('%.3f', $x[$i][$j]); + $y[$i][$j] = sprintf('%.3f', $y[$i][$j]); } } -########################################################################### - # - # Generate a plotstring from the plotdata. - # - # The plotstring is a list of polygons that - # LiveGraphics3D reads as input. - # - # For more information on the format of the plotstring, see - # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html - # http://www.vis.uni-stuttgart.de/~kraus/LiveGraphics3D/documentation.html - # -########################################### - # - # Generate the polygons in the plotstring - # - - my $plotstructure = "{"; - - foreach my $i (0 .. $tsamples1) { - foreach my $j (0 .. $rsamples1) { - - $plotstructure = - $plotstructure - . "Polygon[{" - . "{$x[$i][$j],$y[$i][$j],$z[$i][$j]}," - . "{$x[$i+1][$j],$y[$i+1][$j],$z[$i+1][$j]}," - . "{$x[$i+1][$j+1],$y[$i+1][$j+1],$z[$i+1][$j+1]}," - . "{$x[$i][$j+1],$y[$i][$j+1],$z[$i][$j+1]}" . "}]"; - - if (($i < $tsamples1) || ($j < $rsamples1)) { - $plotstructure = $plotstructure . ","; - } - + # Generate a plotstring from the plotdata. This is a list of polygons that LiveGraphics3D reads as input. + # For more information on the format of the plotstring, see + # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html. + + # Generate the polygons in the plotstring. + my @polygons; + for my $i (0 .. $tsamples1) { + for my $j (0 .. $rsamples1) { + push(@polygons, + 'Polygon[{' + . "{$x[$i][$j],$y[$i][$j],$z[$i][$j]}," + . "{$x[$i+1][$j],$y[$i+1][$j],$z[$i+1][$j]}," + . "{$x[$i+1][$j+1],$y[$i+1][$j+1],$z[$i+1][$j+1]}," + . "{$x[$i][$j+1],$y[$i][$j+1],$z[$i][$j+1]}" + . '}]'); } } + my $plotstructure = '{' . join(',', @polygons) . '}'; - $plotstructure = $plotstructure . "}"; - -############################################## - # - # Add plot options to the plotoptions string - # - - my $plotoptions = ""; - - if (($options{outputtype} > 1) || ($options{axesframed} == 1)) { - $plotoptions = - $plotoptions - . "Axes->True,AxesLabel->" - . "{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}"; - } - -#################################################### - # - # Return only the plotstring (if outputtype=>1), - # or only plotoptions (if outputtype=>2), - # or plotstring, plotoptions (if outputtype=>2), - # or the entire plot (default) (if outputtype=>4) + my $plotoptions = + $options{outputtype} > 1 || $options{axesframed} == 1 + ? "Axes->True,AxesLabel->{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}" + : ''; if ($options{outputtype} == 1) { return $plotstructure; } elsif ($options{outputtype} == 2) { return $plotoptions; } elsif ($options{outputtype} == 3) { - return "{" . $plotstructure . "," . $plotoptions . "}"; + return "{$plotstructure,$plotoptions}"; } elsif ($options{outputtype} == 4) { - return $beginplot . $plotstructure . "," . $plotoptions . $endplot; + return "${main::beginplot}${plotstructure},${plotoptions}${main::endplot}"; } else { - return "Invalid outputtype (outputtype should be a number 1 through 4)."; + return 'Invalid outputtype (outputtype should be a number 1 through 4).'; } - -} # End RectangularPlot3DAnnularDomain -##################################################### -##################################################### +} 1; diff --git a/macros/graph/LiveGraphicsVectorField2D.pl b/macros/graph/LiveGraphicsVectorField2D.pl index 84738956c8..41c91313d3 100644 --- a/macros/graph/LiveGraphicsVectorField2D.pl +++ b/macros/graph/LiveGraphicsVectorField2D.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -19,70 +19,132 @@ =head1 NAME =head1 DESCRIPTION -C provides a macros for creating an -interactive plot of a vector field via the C Javascript applet. -The routine C takes two C Formulas of -2 variables as input and returns a string of plot data that can be -displayed using the C routine of the C macro. +This macro provides a method for creating an interactive plot of a vector field +via the C JavaScript applet. The method takes two C +Formulas of two variables as input and returns a string of plot data that can be +displayed using the C routine of the L macro. -=head1 USAGE +=head1 METHODS - VectorField2D(options); +=head2 VectorField2D -Options are: +Usage: C - Fx => Formula("y"), F = < Fx, Fy, Fz > where Fx, Fy, Fz are each - Fy => Formula("-x"), functions of 3 variables +The available options are as follows. - xvar => "r", independent variable name, default "x" - yvar => "s", independent variable name, default "y" +=over - xmin => -3, domain for xvar - xmax => 3, +=item C<< Fx => Formula('y') >> - ymin => -3, domain for yvar - ymax => 3, +Function for the C-coordinate. - xsamples => 3, deltax = (xmax - xmin) / xsamples - ysamples => 3, deltay = (ymax - ymin) / ysamples +=item C<< Fy => Formula('-x') >> - axesframed => 1, 1 displays framed axes, 0 hides framed axes +Function for the C-coordinate. - xaxislabel => "R", Capital letters may be easier to read - yaxislabel => "S", +=item C<< xvar => 'x' >> - vectorcolor => "RGBColor[1.0,0.0,0.0]", - vectorscale => 0.2, - vectorthickness => 0.001, +First independent variable name, default 'x'. This must correspond to the first +variable used in the C and C. - outputtype => 1, return string of only polygons (or mesh) - 2, return string of only plotoptions - 3, return string of polygons (or mesh) and plotoptions - 4, return complete plot +=item C<< yvar => 'y' >> -=cut +Second independent variable name, default 'y'. This must correspond to the +second variable used in the C and C. -sub _LiveGraphicsVectorField2D_init { }; # don't reload this file +=item C<< xmin => -3 >> -loadMacros("MathObjects.pl", "LiveGraphics3D.pl"); +Lower bound for the domain of the first independent variable. -$beginplot = "Graphics3D["; -$endplot = "]"; +=item C<< xmax => 3 >> -########################################### -########################################### -# Begin VectorField2D +Upper bound for the domain of the first independent variable. -sub VectorField2D { +=item C<< ymin => -3 >> + +Lower bound for the domain of the second independent variable. + +=item C<< ymax => 3 >> + +Upper bound for the domain of the second independent variable. + +=item C<< xsamples => 20 >> + +The number of sample values for the first independent variable in the interval +from C to C to use. + +=item C<< ysamples => 20 >> + +The number of sample values for the second independent variable in the interval +from C to C to use. + +=item C<< axesframed => 1 >> + +If set to 1 then the framed axes are displayed. If set to 0, the the framed +axes are not shown. This is 1 by default. + +=item C<< xaxislabel => 'x' >> + +Label for the axis corresponding to the first independent variable. + +=item C<< yaxislabel => 'y' >> + +Label for the axis corresponding to the second independent variable. + +=item C<< vectorcolor => 'RGBColor[0.0, 0.0, 1.0]' >> + +Color of vectors shown in the slope field. + +=item C<< vectorscale => 0.2 >> + +Multiplier that determines the lentgh of vectors shown in the slope field. + +=item C<< vectorthickness => 0.001 >> + +Thickness (or width) of the line segments used to construct the vectors shown in +the slope field. + +=item C<< outputtype => 1 >> + +This determines what is contained in the string that the method returns. The +values of 1 through 4 are accepted, and have the following meaning. + +=over -########################################### - # - # Set default options - # +=item 1. +Return a string of only polygons (or edge mesh). + +=item 2. + +Return a string of only plot options. + +=item 3. + +Return a string of polygons (or edge mesh) and plot options. + +=item 4. + +Return the complete plot to be passed directly to the C method. + +=back + +=back + +=cut + +sub _LiveGraphicsVectorField2D_init { } + +loadMacros('MathObjects.pl', 'LiveGraphics3D.pl'); + +$main::beginplot = 'Graphics3D['; +$main::endplot = ']'; + +sub VectorField2D { + # Set default options my %options = ( - Fx => Formula("1"), - Fy => Formula("1"), + Fx => Formula('1'), + Fy => Formula('1'), xvar => 'x', yvar => 'y', xmin => -3, @@ -92,136 +154,92 @@ sub VectorField2D { xsamples => 20, ysamples => 20, axesframed => 1, - xaxislabel => "X", - yaxislabel => "Y", - vectorcolor => "RGBColor[1.0,0.0,0.0]", + xaxislabel => 'x', + yaxislabel => 'y', + vectorcolor => 'RGBColor[0.0,0.0,1.0]', vectorscale => 0.2, vectorthickness => 0.001, outputtype => 4, @_ ); - my $Fxsubroutine; - my $Fysubroutine; + $options{Fx}->perlFunction('Fxsubroutine', [ $options{xvar}, $options{yvar} ]); + $options{Fy}->perlFunction('Fysubroutine', [ $options{xvar}, $options{yvar} ]); - $options{Fx}->perlFunction('Fxsubroutine', [ "$options{xvar}", "$options{yvar}" ]); - $options{Fy}->perlFunction('Fysubroutine', [ "$options{xvar}", "$options{yvar}" ]); - -###################################################### - # - # Generate plot data - # + # Generate plot data my $dx = ($options{xmax} - $options{xmin}) / $options{xsamples}; my $dy = ($options{ymax} - $options{ymin}) / $options{ysamples}; - my $xtail; - my $ytail; + my (@xtail, @ytail, @xtip, @ytip, @xleftbarb, @xrightbarb, @yleftbarb, @yrightbarb); - foreach my $i (0 .. $options{xsamples}) { + for my $i (0 .. $options{xsamples}) { $xtail[$i] = $options{xmin} + $i * $dx; - foreach my $j (0 .. $options{ysamples}) { + for my $j (0 .. $options{ysamples}) { $ytail[$j] = $options{ymin} + $j * $dy; - $FX[$i][$j] = sprintf("%.3f", $options{vectorscale} * (Fxsubroutine($xtail[$i], $ytail[$j])->value)); - $FY[$i][$j] = sprintf("%.3f", $options{vectorscale} * (Fysubroutine($xtail[$i], $ytail[$j])->value)); + my $Fx = sprintf('%.3f', $options{vectorscale} * Fxsubroutine($xtail[$i], $ytail[$j])->value); + my $Fy = sprintf('%.3f', $options{vectorscale} * Fysubroutine($xtail[$i], $ytail[$j])->value); - $xtail[$i] = sprintf("%.3f", $xtail[$i]); - $ytail[$j] = sprintf("%.3f", $ytail[$j]); + $xtail[$i] = sprintf('%.3f', $xtail[$i]); + $ytail[$j] = sprintf('%.3f', $ytail[$j]); - $xtip[$i][$j] = $xtail[$i] + sprintf("%.3f", $FX[$i][$j]); - $ytip[$i][$j] = $ytail[$j] + sprintf("%.3f", $FY[$i][$j]); + $xtip[$i][$j] = $xtail[$i] + sprintf('%.3f', $Fx); + $ytip[$i][$j] = $ytail[$j] + sprintf('%.3f', $Fy); - $xleftbarb[$i][$j] = sprintf("%.3f", $xtail[$i] + 0.8 * $FX[$i][$j] - 0.2 * $FY[$i][$j]); - $yleftbarb[$i][$j] = sprintf("%.3f", $ytail[$j] + 0.8 * $FY[$i][$j] + 0.2 * $FX[$i][$j]); + $xleftbarb[$i][$j] = sprintf('%.3f', $xtail[$i] + 0.8 * $Fx - 0.2 * $Fy); + $yleftbarb[$i][$j] = sprintf('%.3f', $ytail[$j] + 0.8 * $Fy + 0.2 * $Fx); - $xrightbarb[$i][$j] = sprintf("%.3f", $xtail[$i] + 0.8 * $FX[$i][$j] + 0.2 * $FY[$i][$j]); - $yrightbarb[$i][$j] = sprintf("%.3f", $ytail[$j] + 0.8 * $FY[$i][$j] - 0.2 * $FX[$i][$j]); + $xrightbarb[$i][$j] = sprintf('%.3f', $xtail[$i] + 0.8 * $Fx + 0.2 * $Fy); + $yrightbarb[$i][$j] = sprintf('%.3f', $ytail[$j] + 0.8 * $Fy - 0.2 * $Fx); } } -########################################################################### - # - # Generate plotstructure from the plotdata. - # - # The plotstucture is a list of arrows (made of lines) that - # LiveGraphics3D reads as input. - # - # For more information on the format of the plotstructure, see - # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html - # http://www.vis.uni-stuttgart.de/~kraus/LiveGraphics3D/documentation.html - # -########################################### - # - # Generate the polygons in the plotstructure - # - - my $plotstructure = "{{{{$options{vectorcolor},EdgeForm[],Thickness[$options{vectorthickness}],"; - - foreach my $i (0 .. $options{xsamples}) { - foreach my $j (0 .. $options{ysamples}) { - - $plotstructure = - $plotstructure - . "Line[{" - . "{$xtail[$i],$ytail[$j],0}," - . "{$xtip[$i][$j],$ytip[$i][$j],0}" . "}]," - . "Line[{" - . "{$xleftbarb[$i][$j],$yleftbarb[$i][$j],0}," - . "{$xtip[$i][$j],$ytip[$i][$j],0}," - . "{$xrightbarb[$i][$j],$yrightbarb[$i][$j],0}" . "}]"; - - if (($i < $options{xsamples}) || ($j < $options{ysamples})) { - $plotstructure = $plotstructure . ","; - } - + # Generate plotstructure from the plotdata. This is a list of arrows (made of lines) that LiveGraphics3D reads as + # input. For more information on the format of the plotstructure, see + # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html. + + # Generate the lines in the plotstructure. + my @lines; + + for my $i (0 .. $options{xsamples}) { + for my $j (0 .. $options{ysamples}) { + push(@lines, + 'Line[{' + . "{$xtail[$i],$ytail[$j],0}," + . "{$xtip[$i][$j],$ytip[$i][$j],0}" + . '}],Line[{' + . "{$xleftbarb[$i][$j],$yleftbarb[$i][$j],0}," + . "{$xtip[$i][$j],$ytip[$i][$j],0}," + . "{$xrightbarb[$i][$j],$yrightbarb[$i][$j],0}" + . '}]'); } } - $plotstructure = $plotstructure . "}}}}"; - -############################################## - # - # Add plot options to the plotoptions string - # - - my $plotoptions = ""; - - if (($options{outputtype} > 1) || ($options{axesframed} == 1)) { + my $plotstructure = "{{{{$options{vectorcolor},Thickness[$options{vectorthickness}]," . join(',', @lines) . '}}}}'; + my $plotoptions = ''; + if ($options{outputtype} > 1) { $plotoptions = $plotoptions . "PlotRange->{{$options{xmin},$options{xmax}},{$options{ymin},$options{ymax}},{-0.1,0.1}}," - . "ViewPoint->{0,0,1000}," - . "ViewVertical->{0,1,0}," - . "Lighting->False," - . "AxesLabel->{$options{xaxislabel},$options{yaxislabel},Z}," - . #; # . - "Axes->{True,True,False}"; - + . 'ViewPoint->{0,0,2},ViewVertical->{0,1,0},Lighting->False,' + . ($options{axesframed} == 1 + ? "AxesLabel->{$options{xaxislabel},$options{yaxislabel},Z},Axes->{True,True,False}" + : ''); } -#################################################### - # - # Return only the plotstring (if outputtype=>1), - # or only plotoptions (if outputtype=>2), - # or plotstring, plotoptions (if outputtype=>2), - # or the entire plot (default) (if outputtype=>4) - if ($options{outputtype} == 1) { return $plotstructure; } elsif ($options{outputtype} == 2) { return $plotoptions; } elsif ($options{outputtype} == 3) { - return "{" . $plotstructure . "," . $plotoptions . "}"; + return "{$plotstructure,$plotoptions}"; } elsif ($options{outputtype} == 4) { - return $beginplot . $plotstructure . "," . $plotoptions . $endplot; + return "${main::beginplot}${plotstructure},${plotoptions}${main::endplot}"; } else { - return "Invalid outputtype (outputtype should be a number 1 through 4)."; + return 'Invalid outputtype (outputtype should be a number 1 through 4).'; } - -} # End VectorField2D -############################################## -############################################## +} 1; diff --git a/macros/graph/LiveGraphicsVectorField3D.pl b/macros/graph/LiveGraphicsVectorField3D.pl index 058264a492..22b35a0402 100644 --- a/macros/graph/LiveGraphicsVectorField3D.pl +++ b/macros/graph/LiveGraphicsVectorField3D.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -19,81 +19,160 @@ =head1 NAME =head1 DESCRIPTION -C provides a macros for creating an -interactive plot of a vector field via the C Javascript applet. -The routine C takes three C Formulas of -3 variables as input and returns a string of plot data that can be -displayed using the C routine of the C macro. +This macro provides a method for creating an interactive plot of a vector field +via the C JavaScript applet. The method takes three +C Formulas of three variables as input and returns a string of plot +data that can be displayed using the C routine of the +L macro. -=head1 USAGE +=head1 METHODS - VectorField3D(options); +=head2 VectorField3D -Options are: +Usage: C - Fx => Formula("y"), F = < Fx, Fy, Fz > where Fx, Fy, Fz are each - Fy => Formula("-x"), functions of 3 variables - Fz => Formula("z"), +The available options are as follows. - xvar => "r", independent variable name, default "x" - yvar => "s", independent variable name, default "y" - zvar => "t", independent variable name, default "z" +=over - xmin => -3, domain for xvar - xmax => 3, +=item C<< Fx => Formula('y') >> - ymin => -3, domain for yvar - ymax => 3, +Function for the C-coordinate. - zmin => -3, domain for zvar - zmax => 3, +=item C<< Fy => Formula('-x') >> - xsamples => 3, deltax = (xmax - xmin) / xsamples - ysamples => 3, deltay = (ymax - ymin) / ysamples - zsamples => 3, deltaz = (zmax - zmin) / zsamples +Function for the C-coordinate. - axesframed => 1, 1 displays framed axes, 0 hides framed axes +=item C<< Fz => Formula('x + y + z') >> - xaxislabel => "R", Capital letters may be easier to read - yaxislabel => "S", - zaxislabel => "T", +Function for the C-coordinate. - vectorcolor => "RGBColor[0.0,0.0,1.0]", - vectorscale => 0.2, - vectorthickness => 0.001, +=item C<< xvar => 'x' >> - outputtype => 1, return string of only polygons (or mesh) - 2, return string of only plotoptions - 3, return string of polygons (or mesh) and plotoptions - 4, return complete plot +First independent variable name, default 'x'. This must correspond to the first +variable used in the C, C, and C. +=item C<< yvar => 'y' >> +Second independent variable name, default 'y'. This must correspond to the +second variable used in the C, C, and C. +=item C<< zvar => 'z' >> -=cut +Third independent variable name, default 'z'. This must correspond to the +third variable used in the C, C, and C. -sub _LiveGraphicsVectorField3D_init { }; # don't reload this file +=item C<< xmin => -3 >> -loadMacros("MathObjects.pl", "LiveGraphics3D.pl"); +Lower bound for the domain of the first independent variable. -$beginplot = "Graphics3D["; -$endplot = "]"; +=item C<< xmax => 3 >> -########################################### -########################################### -# Begin VectorField3D +Upper bound for the domain of the first independent variable. -sub VectorField3D { +=item C<< ymin => -3 >> + +Lower bound for the domain of the second independent variable. + +=item C<< ymax => 3 >> + +Upper bound for the domain of the second independent variable. + +=item C<< zmin => -3 >> + +Lower bound for the domain of the third independent variable. + +=item C<< zmax => 3 >> + +Upper bound for the domain of the third independent variable. + +=item C<< xsamples => 20 >> + +The number of sample values for the first independent variable in the interval +from C to C to use. + +=item C<< ysamples => 20 >> + +The number of sample values for the second independent variable in the interval +from C to C to use. + +=item C<< zsamples => 20 >> + +The number of sample values for the third independent variable in the interval +from C to C to use. + +=item C<< axesframed => 1 >> + +If set to 1 then the framed axes are displayed. If set to 0, the the framed +axes are not shown. This is 1 by default. + +=item C<< xaxislabel => 'x' >> + +Label for the axis corresponding to the first independent variable. + +=item C<< yaxislabel => 'y' >> + +Label for the axis corresponding to the second independent variable. + +=item C<< zaxislabel => 'z' >> + +Label for the axis corresponding to the third independent variable. + +=item C<< vectorcolor => 'RGBColor[0.0, 0.0, 1.0]' >> + +Color of vectors shown in the slope field. + +=item C<< vectorscale => 0.2 >> + +Multiplier that determines the lentgh of vectors shown in the slope field. + +=item C<< vectorthickness => 0.001 >> + +Thickness (or width) of the line segments used to construct the vectors shown in +the slope field. + +=item C<< outputtype => 1 >> -########################################### - # - # Set default options - # +This determines what is contained in the string that the method returns. The +values of 1 through 4 are accepted, and have the following meaning. +=over + +=item 1. + +Return a string of only polygons (or edge mesh). + +=item 2. + +Return a string of only plot options. + +=item 3. + +Return a string of polygons (or edge mesh) and plot options. + +=item 4. + +Return the complete plot to be passed directly to the C method. + +=back + +=back + +=cut + +sub _LiveGraphicsVectorField3D_init { } + +loadMacros('MathObjects.pl', 'LiveGraphics3D.pl'); + +$main::beginplot = 'Graphics3D['; +$main::endplot = ']'; + +sub VectorField3D { + # Set default options. my %options = ( - Fx => Formula("1"), - Fy => Formula("1"), - Fz => Formula("1"), + Fx => Formula('1'), + Fy => Formula('1'), + Fz => Formula('1'), xvar => 'x', yvar => 'y', zvar => 'z', @@ -107,10 +186,10 @@ sub VectorField3D { ysamples => 20, zsamples => 20, axesframed => 1, - xaxislabel => "X", - yaxislabel => "Y", - zaxislabel => "Z", - vectorcolor => "RGBColor[0.0,0.0,1.0]", + xaxislabel => 'x', + yaxislabel => 'y', + zaxislabel => 'z', + vectorcolor => 'RGBColor[0.0,0.0,1.0]', vectorscale => 0.2, vectorthickness => 0.001, xavoid => 1000000, @@ -120,148 +199,96 @@ sub VectorField3D { @_ ); - my $Fxsubroutine; - my $Fysubroutine; - my $Fzsubroutine; + $options{Fx}->perlFunction('Fxsubroutine', [ $options{xvar}, $options{yvar}, $options{zvar} ]); + $options{Fy}->perlFunction('Fysubroutine', [ $options{xvar}, $options{yvar}, $options{zvar} ]); + $options{Fz}->perlFunction('Fzsubroutine', [ $options{xvar}, $options{yvar}, $options{zvar} ]); - $options{Fx}->perlFunction('Fxsubroutine', [ "$options{xvar}", "$options{yvar}", "$options{zvar}" ]); - $options{Fy}->perlFunction('Fysubroutine', [ "$options{xvar}", "$options{yvar}", "$options{zvar}" ]); - $options{Fz}->perlFunction('Fzsubroutine', [ "$options{xvar}", "$options{yvar}", "$options{zvar}" ]); - -###################################################### - # - # Generate plot data - # + # Generate plot data. my $dx = ($options{xmax} - $options{xmin}) / $options{xsamples}; my $dy = ($options{ymax} - $options{ymin}) / $options{ysamples}; my $dz = ($options{zmax} - $options{zmin}) / $options{zsamples}; - my $xtail; - my $ytail; - my $ztail; + my (@xtail, @ytail, @ztail, @xtip, @ytip, @ztip, @xleftbarb, @xrightbarb, @yleftbarb, @yrightbarb, @zbarb); - foreach my $i (0 .. $options{xsamples}) { + for my $i (0 .. $options{xsamples}) { $xtail[$i] = $options{xmin} + $i * $dx; - foreach my $j (0 .. $options{ysamples}) { + for my $j (0 .. $options{ysamples}) { $ytail[$j] = $options{ymin} + $j * $dy; - foreach my $k (0 .. $options{zsamples}) { + for my $k (0 .. $options{zsamples}) { $ztail[$k] = $options{zmin} + $k * $dz; - if ($xtail[$i] == $options{xavoid} && $ytail[$j] == $options{yavoid} && $ztail[$k] == $options{zavoid}) - { - - $FX[$i][$j][$k] = 0; - $FY[$i][$j][$k] = 0; - $FZ[$i][$j][$k] = 0; - - } else { - - $FX[$i][$j][$k] = sprintf("%.3f", - $options{vectorscale} * (Fxsubroutine($xtail[$i], $ytail[$j], $ztail[$k])->value)); - $FY[$i][$j][$k] = sprintf("%.3f", - $options{vectorscale} * (Fysubroutine($xtail[$i], $ytail[$j], $ztail[$k])->value)); - $FZ[$i][$j][$k] = sprintf("%.3f", - $options{vectorscale} * (Fzsubroutine($xtail[$i], $ytail[$j], $ztail[$k])->value)); + my ($Fx, $Fy, $Fz) = (0, 0, 0); + if ($xtail[$i] != $options{xavoid} || $ytail[$j] != $options{yavoid} || $ztail[$k] != $options{zavoid}) + { + $Fx = sprintf('%.3f', + $options{vectorscale} * Fxsubroutine($xtail[$i], $ytail[$j], $ztail[$k])->value); + $Fy = sprintf('%.3f', + $options{vectorscale} * Fysubroutine($xtail[$i], $ytail[$j], $ztail[$k])->value); + $Fz = sprintf('%.3f', + $options{vectorscale} * Fzsubroutine($xtail[$i], $ytail[$j], $ztail[$k])->value); } - $xtail[$i] = sprintf("%.3f", $xtail[$i]); - $ytail[$j] = sprintf("%.3f", $ytail[$j]); - $ztail[$k] = sprintf("%.3f", $ztail[$k]); + $xtail[$i] = sprintf('%.3f', $xtail[$i]); + $ytail[$j] = sprintf('%.3f', $ytail[$j]); + $ztail[$k] = sprintf('%.3f', $ztail[$k]); - $xtip[$i][$j][$k] = $xtail[$i] + sprintf("%.3f", $FX[$i][$j][$k]); - $ytip[$i][$j][$k] = $ytail[$j] + sprintf("%.3f", $FY[$i][$j][$k]); - $ztip[$i][$j][$k] = $ztail[$k] + sprintf("%.3f", $FZ[$i][$j][$k]); + $xtip[$i][$j][$k] = $xtail[$i] + sprintf('%.3f', $Fx); + $ytip[$i][$j][$k] = $ytail[$j] + sprintf('%.3f', $Fy); + $ztip[$i][$j][$k] = $ztail[$k] + sprintf('%.3f', $Fz); - $xleftbarb[$i][$j][$k] = sprintf("%.3f", $xtail[$i] + 0.8 * $FX[$i][$j][$k] - 0.2 * $FY[$i][$j][$k]); - $yleftbarb[$i][$j][$k] = sprintf("%.3f", $ytail[$j] + 0.8 * $FY[$i][$j][$k] + 0.2 * $FX[$i][$j][$k]); + $xleftbarb[$i][$j][$k] = sprintf('%.3f', $xtail[$i] + 0.8 * $Fx - 0.2 * $Fy); + $yleftbarb[$i][$j][$k] = sprintf('%.3f', $ytail[$j] + 0.8 * $Fy + 0.2 * $Fx); - $xrightbarb[$i][$j][$k] = sprintf("%.3f", $xtail[$i] + 0.8 * $FX[$i][$j][$k] + 0.2 * $FY[$i][$j][$k]); - $yrightbarb[$i][$j][$k] = sprintf("%.3f", $ytail[$j] + 0.8 * $FY[$i][$j][$k] - 0.2 * $FX[$i][$j][$k]); - - $zbarb[$i][$j][$k] = sprintf("%.3f", $ztail[$k] + 0.8 * $FZ[$i][$j][$k]); + $xrightbarb[$i][$j][$k] = sprintf('%.3f', $xtail[$i] + 0.8 * $Fx + 0.2 * $Fy); + $yrightbarb[$i][$j][$k] = sprintf('%.3f', $ytail[$j] + 0.8 * $Fy - 0.2 * $Fx); + $zbarb[$i][$j][$k] = sprintf("%.3f", $ztail[$k] + 0.8 * $Fz); } } } -########################################################################### - # - # Generate plotstructure from the plotdata. - # - # The plotstucture is a list of arrows (made of lines) that - # LiveGraphics3D reads as input. - # - # For more information on the format of the plotstructure, see - # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html - # http://www.vis.uni-stuttgart.de/~kraus/LiveGraphics3D/documentation.html - # -########################################### - # - # Generate the polygons in the plotstructure - # - - my $plotstructure = "{{{{$options{vectorcolor},Thickness[$options{vectorthickness}],"; - - foreach my $i (0 .. $options{xsamples}) { - foreach my $j (0 .. $options{ysamples}) { - foreach my $k (0 .. $options{zsamples}) { - - $plotstructure = - $plotstructure - . "Line[{" - . "{$xtail[$i],$ytail[$j],$ztail[$k]}," - . "{$xtip[$i][$j][$k],$ytip[$i][$j][$k],$ztip[$i][$j][$k]}" . "}]," - . "Line[{" - . "{$xleftbarb[$i][$j][$k],$yleftbarb[$i][$j][$k],$zbarb[$i][$j][$k]}," - . "{$xtip[$i][$j][$k],$ytip[$i][$j][$k],$ztip[$i][$j][$k]}," - . "{$xrightbarb[$i][$j][$k],$yrightbarb[$i][$j][$k],$zbarb[$i][$j][$k]}" . "}]"; - - if (($i < $options{xsamples}) || ($j < $options{ysamples}) || ($k < $options{zsamples})) { - $plotstructure = $plotstructure . ","; - } + # Generate plotstructure from the plotdata. This is a list of arrows (made of lines) that LiveGraphics3D reads as + # input. For more information on the format of the plotstructure, see + # http://www.math.umn.edu/~rogness/lg3d/page_NoMathematica.html. + + # Generate the lines in the plotstructure. + my @lines; + for my $i (0 .. $options{xsamples}) { + for my $j (0 .. $options{ysamples}) { + for my $k (0 .. $options{zsamples}) { + push(@lines, + 'Line[{' + . "{$xtail[$i],$ytail[$j],$ztail[$k]}," + . "{$xtip[$i][$j][$k],$ytip[$i][$j][$k],$ztip[$i][$j][$k]}" + . '}],Line[{' + . "{$xleftbarb[$i][$j][$k],$yleftbarb[$i][$j][$k],$zbarb[$i][$j][$k]}," + . "{$xtip[$i][$j][$k],$ytip[$i][$j][$k],$ztip[$i][$j][$k]}," + . "{$xrightbarb[$i][$j][$k],$yrightbarb[$i][$j][$k],$zbarb[$i][$j][$k]}" + . '}]'); } } } - $plotstructure = $plotstructure . "}}}}"; + my $plotstructure = "{{{{$options{vectorcolor},Thickness[$options{vectorthickness}]," . join(',', @lines) . '}}}}'; -############################################## - # - # Add plot options to the plotoptions string - # - - my $plotoptions = ""; - - if (($options{outputtype} > 1) || ($options{axesframed} == 1)) { - $plotoptions = - $plotoptions - . "Axes->True,AxesLabel->" - . "{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}"; - } - -#################################################### - # - # Return only the plotstring (if outputtype=>1), - # or only plotoptions (if outputtype=>2), - # or plotstring, plotoptions (if outputtype=>2), - # or the entire plot (default) (if outputtype=>4) + my $plotoptions = + $options{outputtype} > 1 && $options{axesframed} == 1 + ? "Axes->True,AxesLabel->{$options{xaxislabel},$options{yaxislabel},$options{zaxislabel}}" + : ''; if ($options{outputtype} == 1) { return $plotstructure; } elsif ($options{outputtype} == 2) { return $plotoptions; } elsif ($options{outputtype} == 3) { - return "{" . $plotstructure . "," . $plotoptions . "}"; + return "{$plotstructure,$plotoptions}"; } elsif ($options{outputtype} == 4) { - return $beginplot . $plotstructure . "," . $plotoptions . $endplot; + return "${main::beginplot}${plotstructure},${plotoptions}${main::endplot}"; } else { - return "Invalid outputtype (outputtype should be a number 1 through 4)."; + return 'Invalid outputtype (outputtype should be a number 1 through 4).'; } - -} # End VectorField3D -############################################## -############################################## +} 1; diff --git a/macros/graph/PGgraphmacros.pl b/macros/graph/PGgraphmacros.pl index 39d2103d25..972b3cfbdd 100644 --- a/macros/graph/PGgraphmacros.pl +++ b/macros/graph/PGgraphmacros.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -84,9 +84,7 @@ =head2 init_graph =cut -BEGIN { - be_strict(); -} +BEGIN { strict->import; } sub _PGgraphmacros_init { diff --git a/macros/graph/PGlateximage.pl b/macros/graph/PGlateximage.pl index 15092a35ac..488219dbe6 100644 --- a/macros/graph/PGlateximage.pl +++ b/macros/graph/PGlateximage.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/graph/PGstatisticsGraphMacros.pl b/macros/graph/PGstatisticsGraphMacros.pl index 63791eb5d7..b1e95e2c15 100644 --- a/macros/graph/PGstatisticsGraphMacros.pl +++ b/macros/graph/PGstatisticsGraphMacros.pl @@ -70,9 +70,7 @@ =head2 Other constructs our @accumulatedDataSets = (); # The list of data sets to be used in the graphs. -BEGIN { - be_strict(); -} +BEGIN { strict->import; } sub _PGstatisticGraphMacros_init { clear_stat_graph_data(); @@ -226,8 +224,7 @@ sub add_boxplot { # No go through and add the labels. while ($currentPlot > 0) { - my $label = new Label($xmin, $currentPlot - 0.5, $currentPlot, 'black', 'left'); - $label->font(GD::gdGiantFont); + my $label = new Label($xmin, $currentPlot - 0.5, $currentPlot, 'black', 'left', 'giant'); $graphRef->lb($label); $currentPlot--; } @@ -333,8 +330,7 @@ sub add_histogram { # Go through and add the labels on the left part of the graph # No go through and add the labels. while ($currentPlot > 0) { - my $label = new Label($xmin, $currentPlot - 0.5, $currentPlot, 'black', 'left'); - $label->font(GD::gdGiantFont); + my $label = new Label($xmin, $currentPlot - 0.5, $currentPlot, 'black', 'left', 'giant'); $graphRef->lb($label); $currentPlot--; } diff --git a/macros/graph/PGtikz.pl b/macros/graph/PGtikz.pl index af203d4583..2d4f42b421 100644 --- a/macros/graph/PGtikz.pl +++ b/macros/graph/PGtikz.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/graph/parserGraphTool.pl b/macros/graph/parserGraphTool.pl index aacf8d49a1..826674e257 100644 --- a/macros/graph/parserGraphTool.pl +++ b/macros/graph/parserGraphTool.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -401,6 +401,8 @@ =head1 METHODS =cut +BEGIN { strict->import } + sub _parserGraphTool_init { ADD_CSS_FILE('node_modules/jsxgraph/distrib/jsxgraph.css'); ADD_CSS_FILE('js/GraphTool/graphtool.css'); @@ -418,7 +420,7 @@ sub _parserGraphTool_init { sub GraphTool { parser::GraphTool->new(@_) } -$graphToolObjectCmps = \%parser::GraphTool::graphObjectCmps; +$main::graphToolObjectCmps = \%parser::GraphTool::graphObjectCmps; package parser::GraphTool; our @ISA = qw(Value::List); @@ -1067,7 +1069,7 @@ sub addTools { sub ANS_NAME { my $self = shift; - $self->{name} = main::NEW_ANS_NAME() unless defined($self->{name}); + main::RECORD_IMPLICIT_ANS_NAME($self->{name} = main::NEW_ANS_NAME()) unless defined $self->{name}; return $self->{name}; } @@ -1103,7 +1105,7 @@ sub constructJSXGraphOptions { x => { ticks => { ticksDistance => $self->{ticksDistanceX}, minorTicks => $self->{minorTicksX} } }, y => { ticks => { ticksDistance => $self->{ticksDistanceY}, minorTicks => $self->{minorTicksY} } } }, - grid => { gridX => $self->{gridX}, gridY => $self->{gridY} } + grid => { majorStep => [ $self->{gridX}, $self->{gridY} ] } ) }); @@ -1128,14 +1130,19 @@ sub ans_rule { return ''; } else { $self->constructJSXGraphOptions; - return main::tag('input', type => 'hidden', name => $ans_name, id => $ans_name, value => $answer_value) - . main::tag( - 'input', - type => 'hidden', - name => "previous_$ans_name", - id => "previous_$ans_name", - value => $answer_value - ) . < $ans_name, + class => 'graphtool-outer-container', + main::tag('input', type => 'hidden', name => $ans_name, id => $ans_name, value => $answer_value) + . main::tag( + 'input', + type => 'hidden', + name => "previous_$ans_name", + id => "previous_$ans_name", + value => $answer_value + ) + . <
                      - ) - )); - - # create the answer rule - # - main::NAMED_ANS_RULE($name, $size); - -} # end menu + $w = WordCompletion(['choice 1', 'choice 2', ...], correct); -sub choices_text { - my $self = shift; - my $list = $self->{choices}; - my $output = join ', ', map {qq/$_/} @{$list}; - return $output; -} +where C<'choice 1', 'choice 2', ...> are the allowed answers that will be shown +in the drop-down list and C is the correct answer from the list. -sub choices_list { - my $self = shift; - my $list = $self->{choices}; - my $output = ''; - - if ($main::displayMode eq "TeX") { - $output = join "\n", map {qq/\\item $_/} @{$list}; - return "\\begin{itemize}\n" . $output . "\\end{itemize}\n"; - } else { # HTML mode - $output = join " ", map {qq/
                    • $_<\/li>/} @{$list}; - return "
                        " . $output . "
                      "; - } - return $output; +To insert the WordCompletion answer rule into a problem use -} # end choices_list + BEGIN_PGML + [_]{$w}{40} + END_PGML -################################################## -# -# Answer rule is the menu list (for compatibility with parserMultiAnswer) -# Use alternates given below with older parserMultiAnswer.pl versions +or -sub ans_rule { shift->menu(0, '', @_) } # sub ans_rule {shift->menu(@_)} -sub named_ans_rule { shift->menu(0, @_) } # sub named_ans_rule {shift->menu(@_)} -sub named_ans_rule_extension { shift->menu(1, @_) } # sub named_ans_rule_extension {shift->menu(@_)} + BEGIN_TEXT + \{ $w->ans_rule(40) \} + END_TEXT -################################################## -# -# Replacement for Parser::String that takes the -# complete parse string as its value. (To make ->cmp work.) -# -package parser::WordCompletion::String; -our @ISA = ('Parser::String'); + ANS($wb->cmp); -sub new { - my $self = shift; - my ($equation, $value, $ref) = @_; - $value = $equation->{string}; - $self->SUPER::new($equation, $value, $ref); -} +You can explicitly list all of the choices using -################################################## + $w->choices_text -1; +for a comma separated list of the choices (inline, text style) and + + $w->choices_list + +for an unordered list (display style). + +=cut diff --git a/macros/ui/PGchoicemacros.pl b/macros/ui/PGchoicemacros.pl index b086a472b2..394e37d361 100644 --- a/macros/ui/PGchoicemacros.pl +++ b/macros/ui/PGchoicemacros.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 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 @@ -84,19 +84,12 @@ =head1 DESCRIPTION =cut -# ^uses be_strict -BEGIN { - be_strict; -} +BEGIN { strict->import; } loadMacros('PGauxiliaryFunctions.pl'); package main; -BEGIN { - be_strict(); -} - # ^function _PGchoicemacros_init sub _PGchoicemacros_init { diff --git a/macros/ui/PGinfo.pl b/macros/ui/PGinfo.pl index c42526cb26..9b0fc5a01f 100644 --- a/macros/ui/PGinfo.pl +++ b/macros/ui/PGinfo.pl @@ -1,7 +1,7 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/ui/niceTables.pl b/macros/ui/niceTables.pl index 163b52b7ba..bca5e26ce5 100644 --- a/macros/ui/niceTables.pl +++ b/macros/ui/niceTables.pl @@ -47,7 +47,7 @@ =head2 Description ); The cell entries above like C may be simple cell content, -a hash reference with C cellContent> and options, +a hash reference with C<< data => cellContent >> and options, or an array reference where the 0th entry is the the cell content and it is followed by option key-value pairs. @@ -66,19 +66,19 @@ =head3 All output formats =over -=item C
                      0 or 1> +=item C<< center => 0 or 1 >> center the table (default 1) -=item C string> +=item C<< caption => string >> caption for the table -=item C 0 or 1> +=item C<< horizontalrules => 0 or 1 >> make rules above and below every row (default 0) -=item C string> +=item C<< texalignment => string >> an alignment string like is used in a LaTeX tabular environment: for example C<'r|ccp{1in}'> @@ -88,8 +88,9 @@ =head3 All output formats C for right-aligned column -C for a column with left-aligned paragraphs of fixed width. -The width needs to be absolute to work in all output formats. +C for a column with left-aligned paragraphs of the given width. +The width can be an absolute width or (unlike in LaTeX) a positive decimal number at most 1. +If it is a decimal, it will be interpreted as a portion of the available width. C for a column that expands to fill (see C below), and will have left-aligned paragraphs @@ -99,8 +100,8 @@ =head3 All output formats C for a vertical rule of the indicated width (must be an absolute width; C<3pt> is just an example) -C{commands}> Execute C at each cell in the column. -For example, C<'cE{\color{blue}}c'> will make the second column have blue text. +C<< >{commands} >> Execute C at each cell in the column. +For example, C<< 'c>{\color{blue}}c' >> will make the second column have blue text. The following LaTeX commands may be used: =over @@ -123,11 +124,11 @@ =head3 All output formats Other LaTeX commands apply only to PDF output. -=item C string> +=item C<< align => string >> convenient short version of C -=item C number> +=item C<< Xratio => number >> When C is part of overall alignment, C must be some number between 0 and 1, inclusive of 1. @@ -135,26 +136,26 @@ =head3 All output formats horizontal space. And C columns expand to fill the available space. The default is 0.97. -=item C [ , ]> +=item C<< encase => [ , ] >> Encases all table entries in the two entries. For example, use C<[$BM,$EM]> to wrap all cells in math delimiters. See also C for individual cells. -=item C 0 or 1> +=item C<< rowheaders => 0 or 1 >> Make the first element of every row a row header. Default is 0. -=item C 0 or 1> +=item C<< headerrules => 0 or 1 >> Make a horizontal rule under a row of column headers and a vertical rule to the right of a column of row headers. Default is 1. -=item C 'top'> +=item C<< valign => 'top' >> Can be C<'top'>, C<'middle'>, or C<'bottom'>. Applies to all rows. See below to override for an individual row. -=item C [ , ]> +=item C<< padding => [ , ] >> An array of two non-negative numbers used to define cell-padding. The first is for top-down padding, the second for left-right padding. In HTML, each padding @@ -171,21 +172,21 @@ =head3 All output formats =head3 HTML output Each css property setting should be a hash reference. -For example, C<{'font-family' =E 'fantasy', color =E 'red'}>. +For example, C<< {'font-family' => 'fantasy', color => 'red'} >>. If a key has a dash character, it needs to be in quotes. Alternatively, -you may uses a javascript flavor of CSS key like C<{fontFamily =E 'fantasy'}> +you may uses a javascript flavor of CSS key like C<< {fontFamily => 'fantasy'} >> =over -=item C css string> +=item C<< tablecss => css string >> css styling commands for the table element -=item C css string> +=item C<< captioncss => css string >> css styling commands for the caption element -=item C array ref +=item C<< columnscss => array ref >> an array reference to css strings for columns @@ -203,15 +204,15 @@ =head3 HTML output =back -=item C css string> +=item C<< datacss => css string >> css styling commands for non-header cells -=item C css string> +=item C<< headercss => css string >> css styling commands for header cells -=item C css string> +=item C<< allcellcss => css string >> css styling commands for all cells @@ -221,7 +222,7 @@ =head3 PDF hardcopy output =over -=item C 0 or 1> +=item C<< booktabs => 0 or 1 >> use the booktabs package for horizontal rules (default 1) @@ -243,14 +244,14 @@ =head3 All output formats =over -=item C string> +=item C<< halign => string >> Similar to the components for C above. However, only C, C, C, C, and vertical rule specifications should be used. With vertical rule specifiers, any left vertical rule will only be observed for cells is in the first column. Otherwise, use a right vertical rule on the cell to the left. -=item C
                      type>, +=item C<< header => type >>, Declares the scope of the HTML C element. Case-insensitive: @@ -267,41 +268,41 @@ =head3 All output formats =back -=item C string> +=item C<< color => string >> color name or 6-character hex color code for text color -=item C string> +=item C<< bgcolor => string >> color name or 6-character hex color code for background color -=item C1> +=item C<< b=>1 >> Set the cell to bold font. -=item C1> +=item C<< i=>1 >> Set the cell to italics font. -=item C1> +=item C<< m=>1 >> Set the cell to monospace font. -=item C 0 or 1> +=item C<< noencase => 0 or 1 >> If you are using encase (see above) use this to opt out. -=item C positive integer> +=item C<< colspan => positive integer >> Makes the cell span more than one column. When using this, you often set C as well. -=item C positive integer or string> +=item C<< top => positive integer or string >> Make a top rule for one cell if the cell is in the top row. Thickness is either C pixels or a width like C<'0.04em'>. Has no effect on cells outside of top row. -=item C positive integer or string> +=item C<< bottom => positive integer or string >> Make a bottom rule for one cell. Thickness is either C pixels or a width like C<'0.04em'>. @@ -313,7 +314,7 @@ =head3 HTML output =over -=item C string> +=item C<< cellcss => string >> css styling commands for this cell @@ -325,12 +326,12 @@ =head3 PDF hardcopy output =over -=item C tex code> and C tex code> +=item C<< texpre => tex code >> and C<< texpost => tex code >> For more fussy cell-by-cell alteration of the tex version of the table, code to place before and after the cell content. -=item C array ref> +=item C<< texencase => array ref >> Shortcut for entering C<[texpre,texpost]> at once. @@ -343,32 +344,49 @@ =head2 Options for ROWS =over -=item C string> +=item C<< rowcolor => string >> Sets the row's background color. Must be a color name, 6-character hex color code. -=item C string> +=item C<< rowcss => string >> css styling commands for the row -=item C 0 or 1> +=item C<< headerrow => 0 or 1 >> Makes an entire row use header cells (with column scope). -=item C positive integer or string> +=item C<< rowtop => positive integer or string >> When used on the first row, creates a top rule. Has no effect on other rows. Thickness is either C pixels or a width like C<'0.04em'>. -=item C positive integer string> +=item C<< rowbottom => positive integer string >> Make a bottom rule. Thickness is either C pixels or a width like C<'0.04em'>. -=item C string> +=item C<< valign => string >> Override table's overall vertical alignment for this row. Can be C<'top'>, C<'middle'>, or C<'bottom'>. +=item C<< rows => 2D array reference >> + +If a row contains only one cell with no content or attributes other than C<'rows'>, +and if C<'rows'> is an array reference where each element is itself an array +reference that is appropriately formatted to be a niceTables row, then this row +will be expanded to those rows. This allows a sequence of rows to be computed +algorithmically for example C<[{rows => [ map {[ $_, $_**2 ]} (1..4)]}]> will +expand to C<[1, 1], [2, 4], [3, 9], [4, 16]>. + +This can also be achieved if the cell is an array reference with (possibly empty) +whitespace content followed by the rows attribute, for example: +C<[['', 'rows', [ map {[ $_, $_**2 ]} (1..4)]]]>. This form is used by PGML. So +for instance when using PGML, C<[. .]*{rows => $rows}>. + +This expansion is not recursive; any C<'rows'> attribute in the inner rows will +not be expanded. + =back =head2 Options for COLUMNS @@ -382,9 +400,9 @@ =head2 Deprecations =over =item * Each css setting can be a raw CSS string, including all its colons and a semicolons. -For example, C 'font-family: fantasy; text-decoration: underline;'>. +For example, C<< tablecss => 'font-family: fantasy; text-decoration: underline;' >>. -=item * A cell can have C commands>. +=item * A cell can have C<< tex => commands >>. This executes commands at start of a cell with scope the entire cell. The following LaTeX commands may be used and respected in HTML as well as LaTeX: @@ -519,7 +537,8 @@ sub TableEnvironment { if ($main::displayMode eq 'TeX') { my $tabulartype = $hasX ? 'tabularx' : 'tabular'; my $tabularwidth = $hasX ? "$tableOpts->{Xratio}\\linewidth" : ''; - $rows = latexEnvironment($rows, $tabulartype, [ $tabularwidth, '[t]', $tableOpts->{texalignment} ], ' '); + my $texalignment = $tableOpts->{texalignment} =~ s/p\{0*(0(\.\d*)?|1(\.0*)?)\}/p\{$1\\linewidth\}/gr; + $rows = latexEnvironment($rows, $tabulartype, [ $tabularwidth, '[t]', $texalignment ], ' '); $rows = prefix($rows, '\centering%') if $tableOpts->{center}; $rows = prefix($rows, '\renewcommand{\arraystretch}{' . ($tableOpts->{padding}[0] + 1) . '}', ''); $rows = prefix($rows, '\setlength{\tabcolsep}{' . ($tableOpts->{padding}[1] * 10) . 'pt}', ''); @@ -549,7 +568,6 @@ sub TableEnvironment { $ptxmargins = "${leftmargin}% ${rightmargin}%"; $ptxwidth .= '%'; } elsif (!$tableOpts->{center}) { - $ptxwidth = '100%'; $ptxmargins = '0% 0%'; } if ($tableOpts->{LaYoUt}) { @@ -557,8 +575,8 @@ sub TableEnvironment { $rows, 'sbsgroup', { - width => $ptxwidth, margins => $ptxmargins, + widths => $cols } ); } elsif (!$tableOpts->{LaYoUt}) { @@ -567,13 +585,12 @@ sub TableEnvironment { $rows, 'tabular', { - valign => ($tableOpts->{valign} ne 'middle') ? $tableOpts->{valign} : '', - bottom => $tableOpts->{horizontalrules} ? 'minor' : '', - rowheaders => $tableOpts->{rowheaders} ? 'yes' : '', - margins => $ptxmargins, - width => $ptxwidth, - left => $ptxleft, - top => $ptxtop, + valign => ($tableOpts->{valign} ne 'middle') ? $tableOpts->{valign} : '', + bottom => $tableOpts->{horizontalrules} ? 'minor' : '', + 'row-headers' => $tableOpts->{rowheaders} ? 'yes' : '', + width => $ptxwidth, + left => $ptxleft, + top => $ptxtop, } ); } @@ -657,30 +674,34 @@ sub Cols { } if ($main::displayMode eq 'PTX') { - my $ptxhalign = ''; - $ptxhalign = 'center' if ($align->{halign} eq 'c'); - $ptxhalign = 'right' if ($align->{halign} eq 'r'); - my $ptxright = ''; - $ptxright = getPTXthickness($align->{right}); - my $ptxtop = ''; - $ptxtop = getPTXthickness($top); - my $ptxwidth = ''; - $ptxwidth = getWidthPercent($align->{width}) if $align->{width}; - $ptxwidth = ($tableOpts->{Xratio} / $#$alignment * 100) . '%' - if ($align->{halign} eq 'X'); - $ptxwidth = getWidthPercent($width) if $width; - push( - @cols, - tag( - '', 'col', - { - halign => $ptxhalign, - right => $ptxright, - top => $ptxtop, - width => $ptxwidth - } - ) - ); + if ($tableOpts->{LaYoUt}) { + push @cols, ($align->{width} ? getWidthPercent($align->{width}) : '%'); + } else { + my $ptxhalign = ''; + $ptxhalign = 'center' if ($align->{halign} eq 'c'); + $ptxhalign = 'right' if ($align->{halign} eq 'r'); + my $ptxright = ''; + $ptxright = getPTXthickness($align->{right}); + my $ptxtop = ''; + $ptxtop = getPTXthickness($top); + my $ptxwidth = ''; + $ptxwidth = getWidthPercent($align->{width}) if $align->{width}; + $ptxwidth = ($tableOpts->{Xratio} / $#$alignment * 100) . '%' + if ($align->{halign} eq 'X'); + $ptxwidth = getWidthPercent($width) if $width; + push( + @cols, + tag( + '', 'col', + { + halign => $ptxhalign, + right => $ptxright, + top => $ptxtop, + width => $ptxwidth + } + ) + ); + } } else { my $htmlright = ''; $htmlright .= css('border-right', 'solid 2px') @@ -688,6 +709,12 @@ sub Cols { $htmlright .= css('border-right', getRuleCSS($align->{right})); my $htmltop = ''; $htmltop .= css('border-top', getRuleCSS($top)); + my $htmlwidth = ''; + if ($align->{width}) { + $htmlwidth = css('width', $align->{width}); + $htmlwidth = css('width', getWidthPercent($align->{width})) + if ($align->{width} =~ /^0*(0(\.\d*)?|1(\.0*)?)$/); + } # $i starts at 1, but columncss indexing starts at 0 my $htmlcolcss = css($columnscss->[ $i - 1 ]); @@ -695,11 +722,39 @@ sub Cols { $htmlcolcss .= css('background-color', ($1 ? '#' : '') . $2); } - push(@cols, tag('', 'col', { style => "${htmlright}${htmltop}${htmlcolcss}" })); + push(@cols, tag('', 'col', { style => "${htmlright}${htmltop}${htmlcolcss}${htmlwidth}" })); } } + if ($main::displayMode eq 'PTX' && $tableOpts->{LaYoUt}) { + my @decimalcols = map { substr $_, 0, -1 } @cols; + my $total = 0; + my $count = 0; + for (@decimalcols) { + if ($_ eq '') { + $count++; + } else { + $total += $_; + } + } + # determine if somewhere in the alignment there are X columns + my $hasX = 0; + for my $align (@$alignment) { + if ($align->{halign} eq 'X') { + $hasX = 1; + last; + } + } + my $width = ($hasX ? $tableOpts->{Xratio} * 100 : 100); + my $fill = ($count != 0) ? int(($width - $total) / $count * 10**4) / 10**4 : 0; + for (@decimalcols) { + $_ = $fill if ($_ eq ''); + } + @cols = map { $_ . '%' } @decimalcols; + return join(' ', @cols); + } + return join("\n", @cols); } @@ -788,52 +843,11 @@ sub Rows { if (!$ptxleft && $rowArray->[0]{halign} && $alignment->[0]{left}); if ($tableOpts->{LaYoUt}) { - my $ptxwidthsum = 0; - my $ptxautocols = $#alignment; - for my $j (1 .. $#alignment) { - if ($rowArray->[ $j - 1 ]{width}) { - $ptxwidthsum += - substr getWidthPercent($tableArray->[ $j - 1 ]{width}), - 0, -1; - $ptxautocols -= 1; - } elsif ($alignment->[$j]{width}) { - $ptxwidthsum += substr getWidthPercent($alignment->[$j]{width}), 0, -1; - $ptxautocols -= 1; - } - } - - # determine if somewhere in the overall alignment, there are X columns - my $hasX = 0; - for my $align (@$alignment) { - if ($align->{halign} eq 'X') { - $hasX = 1; - last; - } - } - my $leftoverspace = - (($hasX) ? $tableOpts->{Xratio} * 100 : 100) - $ptxwidthsum; - my $divvyuptherest = 0; - $divvyuptherest = int($leftoverspace / $ptxautocols * 10000) / 10000 - unless ($ptxautocols == 0); - my @ptxwidths; - for my $j (1 .. $#alignment) { - if ($rowOpts->[ $j - 1 ]{width}) { - push(@ptxwidths, getWidthPercent($rowOpts->[ $j - 1 ]{width})); - } elsif ($alignment->[$j]{width}) { - push(@ptxwidths, getWidthPercent($alignment->[$j]{width})); - } else { - push(@ptxwidths, $divvyuptherest . '%'); - } - } - - my $ptxwidths = join(" ", @ptxwidths); $row = tag( $row, 'sidebyside', { - valign => ($valign) ? $valign : $tableOpts->{valign}, - margins => '0% 0%', - widths => $ptxwidths, + valign => ($valign) ? $valign : $tableOpts->{valign}, } ); } else { @@ -937,6 +951,7 @@ sub Row { || ($tableOpts->{rowheaders} && $tableOpts->{headerrules} && $i == 0)) { my $columntype = $cellOpts->{halign}; + $columntype = $columntype =~ s/p\{0*(0(\.\d*)?|1(\.0*)?)\}/p\{$1\\linewidth\}/gr; $columntype = $cellAlign->{halign} // 'l' unless $columntype; $columntype = 'p{' . $tableOpts->{Xratio} / ($#$rowArray + 1) . "\\linewidth}" if ($columntype eq 'X'); @@ -1021,8 +1036,11 @@ sub Row { if ($cellAlign->{halign} eq 'c'); $css .= css('text-align', 'right') if ($cellAlign->{halign} eq 'r'); - $css .= css('width', $cellAlign->{width}) - if ($cellAlign->{width}); + if ($cellAlign->{width} =~ /^0*(0(\.\d*)?|1(\.0*)?)$/) { + $css .= css('width', getWidthPercent($1)); + } elsif ($cellAlign->{width}) { + $css .= css('width', $cellAlign->{width}); + } $css .= css('font-weight', 'bold') if ($cellAlign->{tex} =~ /\\bfseries/); $css .= css('font-style', 'italic') @@ -1058,8 +1076,11 @@ sub Row { if ($cellOpts->{halign} =~ /^c/); $css .= css('text-align', 'right') if ($cellOpts->{halign} =~ /^r/); $css .= css('text-align', 'left') if ($cellOpts->{halign} =~ /^p/); - $css .= css('width', $1) - if ($cellOpts->{halign} =~ /^p\{([^}]*?)}/); + if ($cellOpts->{halign} =~ /^p\{0*(0(\.\d*)?|1(\.0*)?)}/) { + $css .= css('width', getWidthPercent($1)); + } elsif ($cellOpts->{halign} =~ /^p\{([^}]*?)}/) { + $css .= css('width', $1); + } $css .= css('font-weight', 'bold') if ($cellOpts->{tex} =~ /\\bfseries/); $css .= css('font-style', 'italic') @@ -1113,7 +1134,20 @@ sub Row { # Takes the user's nested array and returns a cleaned up version with initializations sub TableArray { - my $userArray = shift; + my $userArray = shift; + for my $i (reverse(0 .. $#$userArray)) { + if (@{ $userArray->[$i] } == 1) { + if (ref($userArray->[$i][0]) eq 'HASH' && defined($userArray->[$i][0]{rows})) { + splice(@{$userArray}, $i, 1, @{ $userArray->[$i][0]{rows} }); + } elsif (ref($userArray->[$i][0]) eq 'ARRAY' + && @{ $userArray->[$i][0] } == 3 + && $userArray->[$i][0][0] =~ /^\s*$/ + && $userArray->[$i][0][1] eq 'rows') + { + splice(@{$userArray}, $i, 1, @{ $userArray->[$i][0][2] }); + } + } + } my %supportedOptions = ( data => '', halign => '', @@ -1282,10 +1316,10 @@ sub ParseAlignment { $alignment =~ s/\R//g; # first we parse things like *{20}{...} to expand them - my $pattern = qr/\*\{(\d+)\}\{(.*?)\}/; + my $pattern = qr/\*\{(\d+)\}(\{((?:(?>[^\{\}]+)|(?2))*)\})/; while ($alignment =~ /$pattern/) { my @captured = ($alignment =~ /$pattern/); - my $replaceWith = $captured[1] x $captured[0]; + my $replaceWith = $captured[2] x $captured[0]; $alignment =~ s/$pattern/$replaceWith/; } @@ -1365,7 +1399,7 @@ sub ParseAlignment { # could parse these further for color identification, etc } else { - main::WARN_MESSAGE("Token $token in texalignment could not be parsed"); + main::WARN_MESSAGE("Token $token in texalignment could not be parsed") unless ($token =~ /^\s*$/); } } @@ -1551,29 +1585,39 @@ sub getPTXthickness { } sub getWidthPercent { - my $absWidth = shift; - my $x = 0; - my $unit = 'cm'; - if ($absWidth =~ /^(\.\d+|\d+\.?\d*)\s*(\w+)/) { + my $width = shift; + return $width if (substr($width, -1) eq '%'); + return $width * 100 . '%' if ($width =~ /^0*(0(\.\d*)?|1(\.0*)?)$/); + my $x = 0; + my $unit = 'cm'; + if ($width =~ /^(\.\d+|\d+\.?\d*)\s*(\w+)$/) { $x = $1; $unit = $2; } - my %convert_to_cm = ( - 'pt' => 1 / 864 * 249 / 250 * 12 * 2.54, - 'mm' => 1 / 10, - 'cm' => 1, - 'in' => 2.54, - 'ex' => 0.15132, - 'em' => 0.35146, - 'mu' => 0.35146 / 8, - 'sp' => 1 / 864 * 249 / 250 * 12 * 2.54 / 65536, - 'bp' => 2.54 / 72, - 'dd' => 1 / 864 * 249 / 250 * 12 * 2.54 * 1238 / 1157, - 'pc' => 1 / 864 * 249 / 250 * 12 * 2.54 * 12, - 'cc' => 1 / 864 * 249 / 250 * 12 * 2.54 * 1238 / 1157 * 12, - 'px' => 2.54 / 72, + my %convert_to_pt = ( + # units with related absolute defintions + # the following are as TeX defines them + pt => 1, + pc => 12, + in => 72.27, + mm => 72.27 / 25.4, + cm => 72.27 / 2.54, + sp => 1 / 65536, + dd => 1238 / 1157, + cc => 12 * 1238 / 1157, + bp => 72.27 / 72, + # CSS defines 1 px to be 1/96 of an inch + # note that px is not a legal unit in TeX + px => 72 / 96, + # units relative to font + # the following are based on TeX default font + # (10pt Computer Modern) + em => 10.00002, + ex => 4.30554, ); - return (int($x * $convert_to_cm{$unit} / (6.25 * 2.54) * 10000) / 100) . '%'; + # This is only used for PTX output, and a PTX document's default width is 340pt. + # We offer a percent with up to six significant digits + return (int($x * $convert_to_pt{$unit} / 340 * 10**6) / 10**4) . '%'; } sub hrule { diff --git a/macros/ui/problemPanic.pl b/macros/ui/problemPanic.pl index c00c2c3b0c..50db3367ef 100644 --- a/macros/ui/problemPanic.pl +++ b/macros/ui/problemPanic.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/macros/ui/quickMatrixEntry.pl b/macros/ui/quickMatrixEntry.pl index 8b5bdcacbb..f10cfe870e 100644 --- a/macros/ui/quickMatrixEntry.pl +++ b/macros/ui/quickMatrixEntry.pl @@ -1,120 +1,148 @@ -#!/usr/bin/perl -w +################################################################################ +# WeBWorK Online Homework Delivery System +# Copyright © 2000-2024 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. +################################################################################ -################################### -# quick matrix entry package -################################### +loadMacros('MathObjects.pl'); -sub _quickMatrixEntry_init { }; # don't reload this file +sub _quickMatrixEntry_init { + ADD_JS_FILE('js/QuickMatrixEntry/quickmatrixentry.js', 0, { defer => undef }); + return; +} + +sub QuickMatrixEntry { return parser::QuickMatrixEntry->new(@_) } + +package parser::QuickMatrixEntry; +our @ISA = qw(Value::Matrix); + +# Allow promotion of Value::Matrix objects. +sub promote { + my ($self, @args) = @_; + my $context = Value::isContext($args[0]) ? shift @args : $self->context; + my $x = @args ? shift @args : $self; + return $self->new($context, $x, @args) if @args || ref($x) eq 'ARRAY'; + $x = Value::makeValue($x, context => $context); + return $x->inContext($context) if ref($x) eq 'Value::Matrix' || ref($x) eq (ref($self) || $self); + return $self->make($context, @{ $x->data }) if Value::classMatch($x, 'Point', 'Vector'); + Value::Error(q{Can't convert %s to %s}, Value::showClass($x), Value::showClass($self)); + return; +} + +sub ans_array { + my ($self, $size, @options) = @_; + + my $name = main::NEW_ANS_NAME(); + main::RECORD_IMPLICIT_ANS_NAME($name); -sub INITIALIZE_QUICK_MATRIX_ENTRY { - main::HEADER_TEXT($quick_entry_javascript); - main::TEXT($quick_entry_form); - return ''; + my ($rows, $columns) = $self->dimensions; + + return main::tag( + 'div', + class => 'my-2', + main::tag( + 'button', + class => 'quick-matrix-entry-btn btn btn-secondary', + type => 'button', + name => $name, + data_rows => $rows, + data_columns => $columns, + main::maketext('Quick Entry') + ) + ) . $self->SUPER::named_ans_array($name, $size, @options, answer_group_name => $name); } -# +sub type { return 'Matrix'; } +sub class { return 'Matrix'; } + +# Backwards compatibility. This is deprecated and should not be used. + +package main; + +sub INITIALIZE_QUICK_MATRIX_ENTRY { } + sub MATRIX_ENTRY_BUTTON { - my $answer_number = shift; - # warn(" input reference is ". ref($answer_number)); - my ($rows, $columns) = @_; - if (ref($answer_number) =~ /Matrix/i) { # (handed a MathObject matrix) - my $matrix = $answer_number; - ($rows, $columns) = $matrix->dimensions(); - $answer_number = $main::PG->{unlabeled_answer_blank_count} + 1; - # the +1 assumes that the quick entry button comes before (above) the matrix answer blanks. + my ($matrix, $rows, $columns) = @_; + + my $answer_number; + + if (Value::isValue($matrix) && Value::classMatch($matrix, 'Matrix')) { + # Given a MathObject matrix. + ($rows, $columns) = $matrix->dimensions; + # This assumes that the quick entry button comes before the matrix answer blanks. + $answer_number = $main::PG->{answer_name_count} + 1; + } else { + $answer_number = $matrix; } - $rows = $rows // 1; - $columns = $columns // 5; - my $answer_name = "AnSwEr" . sprintf('%04d', $answer_number); - # warn("answer number $answer_name rows $rows columns $columns"); - return qq! - $PAR - - $PAR!; + + $rows //= 1; + $columns //= 5; + + return tag( + 'div', + class => 'my-2', + tag( + 'button', + class => 'quick-matrix-entry-btn btn btn-secondary', + type => 'button', + name => 'AnSwEr' . sprintf('%04d', $answer_number), + data_rows => $rows, + data_columns => $columns, + maketext('Quick Entry') + ) + ); } -our $quick_entry_javascript = <<'END_JS'; - -END_JS - -our $quick_entry_form = <<'END_TEXT'; -
                      - - -
                      -END_TEXT - -INITIALIZE_QUICK_MATRIX_ENTRY(); # only need the javascript to be entered once. 1; + +__END__ + +=head1 NAME + +quickMatrixEntry.pl - Add a button to MathObject C array answers that +allows pasting of matrix contents. + +=head1 DESCRIPTION + +A QuickMatrixEntry object lets you add a "Quick Entry" button to a MathObject +C array answer. When the button is clicked it opens a dialog in which +you can edit the entries of the matrix in a text area. This allows pasting of +large matrices from other sources. + +A QuickMatrixEntry object is created in much the same way that a MathObject +C is created. Set the context to the C context, and call the +C method with an array of arrays. For example, + + Context('Matrix'); + + $matrix = QuickMatrixEntry([ + [ 1, 2, 3, 4, 5, 6, 7, 8 ], + [ 8, 7, 6, 5, 4, 3, 2, 1 ], + [ 1, 2, 3, 4, 5, 6, 7, 8 ], + [ 8, 7, 6, 5, 4, 3, 2, 1 ], + [ 1, 2, 3, 4, 5, 6, 7, 8 ] + ]); + +Then add the array of answer rules to the problem with + + BEGIN_PGML + [_]*{$matrix}{4} + END_PGML + +Other than the button that is added above the array of answers, the +C is just a MathObject C, and everything that can be +done with a MathObject C can also be done with a C. +However, if not used via C (the starred answer form in PGML) the +button will not be added and the C object will be nothing more +than a MathObject C. This generally should not be done. + +=cut diff --git a/macros/ui/unionTables.pl b/macros/ui/unionTables.pl index 0032a32789..cf9ea33824 100644 --- a/macros/ui/unionTables.pl +++ b/macros/ui/unionTables.pl @@ -1,6 +1,6 @@ ################################################################################ # WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork +# Copyright © 2000-2024 The WeBWorK Project, https://github.com/openwebwork # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the diff --git a/t/build_PG_envir.pl b/t/build_PG_envir.pl index 929ad84c0a..e5072e779c 100644 --- a/t/build_PG_envir.pl +++ b/t/build_PG_envir.pl @@ -15,12 +15,6 @@ %main::envir = %{ WeBWorK::PG::defineProblemEnvironment(WeBWorK::PG::Environment->new) }; -sub be_strict { - require ww_strict; - strict::import(); - return; -} - sub PG_restricted_eval { my @input = @_; return WeBWorK::PG::Translator::PG_restricted_eval(@input); diff --git a/t/contexts/baseN.t b/t/contexts/baseN.t new file mode 100644 index 0000000000..9f38146a5f --- /dev/null +++ b/t/contexts/baseN.t @@ -0,0 +1,209 @@ +#!/usr/bin/env perl + +=head1 BaseN context + +Test the functionality for the BaseN context. + +=cut + +use Test2::V0 '!E', { E => 'EXISTS' }; + +die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT}; +do "$ENV{PG_ROOT}/t/build_PG_envir.pl"; + +use lib "$ENV{PG_ROOT}/lib"; + +loadMacros('PGstandard.pl', 'MathObjects.pl', 'contextBaseN.pl'); + +use Value; +require Parser::Legacy; +import Parser::Legacy; + +Context('BaseN'); + +subtest 'conversion from a non-decimal base to base 10' => sub { + is convertBase('101010', from => 2), 42, 'convert from base 2'; + is convertBase('44011', from => 5), 3006, 'convert from base 5'; + is convertBase('5073', from => 8), 2619, 'convert from base 8'; + is convertBase('98A', from => 12), 1402, 'convert from base 12'; + is convertBase('98T', from => [ '0' .. '9', 'T', 'E' ]), 1402, 'convert from base 12 with non-standard digits'; + is convertBase('9FE8', from => 16), 40936, 'convert from base 16'; +}; + +subtest 'Convert from decimal to non-decimal bases' => sub { + is convertBase(12, to => 2), '1100', 'convert to base 2'; + is convertBase(47, to => 2), '101111', 'convert to base 2'; + + is convertBase(98, to => 5), '343', 'convert to base 5'; + is convertBase(761, to => 5), '11021', 'convert to base 5'; + + is convertBase(519, to => 8), '1007', 'convert to base 8'; + is convertBase(2023, to => 8), '3747', 'convert to base 8'; + + is convertBase(853, to => 12), '5B1', 'convert to base 12'; + is convertBase(2023, to => 12), '1207', 'convert to base 12'; + is convertBase(1678, to => [ '0' .. '9', 'T', 'E' ]), 'E7T', 'convert to base 12 using non-standard digits'; + + is convertBase(5752, to => 16), '1678', 'convert to base 16'; + is convertBase(41446, to => 16), 'A1E6', 'convert to base 16'; +}; + +subtest 'Convert between two non-decimal bases' => sub { + is convertBase('1234', from => 5, to => 16), 'C2', 'convert from base 5 to 16'; + is convertBase('1111101', from => 2, to => [ 0 .. 9, 'T', 'E' ]), 'T5', + 'convert from base 2 to base 12 with non-standard digits'; +}; + +# Now test the Context. +Context('BaseN')->setBase(5); + +subtest 'Check that the Context parses number correct' => sub { + is Context()->{base}, 5, 'Check that the base is stored.'; + is Context()->{digits}, [ 0 .. 4 ], 'Check that the digits are updated.'; + ok my $a1 = Compute('10'), "The string '10' is created"; + is $a1->value, 5, "The base-5 string '10' is 5 in base 10"; + ok my $a2 = Compute('242'), "The string '242' is created."; + is $a2->value, 72, "The base-5 string '242' is 72 in base 10"; +}; + +subtest 'check that non-valid digits return errors' => sub { + like dies { Compute('456'); }, qr/Numbers should consist only of the digits:/, + 'Try to build a base-5 number will illegal digits'; +}; + +subtest 'check arithmetic in base-5' => sub { + ok my $a1 = Compute('4021'), "Base-5 number '4021' parsed."; + is $a1->value, 511, "Base-5 number '4021' is 511 in base-10"; + + ok my $a2 = Compute('2334'), "Base-5 number '2334' parsed."; + is $a2->value, 344, "Base-5 number '2334' is 344 in base-10"; + + my $a3 = Compute("$a2+$a1"); + is $a3->string, '11410', '4021+2334=11410 in base-5'; + + my $a4 = Compute("$a1-$a2"); + is $a4->string, '1132', '4021-2334=1132 in base-5'; + + my $a5 = Compute("$a1*$a2"); + is $a5->string, '21111114', '4021*2334=21111114 in base-5'; + + my $a6 = Compute("$a1^2"); + is $a6->string, '31323441', '4021^2 = 31323441 in base-5'; + + my $a7 = Compute('23'); + my $a8 = $a1 / $a7; + is $a8->string, '124', '4021/23 = 124 in base-5'; + + is Compute('4021/23')->string, '124', "Compute('4021/23) = 124 in base-5"; +}; + +subtest 'check arithmetic in base-16' => sub { + Context()->setBase(16); + ok my $a1 = Compute('AE'), "Base-16 number 'AE' parsed."; + is $a1->value, 174, "Base-16 number 'AE' is 175 in base-10"; + + ok my $a2 = Compute('D8'), "Base-16 number 'D8' parsed."; + is $a2->value, 216, "Base-16 number 'D8' is 216 in base-10"; + + my $a3 = Compute("$a2+$a1"); + is $a3->string, '186', 'AE+D8=186 in base-16'; + + my $a4 = Compute("$a2-$a1"); + is $a4->string, '2A', 'D8-AE=2A in base-16'; + + my $a5 = Compute("$a2*$a1"); + is $a5->string, '92D0', 'AE*D8=92D0 in base-16'; + + my $a6 = Compute("$a2^2"); + is $a6->string, 'B640', 'D8^2=B640 in base-16'; + + my $a7 = Compute("A2E"); + my $a8 = Compute("B6"); + my $a9 = $a7 / $a8; + is $a9->string, 'E', 'A2E/B6=E in base-16 (using perl expression)'; + + my $a10 = Compute("A2E/B6"); + is $a10->string, 'E', 'A2E/B6=E in base-16 (using Compute)'; +}; + +subtest 'Use alternative digits' => sub { + Context()->setBase([ 0 .. 9, 'T', 'E' ]); + ok my $a1 = Compute('E9'), "Base 12 number 'E9' with E=eleven"; + is $a1->value, 141, "Base-12 number E9=141"; + + ok my $a2 = Compute("3TE"), "Base 12 number '3TE' with T=ten and E = eleven"; + like dies { Compute('A5'); }, qr/Numbers should consist only of the digits:/, 'Check that A=10 is not allowed'; +}; + +subtest 'check for other errors' => sub { + Context('BaseN'); + like dies { Compute('1234') }, qr/The base must be set for this context/, + 'Check that there is a error if the base is not set.'; + + like dies { Context()->setBase(1); }, qr/Base must be at least 2/, 'Check that the base is at least 2'; + like dies { Context()->setBase(8.5); }, qr/Base must be an integer/, 'Check that the base is an integer'; + like dies { Context()->setBase(40); }, qr/You must provide a digit list for bases bigger than 36/, + 'Check that there is a digit list for large bases'; +}; + +subtest 'Check the LimitedBaseN features' => sub { + Context('LimitedBaseN')->setBase(5); + + like dies { Compute("104+320"); }, qr/Can't use '\+' in this context/, "Check that '+' is not allowed."; + like dies { Compute("320-104"); }, qr/Can't use '\-' in this context/, "Check that '-' is not allowed."; + like dies { Compute("14*23"); }, qr/Can't use '\*' in this context/, "Check that '*' is not allowed."; + like dies { Compute("14 23"); }, qr/Can't use '\*' in this context/, "Check that '*' is not allowed."; + like dies { Compute("4221/13"); }, qr/Can't use '\/' in this context/, "Check that '*' is not allowed."; + like dies { Compute("23^2"); }, qr/Can't use '\^' in this context/, "Check that '^' is not allowed."; + +}; + +subtest 'Test with different set of digits' => sub { + Context('BaseN')->setBase([ 0 .. 9, 'B', 'D' ]); + + ok my $a1 = Compute("3BD"), "Create '3BD' in base-12 with B=10, D=11"; + is $a1->value, 563, "'3BD'=563 in base-12 with B=10, D=11"; + + Context()->setBase([ 0 .. 9, 'T', 'E' ]); + ok my $a2 = Compute('E9T'), "Create 'E9T' in base-12 with T=10, E=11"; + is $a2->value, 1702, "'E9T'= 1702 in base-12 with T=10, E=11"; + +}; + +subtest 'Test for using named bases' => sub { + Context('BaseN')->setBase('binary'); + my $a1 = Compute('100101'); + is $a1->value, 37, "check base => 'binary'"; + + Context()->setBase('octal'); + my $a2 = Compute('367'); + is $a2->value, 247, "check base => 'octal'"; + + Context()->setBase('decimal'); + my $a3 = Compute('459'); + is $a3->value, 459, "check base => 'decimal'"; + + Context()->setBase('duodecimal'); + my $a4 = Compute('A91'); + is $a4->value, 1549, "check base => 'duodecimal'"; + + Context()->setBase('hexadecimal'); + my $a5 = Compute('3CB'); + is $a5->value, 971, "check base => 'hexadecimal'"; + + Context()->setBase('base64'); + my $a6 = Compute('Z_'); + is $a6->value, 1662, "check base => 'decimal'"; +}; + +subtest 'Test modulo operator' => sub { + Context('BaseN')->setBase('binary'); + my $a1 = Compute('100101 % 11'); + is $a1->value, 1, 'check binary modulo 100101 % 11'; + + Context()->setBase('octal'); + my $a2 = Compute('347 % 14'); + is $a2->value, 3, 'check octal modulo 347 % 14'; +}; + +done_testing(); diff --git a/t/contexts/fraction.t b/t/contexts/fraction.t index d873bba78e..6019df0ce0 100644 --- a/t/contexts/fraction.t +++ b/t/contexts/fraction.t @@ -21,9 +21,24 @@ import Parser::Legacy; Context('Fraction'); -ok my $a1 = Compute('1/2'); -ok my $a2 = Compute('2/4'); - -is $a1->value, $a2->value, 'contextFraction: reduce fractions'; +subtest 'contextFraction: Basic computation and reduction' => sub { + ok my $a1 = Compute('1/2'), 'compute 1/2'; + ok my $a2 = Compute('2/4'), 'compute 2/4'; + + is $a1->value, $a2->value, 'comparison (1/2 = 2/4)'; +}; + +subtest 'contextFraction: Conversion of real to fraction' => sub { + my ($result, $direct); + for my $num (1 .. 100) { + for my $den (1 .. 100) { + my $real = Real($num / $den); + push(@$result, Fraction($real)->value); + push(@$direct, Fraction($num, $den)->value); + } + } + + is $result, $direct, 'converted real gives correct fraction'; +}; done_testing(); diff --git a/t/macros/basicmacros.t b/t/macros/basicmacros.t index f7633c9426..1b6a1b086a 100644 --- a/t/macros/basicmacros.t +++ b/t/macros/basicmacros.t @@ -18,7 +18,7 @@ is($inputs[0]->attributes->{name}, $name, 'basicmacros: test NAMED_ANS_RULE nam is($inputs[0]->attributes->{type}, 'text', 'basicmacros: test NAMED_ANS_RULE type attribute'); ok(!$inputs[0]->attributes->{value}, 'basicmacros: test NAMED_ANS_RULE value attribute'); -is($inputs[1]->attributes->{name}, "previous_$name", 'basicmacros: test NAMED_ANS_RULE hidden name attribute'); -is($inputs[1]->attributes->{type}, 'hidden', 'basicmacros: test NAMED_ANS_RULE hidden type attribute'); +is($inputs[1]->attributes->{name}, "previous_$name", 'basicmacros: test NAMED_ANS_RULE hidden name attribute'); +is($inputs[1]->attributes->{type}, 'hidden', 'basicmacros: test NAMED_ANS_RULE hidden type attribute'); done_testing(); diff --git a/t/macros/numerical_methods.t b/t/macros/numerical_methods.t new file mode 100644 index 0000000000..e0e36f4f43 --- /dev/null +++ b/t/macros/numerical_methods.t @@ -0,0 +1,135 @@ +#!/usr/bin/env perl + +# Tests subroutines in the PGnumericamacros.pl macro. + +use Test2::V0 '!E', { E => 'EXISTS' }; + +die "PG_ROOT not found in environment.\n" unless $ENV{PG_ROOT}; +do "$ENV{PG_ROOT}/t/build_PG_envir.pl"; + +loadMacros('PGnumericalmacros.pl', 'MathObjects.pl', 'PGauxiliaryFunctions.pl'); + +subtest 'plot_list' => sub { + ok my $p1 = plot_list([ 0, 0, 1, 2 ]); + is &$p1(0.75), 1.5, 'linear interpolation at $x=0.75'; + is &$p1(0.25), 0.5, 'linear interpolation at $x=0.25'; + + ok my $p2 = plot_list([ (0, 0), (1, 2) ]); + is &$p2(0.75), 1.5, 'linear interpolation at $x=0.75'; + is &$p2(0.25), 0.5, 'linear interpolation at $x=0.25'; + + ok my $p3 = plot_list([ 0, 3 ], [ 4, 0 ]); + is &$p3(1.5), 2, 'linear interpolation at $x=0.75'; + is &$p3(2), 4 / 3, 'linear interpolation at $x=0.25'; + + like dies { plot_list([ 0, 1, 3, 4, 5 ]) }, + qr/single array of input has odd number/, + 'Input of odd number of values'; + like dies { plot_list(0, 1, 3, 4, 5) }, + qr/Error in plot_list:X values must be given as an array reference./, + 'Values are not given as an array reference'; +}; + +subtest 'horner' => sub { + ok my $h1 = horner([ 0, 1, 2 ], [ 1, -1, 2 ]); # 1-1*(x-0)+2(x-0)*(x-1) + is &$h1(0.5), 0, 'h1(0.5)=0'; #1-1*0.5+2*(0.5)*(-0.5) = 0 + is &$h1(1.5), 1, 'h1(1.5)=1'; # 1-1*(1.5)+2*(1.5)*(0.5)= 1 + + ok my $h2 = horner([ -1, 1, 2, 5 ], [ 2, 0, -2, 1 ]); # 2+0(x+1)-2(x+1)(x-1)+(x+1)(x-1)(x-2) + is &$h2(0), 6, 'h2(0)=6'; # 2-2(1)(-1)+(1)(-1)(-2) = 6 + is &$h2(3), -6, 'h2(3)=-6'; # 2-2(4)(2)+(4)(2)(1) = -6 + + like dies { horner([ 0, 1, 2 ], [ -1, 0, 2, 3 ]); }, + qr/The x inputs and q inputs must be the same length/, + 'Input array refs are different lengths.'; +}; + +subtest 'hermite' => sub { + ok my $h1 = hermite([ 0, 1 ], [ 0, 0 ], [ 1, -1 ]); # x-x^2 + is &$h1(0), 0, 'h1(0)=0'; + is &$h1(1), 0, 'h1(1)=0'; + is &$h1(0.5), 0.25, 'h1(0.5)=0.25'; + is &$h1(0.25), 0.1875, 'h1(0.25)=0.1875'; + + ok my $h2 = hermite([ 0, 1, 3 ], [ 2, 0, 1 ], [ 1, 0, -1 ]); + is &$h2(0), 2, 'h2(0)=2'; + is &$h2(1), 0, 'h2(1)=0'; + is Round(&$h2(3), 10), 1, 'h2(3)=1'; + is Round(&$h2(0.5), 10), Round(1573 / 1728, 10), 'h2(1/2)=1573/1728'; + is Round(&$h2(2), 10), Round(55 / 27, 10), 'h2(2)=55/27'; + + like dies { hermite([ 0, 1, 2 ], [ 1, 1, 1 ], [ 0, 2 ]) }, + qr/The input array refs all must be the same length/, + 'Input array refs are different lengths.'; +}; + +subtest 'hermite spline' => sub { + ok my $h = hermite_spline([ 0, 1, 3 ], [ 3, 1, -5 ], [ 1, -2, 0 ]); + is &$h(0), 3, 'h(0)=3'; + is &$h(1), 1, 'h(1)=1'; + is &$h(3), -5, 'h(3)=-5'; + is &$h(0.5), 19 / 8, 'h(1/2)=19/8'; + is &$h(2), -2.5, 'h(2)=-2.5'; + is &$h(1.3), 0.202, 'h(1.3)=0.202'; +}; + +subtest 'cubic spline' => sub { + ok my $s = cubic_spline([ 0, 1, 2 ], [ 0, 1, 0 ]); + is &$s(0), 0, 's(0)=0'; + is &$s(1), 1, 's(1)=1'; + is &$s(2), 0, 's(2)=0'; + # check intermediate points: + is &$s(0.25), 0.3671875, 'check s(0.25)'; + is &$s(0.5), 0.6875, 'check s(0.5)'; +}; + +subtest 'Riemann Sums' => sub { + my $f = sub { my $x = shift; return $x * $x; }; + is lefthandsum($f, 0, 2, steps => 4), 1.75, 'left hand sum of x^2 on [0,2]'; + is righthandsum($f, 0, 2, steps => 4), 3.75, 'right hand sum of x^2 on [0,2]'; + is midpoint($f, 0, 2, steps => 4), 2.625, 'midpoint rule of x^2 on [0,2]'; +}; + +subtest 'Quadrature' => sub { + my $f = sub { my $x = shift; return $x * $x; }; + my $g = sub { my $x = shift; return exp($x); }; + is simpson($f, 0, 2, steps => 4), 8 / 3, "Simpson's rule of x^2 on [0,2]"; + is Round(simpson($g, 0, 1), 7), Round(exp(1) - 1, 7), "Simpson's rule of e^x on [0,1]"; + like dies { simpson($f, 0, 2, steps => 5); }, + qr /Error: Simpson's rule requires an even number of steps./, + 'Check for odd number of steps'; + + is trapezoid($f, 0, 2, steps => 4), 2.75, 'Trapezoid rule of x^2 on [0,2]'; + + is romberg($f, 0, 2), 8 / 3, 'Romberg interation for x^2 on [0,2]'; + is romberg($g, 0, 1), exp(1) - 1, 'Romberg interation on e^x on [0,1]'; + + is inv_romberg($g, 0, exp(1) - 1), 1.0, 'Inverse Romberg to find b with int of e^x on [0,b] returns 1'; +}; + +subtest 'Runge Kutta 4th order' => sub { + my $f = sub { + my ($t, $y) = @_; + return $t * $t + $y * $y; + }; + my $rk4 = rungeKutta4( + $f, + initial_t => 0, + initial_y => 1, + dt => 0.2, + num_of_points => 5, + interior_points => 1 + ); + is [ map { $_->[0] } @$rk4 ], [ 0, 0.2, 0.4, 0.6, 0.8, 1.0 ], 'returns correct x values'; + is roundArray([ map { $_->[1] } @$rk4 ]), + roundArray([ 1, 1.25299088, 1.6959198, 2.6421097, 5.7854627, 99.9653469 ]), + 'returns correct y values'; +}; + +sub roundArray { + my ($arr, %options) = @_; + %options = (digits => 6, %options); + return [ map { defined($_) ? Round($_, $options{digits}) : $_ } @$arr ]; +} + +done_testing; diff --git a/t/pg_problems/problem_file.t b/t/pg_problems/problem_file.t index 03ab3842a0..4aa6e7e01d 100644 --- a/t/pg_problems/problem_file.t +++ b/t/pg_problems/problem_file.t @@ -23,11 +23,13 @@ is( qq{
                      \n} . qq{Enter a value for .\n} . qq{
                      \n} - . qq{} - . qq{\n} - . qq{
                      \n} - . qq{}, + . qq{
                      } + . qq{} + . qq{} + . qq{
                      } + . qq{\n} + . qq{\n}, 'body_text has correct content' ); @@ -102,9 +104,10 @@ is( { file => 'js/MathQuill/mqeditor.css', external => undef } ], extra_js_files => [ - { file => 'js/InputColor/color.js', external => 0, attributes => { defer => undef } }, + { file => 'js/Feedback/feedback.js', external => 0, attributes => { defer => undef } }, { file => 'js/Base64/Base64.js', external => 0, attributes => { defer => undef } }, { file => 'js/Knowls/knowl.js', external => 0, attributes => { defer => undef } }, + { file => 'js/Problem/details-accordion.js', external => 0, attributes => { defer => undef } }, { file => 'js/ImageView/imageview.js', external => 0, attributes => { defer => undef } }, { file => 'js/Essay/essay.js', external => 0, attributes => { defer => undef } }, { file => 'node_modules/mathquill/dist/mathquill.js', external => 0, attributes => { defer => undef } }, diff --git a/t/tikz_test/tikz_image.t b/t/tikz_test/tikz_image.t index bcc0e0e19a..d4c692c96c 100644 --- a/t/tikz_test/tikz_image.t +++ b/t/tikz_test/tikz_image.t @@ -28,7 +28,7 @@ ok my $img = image($drawing), 'img tag is generated'; like $img, qr! ^