Skip to content

Commit fc88a16

Browse files
committed
Changed the parsing of the load macros
Add better documentation of the convert-to-pgml script. Separate the loadMacros to a separate subroutine and perform error handling in the subroutine to better capture possible syntax errors. Also, add a better way to parse the answer blanks in PGML form.
1 parent e8114b7 commit fc88a16

2 files changed

Lines changed: 95 additions & 76 deletions

File tree

bin/convert-to-pgml.pl

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ =head1 SYNOPSIS
88
99
convert-to-pgml -b -s pgml file1.pg file2.pg ...
1010
11+
Options:
12+
13+
-b|--backup Create a backup of the original file before converting.
14+
-s|--suffix=s Suffix for the converted files. Default is 'pgml'.
15+
-v|--verbose Print verbose output.
16+
-h|--help Show the help message.
17+
18+
1119
=head1 DESCRIPTION
1220
1321
This converts each pg file to PGML formatting. In particular, text blocks are
@@ -20,14 +28,14 @@ =head1 DESCRIPTION
2028
to the form C<[_]{}>
2129
2230
Many code features that are no longer needed are removed including
23-
C<TEXT(beginproblem())>, C<<Context()->texStrings;>> and C<<Context()->normalStrings;>>.
31+
C<TEXT(beginproblem())>, C<< Context()->texStrings; >> and C<< Context()->normalStrings; >>.
2432
Any C<ANS> commands are commented out.
2533
2634
The C<loadMacros> command is parsed, the C<PGML.pl> is included and C<MathObjects.pl>
2735
is removed (because it is loaded by C<PGML.pl>) and C<PGcourse.pl> is added to the
2836
end of the list.
2937
30-
Note: many of the features are converted correctly, but often there will be errors
38+
Note: many of the features are converted correctly, but there may be errors
3139
after the conversion. Generally after using this script, the PGML style answers
3240
will need to have their corresponding variable added.
3341
@@ -42,28 +50,25 @@ =head2 OPTIONS
4250
4351
=cut
4452

45-
use strict;
46-
use warnings;
47-
use experimental 'signatures';
53+
use Mojo::Base -signatures;
4854

4955
use Mojo::File qw(curfile);
5056
use Getopt::Long;
57+
use Pod::Usage;
5158

5259
use lib curfile->dirname->dirname . '/lib';
5360

5461
use WeBWorK::PG::ConvertToPGML qw(convertToPGML);
5562

56-
my $backup = 0;
57-
my $verbose = 0;
58-
my $suffix = 'pgml';
59-
6063
GetOptions(
61-
"b|backup" => \$backup,
62-
"s|suffix=s" => \$suffix,
63-
"v|verbose" => \$verbose,
64+
'b|backup' => \my $backup,
65+
's|suffix=s' => \my $suffix,
66+
'v|verbose' => \my $verbose,
67+
'h|help' => \my $show_help
6468
);
69+
pod2usage(2) if $show_help || @ARGV == 0;
6570

66-
die 'arguments must have a list of pg files' unless @ARGV > 0;
71+
$suffix //= 'pgml';
6772
convertFile($_) for (grep { $_ =~ /\.pg$/ } @ARGV);
6873

6974
sub convertFile ($filename) {
@@ -73,11 +78,11 @@ ($filename)
7378
my $pg_source = $path->slurp;
7479
my $result = convertToPGML($pg_source);
7580
if (ref($result) eq 'HASH' && $result->{errors}) {
76-
warn "Error parsing $filename. " . $result->{errors};
81+
warn "Error parsing $filename. " . $result->{errors} . "\n";
7782
return;
7883
}
7984

80-
# copy the original file to a backup and then write the file
85+
# Copy the original file to a backup and then write the file.
8186
my $new_path = $backup ? $path : Mojo::File->new($filename =~ s/\.pg/.$suffix/r);
8287
my $backup_file = $filename =~ s/\.pg$/.pg.bak/r;
8388
$path->copy_to($backup_file) if $backup;

lib/WeBWorK/PG/ConvertToPGML.pm

Lines changed: 75 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ use parent qw(Exporter);
5757
use strict;
5858
use warnings;
5959

60-
our @EXPORT = qw(convertToPGML);
60+
our @EXPORT_OK = qw(convertToPGML);
6161

6262
=head2 convertToPGML
6363
@@ -76,16 +76,6 @@ my @ans_list;
7676
sub convertToPGML {
7777
my ($pg_source) = @_;
7878

79-
# Check that the file is not already in PGML format by looking for PGML.pl in the loadMacros statement,
80-
# and there are no BEGIN_TEXT, BEGIN_SOLUTION, etc. blocks.
81-
82-
return { pgmlCode => $pg_source }
83-
if ($pg_source =~ /loadMacros\((.*)PGML\.pl(.*)\)/m && $pg_source !~ /BEGIN_(TEXT|HINT|SOLUTION)/);
84-
85-
# Return an error if the loadMacros isn't in the form loadMacros( ... );
86-
return { errors => "The loadMacros command cannot be parsed.", pgmlCode => $pg_source }
87-
unless $pg_source =~ /loadMacros\((.*?)\)\s*;/m;
88-
8979
# Get a list of all of the ANS, LABELED_ANS, etc. in the problem.
9080
@ans_list = getANS($pg_source);
9181

@@ -123,58 +113,32 @@ sub convertToPGML {
123113
}
124114
$macros .= $row;
125115

126-
my @macros;
127-
my ($qw_start, $qw_end); # the characters if the loadMacros has a qw block.
128-
129-
# The following can parse loadMacros in the form loadMacros('macro1.pl', 'macro2.pl'); or
130-
# loadMacros(qw{macro1.pl macro2.pl});
131-
if ($macros =~ /loadMacros\((.*?)\);/ms) {
132-
my @macro_str = split(/\s*,\s*/, $1);
133-
134-
for my $str (@macro_str) {
135-
if ($str =~ /^qw(.)/) {
136-
my $qw_matches = { '{' => '}', '(' => ')', '[' => ']', '/' => '/', '|' => '|' };
137-
$qw_start = $1;
138-
$qw_end = $qw_matches->{$qw_start};
139-
140-
if ($str =~ /^qw\Q${qw_start}\E(.*?)\Q${qw_end}\E/) {
141-
push(@macros, split(/\s+/, $1));
142-
}
143-
} else {
144-
push(@macros, $str);
145-
}
146-
}
116+
my $load_macros_block = parseLoadMacros($macros);
147117

148-
@macros =
149-
grep {
150-
$_
151-
&& $_ !~
152-
/(PGstandard|PGML|PGauxiliaryFunctions|PGbasicmacros|PGanswermacros|MathObjects|PGcourse|AnswerFormatHelp).pl/x
153-
}
154-
map {s/['"]//gr} @macros;
155-
156-
# Remove any duplicates:
157-
my %seen;
158-
@macros = grep { !$seen{$_}++ } @macros;
159-
} else {
160-
return {
161-
errors => 'The loadMacros command cannot be processed.',
162-
pgmlCode => $pg_source
163-
};
164-
}
118+
# If PGML.pl is a macro and there are no BEGIN_TEXT/HINT/SOLUTION blocks
119+
# return the original source.
120+
return { pgmlCode => $pg_source }
121+
if (!defined($load_macros_block->{errors})
122+
&& grep { $_ eq 'PGML.pl' } @{ $load_macros_block->{macros} }
123+
&& $pg_source !~ /^\s*BEGIN_(TEXT|HINT|SOLUTION)/m);
165124

166-
@macros = ('PGstandard.pl', 'PGML.pl', @macros, 'PGcourse.pl');
125+
return { errors => $load_macros_block->{errors}, pgmlCode => $pg_source } if ($load_macros_block->{errors});
167126

168-
if ($qw_start) {
127+
if ($load_macros_block->{qw_start}) {
169128
if ($num_macro_lines > 1) { # put each macro on a separate line
170-
push(@all_lines, "loadMacros(qw$qw_start");
171-
push(@all_lines, "\t$_") for (@macros);
172-
push(@all_lines, "$qw_end);");
129+
push(@all_lines, 'loadMacros(qw' . $load_macros_block->{qw_start});
130+
push(@all_lines, "\t$_") for (@{ $load_macros_block->{macros} });
131+
push(@all_lines, $load_macros_block->{qw_end} . ');');
173132
} else {
174-
push(@all_lines, "loadMacros(qw$qw_start" . join(' ', @macros) . "$qw_end);", '');
133+
push(@all_lines,
134+
'loadMacros(qw'
135+
. $load_macros_block->{qw_start}
136+
. join(' ', @{ $load_macros_block->{macros} })
137+
. $load_macros_block->{qw_end} . ');',
138+
'');
175139
}
176140
} else {
177-
push(@all_lines, 'loadMacros(' . join(', ', map {"'$_'"} @macros) . ');', '');
141+
push(@all_lines, 'loadMacros(' . join(', ', map {"'$_'"} @{ $load_macros_block->{macros} }) . ');', '');
178142
}
179143
} else {
180144
push(@all_lines, cleanUpCode($row));
@@ -192,6 +156,56 @@ sub convertToPGML {
192156
return { pgmlCode => join "\n", @all_lines };
193157
}
194158

159+
sub parseLoadMacros {
160+
my ($macros) = @_;
161+
162+
my $error_string = 'The loadMacros statement could not be parsed. Check for syntax errors.';
163+
164+
return { errors => $error_string }
165+
if $macros =~ /loadMacros\(.*?\)(.*?);/s && $1 !~ /^\s*$/m;
166+
167+
my @macros;
168+
my ($qw_start, $qw_end); # the characters if the loadMacros has a qw block.
169+
my $qw_matches = { '{' => '}', '(' => ')', '[' => ']', '/' => '/', '|' => '|' };
170+
171+
# The following can parse loadMacros in the form loadMacros('macro1.pl', 'macro2.pl'); or
172+
# loadMacros(qw{macro1.pl macro2.pl});
173+
if ($macros =~ /loadMacros\((.*?)\);/ms) {
174+
my @macro_str = split(/\s*,\s*/, $1);
175+
176+
for my $str (@macro_str) {
177+
if ($str =~ /^qw(.)/) {
178+
$qw_start = $1;
179+
$qw_end = $qw_matches->{$qw_start};
180+
push(@macros, split(/\s+/, $1)) if $str =~ /^qw\Q${qw_start}\E(.*?)\Q${qw_end}\E/;
181+
} else {
182+
push(@macros, $str);
183+
}
184+
}
185+
186+
@macros =
187+
grep {
188+
$_
189+
&& $_ !~
190+
/(PGstandard|PGML|PGauxiliaryFunctions|PGbasicmacros|PGanswermacros|MathObjects|PGcourse|AnswerFormatHelp).pl/x
191+
}
192+
map {s/['"]//gr} @macros;
193+
194+
# Remove any duplicates:
195+
my %seen;
196+
@macros = grep { !$seen{$_}++ } @macros;
197+
} else {
198+
return { errors => $error_string };
199+
}
200+
201+
@macros = ('PGstandard.pl', 'PGML.pl', @macros, 'PGcourse.pl');
202+
return {
203+
qw_start => $qw_start,
204+
qw_end => $qw_end,
205+
macros => \@macros
206+
};
207+
}
208+
195209
# This subroutine converts a block (passed in as an array ref of strings) to
196210
# PGML format. This includes:
197211
# * converting BEGIN_TEXT/END_TEXT to BEGIN_PGML/END_PGML
@@ -266,12 +280,12 @@ sub convertPGMLBlock {
266280
}
267281

268282
# After many other variables have been replaced, replace the variables in the PGML block.
269-
# However if not in a {}, assumed to be in an answer blank.
283+
# If a variable is inside [_]{}, like '[_]{$a}', then leave it alone.
270284
if (my @matches = $row =~ /\$[\w\_]+/g) {
271-
for my $m (@matches) {
272-
$m =~ s/\$/\\\$/;
273-
# Wrap variables in []. Handle arrays, hashes, array refs and hashrefs.
274-
$row =~ s/(?<!\]{)($m+(\[\d+\])?((->)?\{.*?\})?)/[$1]/;
285+
my %seen;
286+
for my $m (grep { !$seen{$_}++ } @matches) {
287+
# $row =~ s/(?<!\{)(\Q$m\E)(?!\})/[$1]/g;
288+
$row =~ s/\[_+\]\{\Q$m\E\}(*SKIP)(*F)|(\Q$m\E)/[$1]/g;
275289
}
276290
}
277291

0 commit comments

Comments
 (0)