Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions bin/convert-to-pgml.pl
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Comment thread
pstaabp marked this conversation as resolved.
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;
}
Expand Down
94 changes: 73 additions & 21 deletions lib/WeBWorK/PG/ConvertToPGML.pm
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,17 @@ my @ans_list;
sub convertToPGML {
my ($pg_source) = @_;

# First get a list of all of the ANS, LABELED_ANS, etc. in the problem.
# 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 { pgmlCode => $pg_source }
if ($pg_source =~ /loadMacros\((.*)PGML\.pl(.*)\)/m && $pg_source !~ /BEGIN_(TEXT|HINT|SOLUTION)/);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This extremely naive regular expression check is just not going to work. First, since you do not include the s flag on the regular expression, the . character does not match newlines. So unless PGML.pl is literally on the same line as the loadMacros call, this check will not find it. So this will catch something like loadMacros( #PGML.pl) which it shouldn't, but it will not catch something like

loadMacros(qw{
    PGstandard.pl
    PGML.pl
});

Note that just adding the s flag won't fix this either because then it would still catch the false positive noted above, but would also catch something like

loadMacros('PGstandard.pl');
# (This does not use PGML.pl)

In addition, the second regular expression check will also catch many incorrect things.  For example, if someone mentions `BEGIN_TEXT` in a comment in the file, but does not use it.

@pstaabp pstaabp Jul 13, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had noticed this a few days ago too. I think changing to the /s on the RegEx and then

$pg_source =~ /loadMacros\((.*)PGML\.pl(.*)\)\s*;/s

parses your first example correctly, but not the 2nd one.

I do see that putting BEGIN_TEXT comment will catch it incorrectly. Would

$pg_source !~ /^\s*BEGIN_(TEXT|HINT|SOLUTION)/ms

be sufficient?

My hope was to catch most of the cases that have already been converted.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will need $pg_source !~ /^\s*BEGIN_(TEXT|HINT|SOLUTION)/m. Remove the s flag for this one. Then this will be pretty much what the translator uses. Note that the s flag makes . match a new line, so the ^ anchor would be nullified if the s flag is added.

The loadMacros check is more complicated. As I said, adding the s flag is not going to be enough. I am not sure what to do for this.


# 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;
Expand All @@ -101,29 +111,71 @@ 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 = '';
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;
++$num_macro_lines;
$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/
$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
# loadMacros(qw{macro1.pl macro2.pl});
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);
}
}
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'))
. ');');
@macros =
grep {
$_
&& $_ !~
/(PGstandard|PGML|PGauxiliaryFunctions|PGbasicmacros|PGanswermacros|MathObjects|PGcourse|AnswerFormatHelp).pl/x
}
map {s/['"]//gr} @macros;

# Remove any duplicates:
my %seen;
@macros = grep { !$seen{$_}++ } @macros;
} else {
return {
errors => 'The loadMacros command cannot be processed.',
pgmlCode => $pg_source
};
}

@macros = ('PGstandard.pl', 'PGML.pl', @macros, 'PGcourse.pl');

if ($qw_start) {
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) . ');', '');
}
} else {
push(@all_lines, cleanUpCode($row));
}
Expand All @@ -137,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
Expand Down