From ded55a361c2bb3f2a4ac8a253604cce10d11f623 Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Thu, 28 May 2026 15:20:51 -0400 Subject: [PATCH 1/4] Add the ability to parse loadMacros if the arguments are in a qw block. Also, removes empty macros and duplicate macros. In addition, if it appears that the problem is already in PGML mode, then return the file without changes. --- lib/WeBWorK/PG/ConvertToPGML.pm | 67 ++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/lib/WeBWorK/PG/ConvertToPGML.pm b/lib/WeBWorK/PG/ConvertToPGML.pm index 04e4eeddba..b8f67c77e2 100644 --- a/lib/WeBWorK/PG/ConvertToPGML.pm +++ b/lib/WeBWorK/PG/ConvertToPGML.pm @@ -70,12 +70,21 @@ This returns a string that is the converted input string. =cut +use Mojo::Util qw(dumper); + # This stores the answers inside of ANS and related functions. my @ans_list; sub convertToPGML { my ($pg_source) = @_; + print dumper ($pg_source); + + # Check that the file is not already in PGML format by looking for PGML.pl in the loadMacros statement. + # and there are no BEGIN_TEXT, BEGIN_SOLUTION, etc. blocks. + + return $pg_source if ($pg_source =~ /loadMacros\((.*)PGML\.pl(.*)\)/ && $pg_source !~ /BEGIN_(TEXT|HINT|SOLUTION)/); + # First get a list of all of the ANS, LABELED_ANS, etc. in the problem. @ans_list = getANS($pg_source); @@ -101,29 +110,45 @@ sub convertToPGML { } elsif ($in_pgml_block) { push(@pgml_block, $row); } elsif ($row =~ /loadMacros\(/) { - # Parse the macros, which may be on multiple rows. - # Remove comments within loadMacros block (should we keep them?) - my $macros = $row; - while ($row && $row !~ /\);\s*$/) { + # Parse the macros, which may be on multiple rows and may be in a qw block. + my $macros = ''; + while (1) { + # Remove comments within loadMacros block (should we keep them?) + $row =~ s/#.*$//; + $macros .= $row; + last if ($row =~ /(.*)\);/); $row = shift @rows; - my @mrow = split(/#/, $row); - # This only adds the row if there is something relevent to the left of a # - $macros .= $mrow[0] if $mrow[0] !~ /^\s*$/; } - # Split by commas and pull out the quotes. - my @macros = - grep { $_ !~ /^#/ } - grep { - $_ !~ - /(PGstandard|PGML|PGauxiliaryFunctions|PGbasicmacros|PGanswermacros|MathObjects|PGcourse|AnswerFormatHelp).pl/ - } - map {s/['"\s]//gr} - split(/\s*,\s*/, $macros =~ s/loadMacros\((.*)\)\;$/$1/r); - - push(@all_lines, - 'loadMacros(' - . join(', ', map {"'$_'"} ('PGstandard.pl', 'PGML.pl', @macros, 'PGcourse.pl')) - . ');'); + + my @macros = (); + my ($qw_start, $qw_end); # the characters if the loadMacros has a qw block. + + # The following can parse loadMacros in the form loadMacros('macro1.pl', 'macro2.pl'); or + # loadMacros(qw{macro1.pl macro2.pl}); + if ($macros =~ /loadMacros\((qw(.))?(.*?)(.)?\)/ms) { + ($qw_start, $qw_end) = ($2, $4); + @macros = + grep { + $_ + && $_ !~ + /(PGstandard|PGML|PGauxiliaryFunctions|PGbasicmacros|PGanswermacros|MathObjects|PGcourse|AnswerFormatHelp).pl/ + } + map {s/['"]//gr} split(/\s+|\s*,\s*/, $3); + + # Remove any duplicates: + my %seen; + @macros = grep { !$seen{$_}++ } @macros; + } else { + warn 'The loadMacros statement in this file could not be processed.'; + } + + @macros = ('PGstandard.pl', 'PGML.pl', @macros, 'PGcourse.pl'); + + if ($qw_start) { + push(@all_lines, "loadMacros(qw$qw_start\n\t" . join("\n\t", @macros) . "\n$qw_end);"); + } else { + push(@all_lines, 'loadMacros(' . join(', ', map {"'$_'"} @macros) . ');'); + } } else { push(@all_lines, cleanUpCode($row)); } From 8fee5f9c7fd1debf11a36dd21e3f11eab4ca2364 Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Mon, 1 Jun 2026 09:13:55 -0400 Subject: [PATCH 2/4] remove debug statements --- lib/WeBWorK/PG/ConvertToPGML.pm | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/WeBWorK/PG/ConvertToPGML.pm b/lib/WeBWorK/PG/ConvertToPGML.pm index b8f67c77e2..31e5e0f5ee 100644 --- a/lib/WeBWorK/PG/ConvertToPGML.pm +++ b/lib/WeBWorK/PG/ConvertToPGML.pm @@ -70,16 +70,12 @@ This returns a string that is the converted input string. =cut -use Mojo::Util qw(dumper); - # This stores the answers inside of ANS and related functions. my @ans_list; sub convertToPGML { my ($pg_source) = @_; - print dumper ($pg_source); - # Check that the file is not already in PGML format by looking for PGML.pl in the loadMacros statement. # and there are no BEGIN_TEXT, BEGIN_SOLUTION, etc. blocks. From a14eed676eaee76f71b0a5bd683487c273a0b970 Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Tue, 9 Jun 2026 11:49:20 -0400 Subject: [PATCH 3/4] Handle cases where qw blocks are mixed with strings in ' or " in loadMacros --- lib/WeBWorK/PG/ConvertToPGML.pm | 34 +++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/WeBWorK/PG/ConvertToPGML.pm b/lib/WeBWorK/PG/ConvertToPGML.pm index 31e5e0f5ee..dfb9c5e863 100644 --- a/lib/WeBWorK/PG/ConvertToPGML.pm +++ b/lib/WeBWorK/PG/ConvertToPGML.pm @@ -107,29 +107,45 @@ sub convertToPGML { push(@pgml_block, $row); } elsif ($row =~ /loadMacros\(/) { # Parse the macros, which may be on multiple rows and may be in a qw block. - my $macros = ''; + my $macros = ''; + my $num_macro_lines = 0; # store the number of lines in the loadMacro so the output is similar to the input. while (1) { # Remove comments within loadMacros block (should we keep them?) $row =~ s/#.*$//; $macros .= $row; last if ($row =~ /(.*)\);/); + ++$num_macro_lines; $row = shift @rows; } - my @macros = (); my ($qw_start, $qw_end); # the characters if the loadMacros has a qw block. # The following can parse loadMacros in the form loadMacros('macro1.pl', 'macro2.pl'); or # loadMacros(qw{macro1.pl macro2.pl}); - if ($macros =~ /loadMacros\((qw(.))?(.*?)(.)?\)/ms) { - ($qw_start, $qw_end) = ($2, $4); + if ($macros =~ /loadMacros\((.*?)\);/ms) { + my @macro_str = split(/\s*,\s*/, $1); + + for my $str (@macro_str) { + if ($str =~ /^qw(.)/) { + my $qw_matches = { '{' => '}', '(' => ')', '[' => ']', '/' => '/', '|' => '|' }; + $qw_start = $1; + $qw_end = $qw_matches->{$qw_start}; + + if ($str =~ /^qw\Q${qw_start}\E(.*?)\Q${qw_end}\E/) { + push(@macros, split(/\s+/, $1)); + } + } else { + push(@macros, $str); + } + } + @macros = grep { $_ && $_ !~ /(PGstandard|PGML|PGauxiliaryFunctions|PGbasicmacros|PGanswermacros|MathObjects|PGcourse|AnswerFormatHelp).pl/ } - map {s/['"]//gr} split(/\s+|\s*,\s*/, $3); + map {s/['"]//gr} @macros; # Remove any duplicates: my %seen; @@ -141,7 +157,13 @@ sub convertToPGML { @macros = ('PGstandard.pl', 'PGML.pl', @macros, 'PGcourse.pl'); if ($qw_start) { - push(@all_lines, "loadMacros(qw$qw_start\n\t" . join("\n\t", @macros) . "\n$qw_end);"); + if ($num_macro_lines > 1) { # put each macro on a separate line + push(@all_lines, "loadMacros(qw$qw_start"); + push(@all_lines, "\t$_") for (@macros); + push(@all_lines, "$qw_end);"); + } else { + push(@all_lines, "loadMacros(qw$qw_start" . join(' ', @macros) . "$qw_end);"); + } } else { push(@all_lines, 'loadMacros(' . join(', ', map {"'$_'"} @macros) . ');'); } From e8114b75a467fbc06f29216ff2f68fa9901727da Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Thu, 25 Jun 2026 07:03:02 -0400 Subject: [PATCH 4/4] Add some error checking for loadMacros on the source Also, return a hash of the converted code and any errors. --- bin/convert-to-pgml.pl | 10 +++++++--- lib/WeBWorK/PG/ConvertToPGML.pm | 33 +++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/bin/convert-to-pgml.pl b/bin/convert-to-pgml.pl index 31d230411f..63059cc622 100755 --- a/bin/convert-to-pgml.pl +++ b/bin/convert-to-pgml.pl @@ -70,14 +70,18 @@ ($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); + my $pg_source = $path->slurp; + my $result = convertToPGML($pg_source); + if (ref($result) eq 'HASH' && $result->{errors}) { + warn "Error parsing $filename. " . $result->{errors}; + return; + } # 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); + $new_path->spurt($result->{pgmlCode}); print "Writing converted file to $new_path\n" if $verbose; print "Backing up original file to $backup_file\n" if $verbose && $backup; } diff --git a/lib/WeBWorK/PG/ConvertToPGML.pm b/lib/WeBWorK/PG/ConvertToPGML.pm index dfb9c5e863..d3abb491df 100644 --- a/lib/WeBWorK/PG/ConvertToPGML.pm +++ b/lib/WeBWorK/PG/ConvertToPGML.pm @@ -76,12 +76,17 @@ my @ans_list; sub convertToPGML { my ($pg_source) = @_; - # Check that the file is not already in PGML format by looking for PGML.pl in the loadMacros statement. + # Check that the file is not already in PGML format by looking for PGML.pl in the loadMacros statement, # and there are no BEGIN_TEXT, BEGIN_SOLUTION, etc. blocks. - return $pg_source if ($pg_source =~ /loadMacros\((.*)PGML\.pl(.*)\)/ && $pg_source !~ /BEGIN_(TEXT|HINT|SOLUTION)/); + return { pgmlCode => $pg_source } + if ($pg_source =~ /loadMacros\((.*)PGML\.pl(.*)\)/m && $pg_source !~ /BEGIN_(TEXT|HINT|SOLUTION)/); - # First get a list of all of the ANS, LABELED_ANS, etc. in the problem. + # Return an error if the loadMacros isn't in the form loadMacros( ... ); + return { errors => "The loadMacros command cannot be parsed.", pgmlCode => $pg_source } + unless $pg_source =~ /loadMacros\((.*?)\)\s*;/m; + + # Get a list of all of the ANS, LABELED_ANS, etc. in the problem. @ans_list = getANS($pg_source); my @pgml_block; @@ -108,16 +113,17 @@ sub convertToPGML { } elsif ($row =~ /loadMacros\(/) { # Parse the macros, which may be on multiple rows and may be in a qw block. my $macros = ''; - my $num_macro_lines = 0; # store the number of lines in the loadMacro so the output is similar to the input. - while (1) { + my $num_macro_lines = 1; # store the number of lines in the loadMacro so the output is similar to the input. + while ($row !~ /\)\s*;/) { # Remove comments within loadMacros block (should we keep them?) $row =~ s/#.*$//; $macros .= $row; - last if ($row =~ /(.*)\);/); ++$num_macro_lines; $row = shift @rows; } - my @macros = (); + $macros .= $row; + + my @macros; my ($qw_start, $qw_end); # the characters if the loadMacros has a qw block. # The following can parse loadMacros in the form loadMacros('macro1.pl', 'macro2.pl'); or @@ -143,7 +149,7 @@ sub convertToPGML { grep { $_ && $_ !~ - /(PGstandard|PGML|PGauxiliaryFunctions|PGbasicmacros|PGanswermacros|MathObjects|PGcourse|AnswerFormatHelp).pl/ + /(PGstandard|PGML|PGauxiliaryFunctions|PGbasicmacros|PGanswermacros|MathObjects|PGcourse|AnswerFormatHelp).pl/x } map {s/['"]//gr} @macros; @@ -151,7 +157,10 @@ sub convertToPGML { my %seen; @macros = grep { !$seen{$_}++ } @macros; } else { - warn 'The loadMacros statement in this file could not be processed.'; + return { + errors => 'The loadMacros command cannot be processed.', + pgmlCode => $pg_source + }; } @macros = ('PGstandard.pl', 'PGML.pl', @macros, 'PGcourse.pl'); @@ -162,10 +171,10 @@ sub convertToPGML { push(@all_lines, "\t$_") for (@macros); push(@all_lines, "$qw_end);"); } else { - push(@all_lines, "loadMacros(qw$qw_start" . join(' ', @macros) . "$qw_end);"); + push(@all_lines, "loadMacros(qw$qw_start" . join(' ', @macros) . "$qw_end);", ''); } } else { - push(@all_lines, 'loadMacros(' . join(', ', map {"'$_'"} @macros) . ');'); + push(@all_lines, 'loadMacros(' . join(', ', map {"'$_'"} @macros) . ');', ''); } } else { push(@all_lines, cleanUpCode($row)); @@ -180,7 +189,7 @@ sub convertToPGML { splice(@all_lines, $empty_lines[$n], 1); } } - return join "\n", @all_lines; + return { pgmlCode => join "\n", @all_lines }; } # This subroutine converts a block (passed in as an array ref of strings) to