From 8c27517cb296479eb7e4c001c30adfd658e207ac Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Thu, 4 Sep 2025 05:06:05 -0500 Subject: [PATCH] Changes needed to accomodate changes to the PG POD and sample problem generation. First, there are some minor tweaks to the POD and sample problem viewers: * Fix the wording on the sample problem viewer index page for "Sample Problems by Macro". It current says, "For many macros, this lists all sample problems used by the macro." However, macros do not use sample problems. Sample problems use macros. So the wording is now, "For many macros, this lists all sample problems that use the macro." * Update the version of bootstrap in the POD templates to 5.3.7 which is currently the latest. * Update the links in the pod templates to wiki.openwebwork.org. * Remove the `favicon` links. Those are not explicitly needed. Browser's now load the favicon even if the page does not contain the link. The link only needs to be added if the file is not named favicon.ico. The following are the actual changes needed to accomodate changes to the PG POD and sample problem generation: * Remove files that are now used from the PG repository. * Remove the PODViewer js and css files, the PODParser.pm and PODtoHTML.pm modules, and category-index.mt and pod.mt templates, all of which are now provided by PG. * Make the necessary changes to the generate-ww-pg-pod.pl script so that it can use those files from the PG repository. * Update the SampleProblemParser module usage for its changes and namespace change. * The `WeBWorK::PG::SampleProblemParser` module now handles generation of the actual search data with its `getSearchData` method. So the `WeBWorK::ContentGenerator::SampleProblemViewer` just calls that method. * The `assets/stop-words-en.txt` file is now in the PG repository so that the `WeBWorK::PG::SampleProblemParser::getSearchData` method can get at it easily. * The `htdocs/js/SampleProblemViewer/documentation-search.js` file is now in the PG repository, and webwork2 uses it from there. PG also copies it to the PG docs github pages site in its new workflow. The options are passed in via the script in `templates/ContentGenerator/SampleProblemViewer.html.ep`. --- assets/stop-words-en.txt | 1320 ----------------- bin/dev_scripts/PODtoHTML.pm | 201 --- bin/dev_scripts/generate-ww-pg-pod.pl | 68 +- .../pod-templates/category-index.mt | 95 -- bin/dev_scripts/pod-templates/main-index.mt | 3 +- bin/dev_scripts/pod-templates/pod.mt | 55 - htdocs/js/PODViewer/podviewer.css | 66 - htdocs/js/PODViewer/podviewer.js | 8 - .../documentation-search.js | 76 - .../Instructor/PGProblemEditor.pm | 12 +- .../ContentGenerator/SampleProblemViewer.pm | 206 +-- lib/WeBWorK/Utils/PODParser.pm | 66 - .../SampleProblemViewer.html.ep | 13 +- 13 files changed, 65 insertions(+), 2124 deletions(-) delete mode 100644 assets/stop-words-en.txt delete mode 100644 bin/dev_scripts/PODtoHTML.pm delete mode 100644 bin/dev_scripts/pod-templates/category-index.mt delete mode 100644 bin/dev_scripts/pod-templates/pod.mt delete mode 100644 htdocs/js/PODViewer/podviewer.css delete mode 100644 htdocs/js/PODViewer/podviewer.js delete mode 100644 htdocs/js/SampleProblemViewer/documentation-search.js delete mode 100644 lib/WeBWorK/Utils/PODParser.pm diff --git a/assets/stop-words-en.txt b/assets/stop-words-en.txt deleted file mode 100644 index cc09be2ec7..0000000000 --- a/assets/stop-words-en.txt +++ /dev/null @@ -1,1320 +0,0 @@ -# Stop words from https://github.com/Alir3z4/stop-words. - -'ll -'tis -'twas -'ve -a -a's -able -ableabout -about -above -abroad -abst -accordance -according -accordingly -across -act -actually -ad -added -adj -adopted -ae -af -affected -affecting -affects -after -afterwards -ag -again -against -ago -ah -ahead -ai -ain't -aint -al -all -allow -allows -almost -alone -along -alongside -already -also -although -always -am -amid -amidst -among -amongst -amoungst -amount -an -and -announce -another -any -anybody -anyhow -anymore -anyone -anything -anyway -anyways -anywhere -ao -apart -apparently -appear -appreciate -appropriate -approximately -aq -ar -are -area -areas -aren -aren't -arent -arise -around -arpa -as -aside -ask -asked -asking -asks -associated -at -au -auth -available -aw -away -awfully -az -b -ba -back -backed -backing -backs -backward -backwards -bb -bd -be -became -because -become -becomes -becoming -been -before -beforehand -began -begin -beginning -beginnings -begins -behind -being -beings -believe -below -beside -besides -best -better -between -beyond -bf -bg -bh -bi -big -bill -billion -biol -bj -bm -bn -bo -both -bottom -br -brief -briefly -bs -bt -but -buy -bv -bw -by -bz -c -c'mon -c's -ca -call -came -can -can't -cannot -cant -caption -case -cases -cause -causes -cc -cd -certain -certainly -cf -cg -ch -changes -ci -ck -cl -clear -clearly -click -cm -cmon -cn -co -co. -com -come -comes -computer -con -concerning -consequently -consider -considering -contain -containing -contains -copy -corresponding -could -could've -couldn -couldn't -couldnt -course -cr -cry -cs -cu -currently -cv -cx -cy -cz -d -dare -daren't -darent -date -de -dear -definitely -describe -described -despite -detail -did -didn -didn't -didnt -differ -different -differently -directly -dj -dk -dm -do -does -doesn -doesn't -doesnt -doing -don -don't -done -dont -doubtful -down -downed -downing -downs -downwards -due -during -dz -e -each -early -ec -ed -edu -ee -effect -eg -eh -eight -eighty -either -eleven -else -elsewhere -empty -end -ended -ending -ends -enough -entirely -er -es -especially -et -et-al -etc -even -evenly -ever -evermore -every -everybody -everyone -everything -everywhere -ex -exactly -example -except -f -face -faces -fact -facts -fairly -far -farther -felt -few -fewer -ff -fi -fifteen -fifth -fifty -fify -fill -find -finds -fire -first -five -fix -fj -fk -fm -fo -followed -following -follows -for -forever -former -formerly -forth -forty -forward -found -four -fr -free -from -front -full -fully -further -furthered -furthering -furthermore -furthers -fx -g -ga -gave -gb -gd -ge -general -generally -get -gets -getting -gf -gg -gh -gi -give -given -gives -giving -gl -gm -gmt -gn -go -goes -going -gone -good -goods -got -gotten -gov -gp -gq -gr -great -greater -greatest -greetings -group -grouped -grouping -groups -gs -gt -gu -gw -gy -h -had -hadn't -hadnt -half -happens -hardly -has -hasn -hasn't -hasnt -have -haven -haven't -havent -having -he -he'd -he'll -he's -hed -hell -hello -help -hence -her -here -here's -hereafter -hereby -herein -heres -hereupon -hers -herself -herse” -hes -hi -hid -high -higher -highest -him -himself -himse” -his -hither -hk -hm -hn -home -homepage -hopefully -how -how'd -how'll -how's -howbeit -however -hr -ht -htm -html -http -hu -hundred -i -i'd -i'll -i'm -i've -i.e. -id -ie -if -ignored -ii -il -ill -im -immediate -immediately -importance -important -in -inasmuch -inc -inc. -indeed -index -indicate -indicated -indicates -information -inner -inside -insofar -instead -int -interest -interested -interesting -interests -into -invention -inward -io -iq -ir -is -isn -isn't -isnt -it -it'd -it'll -it's -itd -itll -its -itself -itse” -ive -j -je -jm -jo -join -jp -just -k -ke -keep -keeps -kept -keys -kg -kh -ki -kind -km -kn -knew -know -known -knows -kp -kr -kw -ky -kz -l -la -large -largely -last -lately -later -latest -latter -latterly -lb -lc -least -length -less -lest -let -let's -lets -li -like -liked -likely -likewise -line -little -lk -ll -long -longer -longest -look -looking -looks -low -lower -lr -ls -lt -ltd -lu -lv -ly -m -ma -made -mainly -make -makes -making -man -many -may -maybe -mayn't -maynt -mc -md -me -mean -means -meantime -meanwhile -member -members -men -merely -mg -mh -microsoft -might -might've -mightn't -mightnt -mil -mill -million -mine -minus -miss -mk -ml -mm -mn -mo -more -moreover -most -mostly -move -mp -mq -mr -mrs -ms -msie -mt -mu -much -mug -must -must've -mustn't -mustnt -mv -mw -mx -my -myself -myse” -mz -n -na -name -namely -nay -nc -nd -ne -near -nearly -necessarily -necessary -need -needed -needing -needn't -neednt -needs -neither -net -netscape -never -neverf -neverless -nevertheless -new -newer -newest -next -nf -ng -ni -nine -ninety -nl -no -no-one -nobody -non -none -nonetheless -noone -nor -normally -nos -not -noted -nothing -notwithstanding -novel -now -nowhere -np -nr -nu -null -number -numbers -nz -o -obtain -obtained -obviously -of -off -often -oh -ok -okay -old -older -oldest -om -omitted -on -once -one -one's -ones -only -onto -open -opened -opening -opens -opposite -or -ord -order -ordered -ordering -orders -org -other -others -otherwise -ought -oughtn't -oughtnt -our -ours -ourselves -out -outside -over -overall -owing -own -p -pa -page -pages -part -parted -particular -particularly -parting -parts -past -pe -per -perhaps -pf -pg -ph -pk -pl -place -placed -places -please -plus -pm -pmid -pn -point -pointed -pointing -points -poorly -possible -possibly -potentially -pp -pr -predominantly -present -presented -presenting -presents -presumably -previously -primarily -probably -problem -problems -promptly -proud -provided -provides -pt -put -puts -pw -py -q -qa -que -quickly -quite -qv -r -ran -rather -rd -re -readily -really -reasonably -recent -recently -ref -refs -regarding -regardless -regards -related -relatively -research -reserved -respectively -resulted -resulting -results -right -ring -ro -room -rooms -round -ru -run -rw -s -sa -said -same -saw -say -saying -says -sb -sc -sd -se -sec -second -secondly -seconds -section -see -seeing -seem -seemed -seeming -seems -seen -sees -self -selves -sensible -sent -serious -seriously -seven -seventy -several -sg -sh -shall -shan't -shant -she -she'd -she'll -she's -shed -shell -shes -should -should've -shouldn -shouldn't -shouldnt -show -showed -showing -shown -showns -shows -si -side -sides -significant -significantly -similar -similarly -since -sincere -site -six -sixty -sj -sk -sl -slightly -sm -small -smaller -smallest -sn -so -some -somebody -someday -somehow -someone -somethan -something -sometime -sometimes -somewhat -somewhere -soon -sorry -specifically -specified -specify -specifying -sr -st -state -states -still -stop -strongly -su -sub -substantially -successfully -such -sufficiently -suggest -sup -sure -sv -sy -system -sz -t -t's -take -taken -taking -tc -td -tell -ten -tends -test -text -tf -tg -th -than -thank -thanks -thanx -that -that'll -that's -that've -thatll -thats -thatve -the -their -theirs -them -themselves -then -thence -there -there'd -there'll -there're -there's -there've -thereafter -thereby -thered -therefore -therein -therell -thereof -therere -theres -thereto -thereupon -thereve -these -they -they'd -they'll -they're -they've -theyd -theyll -theyre -theyve -thick -thin -thing -things -think -thinks -third -thirty -this -thorough -thoroughly -those -thou -though -thoughh -thought -thoughts -thousand -three -throug -through -throughout -thru -thus -til -till -tip -tis -tj -tk -tm -tn -to -today -together -too -took -top -toward -towards -tp -tr -tried -tries -trillion -truly -try -trying -ts -tt -turn -turned -turning -turns -tv -tw -twas -twelve -twenty -twice -two -tz -u -ua -ug -uk -um -un -under -underneath -undoing -unfortunately -unless -unlike -unlikely -until -unto -up -upon -ups -upwards -us -use -used -useful -usefully -usefulness -uses -using -usually -uucp -uy -uz -v -va -value -various -vc -ve -versus -very -vg -vi -via -viz -vn -vol -vols -vs -vu -w -want -wanted -wanting -wants -was -wasn -wasn't -wasnt -way -ways -we -we'd -we'll -we're -we've -web -webpage -website -wed -welcome -well -wells -went -were -weren -weren't -werent -weve -wf -what -what'd -what'll -what's -what've -whatever -whatll -whats -whatve -when -when'd -when'll -when's -whence -whenever -where -where'd -where'll -where's -whereafter -whereas -whereby -wherein -wheres -whereupon -wherever -whether -which -whichever -while -whilst -whim -whither -who -who'd -who'll -who's -whod -whoever -whole -wholl -whom -whomever -whos -whose -why -why'd -why'll -why's -widely -width -will -willing -wish -with -within -without -won -won't -wonder -wont -words -work -worked -working -works -world -would -would've -wouldn -wouldn't -wouldnt -ws -www -x -y -ye -year -years -yes -yet -you -you'd -you'll -you're -you've -youd -youll -young -younger -youngest -your -youre -yours -yourself -yourselves -youve -yt -yu -z -za -zero -zm -zr - -# Additional specific stop words specific to POD and sample problem documentation. -constructor -description -error -errors -macro -macros -pod -podlink -problink -synopsis -usage -funciton -functions -method -methods -option -options -todo -fixme -_ diff --git a/bin/dev_scripts/PODtoHTML.pm b/bin/dev_scripts/PODtoHTML.pm deleted file mode 100644 index a922314011..0000000000 --- a/bin/dev_scripts/PODtoHTML.pm +++ /dev/null @@ -1,201 +0,0 @@ -package PODtoHTML; - -use strict; -use warnings; -use utf8; - -use Pod::Simple::Search; -use Mojo::Template; -use Mojo::DOM; -use Mojo::Collection qw(c); -use File::Path qw(make_path); -use File::Basename qw(dirname); -use IO::File; -use POSIX qw(strftime); - -use WeBWorK::Utils::PODParser; - -our @sections = ( - doc => 'Documentation', - bin => 'Scripts', - macros => 'Macros', - lib => 'Libraries', -); -our %macro_names = ( - answers => 'Answers', - contexts => 'Contexts', - core => 'Core', - deprecated => 'Deprecated', - graph => 'Graph', - math => 'Math', - misc => 'Miscellaneous', - parsers => 'Parsers', - ui => 'User Interface' -); - -sub new { - my ($invocant, %o) = @_; - my $class = ref $invocant || $invocant; - - my @section_list = ref($o{sections}) eq 'ARRAY' ? @{ $o{sections} } : @sections; - my $section_hash = {@section_list}; - my $section_order = [ map { $section_list[ 2 * $_ ] } 0 .. $#section_list / 2 ]; - delete $o{sections}; - - my $self = { - %o, - idx => {}, - section_hash => $section_hash, - section_order => $section_order, - macros_hash => {}, - }; - return bless $self, $class; -} - -sub convert_pods { - my $self = shift; - my $source_root = $self->{source_root}; - my $dest_root = $self->{dest_root}; - - my $regex = join('|', map {"^$_"} @{ $self->{section_order} }); - - my ($name2path, $path2name) = Pod::Simple::Search->new->inc(0)->limit_re(qr!$regex!)->survey($self->{source_root}); - for (keys %$path2name) { - print "Processing file: $_\n" if $self->{verbose} > 1; - $self->process_pod($_, $name2path); - } - - $self->write_index("$dest_root/index.html"); - - return; -} - -sub process_pod { - my ($self, $pod_path, $pod_files) = @_; - - my $pod_name; - - my ($subdir, $filename) = $pod_path =~ m|^$self->{source_root}/(?:(.*)/)?(.*)$|; - - my ($subdir_first, $subdir_rest) = ('', ''); - - if (defined $subdir) { - if ($subdir =~ m|/|) { - ($subdir_first, $subdir_rest) = $subdir =~ m|^([^/]*)/(.*)|; - } else { - $subdir_first = $subdir; - } - } - - $pod_name = (defined $subdir_rest ? "$subdir_rest/" : '') . $filename; - if ($filename =~ /\.pl$/) { - $filename =~ s/\.pl$/.html/; - } elsif ($filename =~ /\.pod$/) { - $pod_name =~ s/\.pod$//; - $filename =~ s/\.pod$/.html/; - } elsif ($filename =~ /\.pm$/) { - $pod_name =~ s/\.pm$//; - $pod_name =~ s|/+|::|g; - $filename =~ s/\.pm$/.html/; - } elsif ($filename !~ /\.html$/) { - $filename .= '.html'; - } - - $pod_name =~ s/^(\/|::)//; - - my $html_dir = $self->{dest_root} . (defined $subdir ? "/$subdir" : ''); - my $html_path = "$html_dir/$filename"; - my $html_rel_path = defined $subdir ? "$subdir/$filename" : $filename; - - $self->update_index($subdir, $html_rel_path, $pod_name); - make_path($html_dir); - my $html = $self->do_pod2html( - pod_path => $pod_path, - pod_name => $pod_name, - pod_files => $pod_files - ); - my $fh = IO::File->new($html_path, '>:encoding(UTF-8)') - or die "Failed to open file '$html_path' for writing: $!\n"; - print $fh $html; - - return; -} - -sub update_index { - my ($self, $subdir, $html_rel_path, $pod_name) = @_; - - $subdir =~ s|/.*$||; - my $idx = $self->{idx}; - my $sections = $self->{section_hash}; - if ($subdir eq 'macros') { - $idx->{macros} = []; - if ($pod_name =~ m!^(.+)/(.+)$!) { - push @{ $self->{macros_hash}{$1} }, [ $html_rel_path, $2 ]; - } else { - push @{ $idx->{doc} }, [ $html_rel_path, $pod_name ]; - } - } elsif (exists $sections->{$subdir}) { - push @{ $idx->{$subdir} }, [ $html_rel_path, $pod_name ]; - } else { - warn "no section for subdir '$subdir'\n"; - } - - return; -} - -sub write_index { - my ($self, $out_path) = @_; - - my $fh = IO::File->new($out_path, '>:encoding(UTF-8)') or die "Failed to open index '$out_path' for writing: $!\n"; - print $fh Mojo::Template->new(vars => 1)->render_file( - "$self->{template_dir}/category-index.mt", - { - title => 'POD for ' . ($self->{source_root} =~ s|^.*/||r), - base_url => $self->{dest_url}, - pod_index => $self->{idx}, - sections => $self->{section_hash}, - section_order => $self->{section_order}, - macros => $self->{macros_hash}, - macros_order => [ sort keys %{ $self->{macros_hash} } ], - macro_names => \%macro_names, - date => strftime('%a %b %e %H:%M:%S %Z %Y', localtime) - } - ); - - return; -} - -sub do_pod2html { - my ($self, %o) = @_; - - my $psx = WeBWorK::Utils::PODParser->new($o{pod_files}); - $psx->{source_root} = $self->{source_root}; - $psx->{verbose} = $self->{verbose}; - $psx->{assert_html_ext} = 1; - $psx->{base_url} = ($self->{dest_url} // '') . '/' . (($self->{source_root} // '') =~ s|^.*/||r); - $psx->output_string(\my $html); - $psx->html_header(''); - $psx->html_footer(''); - $psx->parse_file($o{pod_path}); - - my $dom = Mojo::DOM->new($html); - my $podIndexUL = $dom->at('ul[id="index"]'); - my $podIndex = $podIndexUL ? $podIndexUL->find('ul[id="index"] > li') : c(); - for (@$podIndex) { - $_->attr({ class => 'nav-item' }); - $_->at('a')->attr({ class => 'nav-link p-0' }); - for (@{ $_->find('ul') }) { - $_->attr({ class => 'nav flex-column w-100' }); - } - for (@{ $_->find('li') }) { - $_->attr({ class => 'nav-item' }); - $_->at('a')->attr({ class => 'nav-link p-0' }); - } - } - my $podHTML = $podIndexUL ? $podIndexUL->remove : $html; - - return Mojo::Template->new(vars => 1)->render_file("$self->{template_dir}/pod.mt", - { title => $o{pod_name}, base_url => dirname($psx->{base_url}), index => $podIndex, content => $podHTML }); -} - -1; diff --git a/bin/dev_scripts/generate-ww-pg-pod.pl b/bin/dev_scripts/generate-ww-pg-pod.pl index f1591ae485..d20902a171 100755 --- a/bin/dev_scripts/generate-ww-pg-pod.pl +++ b/bin/dev_scripts/generate-ww-pg-pod.pl @@ -9,17 +9,16 @@ =head1 SYNOPSIS generate-ww-pg-pod.pl [options] Options: - -p|--pg-root Directory containing a git clone of pg. - If this option is not set, then the environment - variable $PG_ROOT will be used if it is set. -o|--output-dir Directory to save the output files to. (required) -b|--base-url Base url location used on server. (default: /) This is needed for internal POD links to work correctly. -v|--verbose Increase the verbosity of the output. (Use multiple times for more verbosity.) -Note that --pg-root must be provided or the PG_ROOT environment variable set -if the POD for pg is desired. +Note that C must be set in the C file, or +if that file does not exist then the clone of the PG repository must be located +at C as defined in the C +file. =head1 DESCRIPTION @@ -30,37 +29,47 @@ =head1 DESCRIPTION use strict; use warnings; +my ($webwork_root, $pg_root); + +BEGIN { + use File::Basename qw(dirname); + use Cwd qw(abs_path); + use YAML::XS qw(LoadFile); + + $webwork_root = abs_path(dirname(dirname(dirname(__FILE__)))); + + # Load the configuration file to obtain the PG root directory. + my $config_file = "$webwork_root/conf/webwork2.mojolicious.yml"; + $config_file = "$webwork_root/conf/webwork2.mojolicious.dist.yml" unless -e $config_file; + my $config = LoadFile($config_file); + + $pg_root = $config->{pg_dir}; +} + use Getopt::Long qw(:config bundling); use Pod::Usage; -my ($pg_root, $output_dir, $base_url); +my ($output_dir, $base_url); my $verbose = 0; GetOptions( - 'p|pg-root=s' => \$pg_root, 'o|output-dir=s' => \$output_dir, 'b|base-url=s' => \$base_url, 'v|verbose+' => \$verbose ); -$pg_root = $ENV{PG_ROOT} if !$pg_root; - -pod2usage(2) unless $output_dir; +pod2usage(2) unless $output_dir && $pg_root && -d $pg_root; $base_url = "/" if !$base_url; use Mojo::Template; use IO::File; use File::Copy; -use File::Path qw(make_path remove_tree); -use File::Basename qw(dirname); -use Cwd qw(abs_path); - -use lib dirname(dirname(dirname(__FILE__))) . '/lib'; -use lib dirname(__FILE__); +use File::Path qw(make_path remove_tree); -use PODtoHTML; +use lib "$webwork_root/lib"; +use lib "$pg_root/lib"; -my $webwork_root = abs_path(dirname(dirname(dirname(__FILE__)))); +use WeBWorK::Utils::PODtoHTML; for my $dir ($webwork_root, $pg_root) { next unless $dir && -d $dir; @@ -73,10 +82,10 @@ =head1 DESCRIPTION write_index($index_fh); make_path("$output_dir/assets"); -copy("$webwork_root/htdocs/js/PODViewer/podviewer.css", "$output_dir/assets/podviewer.css"); -print "copying $webwork_root/htdocs/js/PODViewer/podviewer.css to $output_dir/assets/podviewer.css\n" if $verbose; -copy("$webwork_root/htdocs/js/PODViewer/podviewer.js", "$output_dir/assets/podviewer.js"); -print "copying $webwork_root/htdocs/js/PODViewer/podviewer.css to $output_dir/assets/podviewer.js\n" if $verbose; +copy("$pg_root/htdocs/js/PODViewer/podviewer.css", "$output_dir/assets/podviewer.css"); +print "copying $pg_root/htdocs/js/PODViewer/podviewer.css to $output_dir/assets/podviewer.css\n" if $verbose; +copy("$pg_root/htdocs/js/PODViewer/podviewer.js", "$output_dir/assets/podviewer.js"); +print "copying $pg_root/htdocs/js/PODViewer/podviewer.css to $output_dir/assets/podviewer.js\n" if $verbose; sub process_dir { my $source_dir = shift; @@ -89,12 +98,15 @@ sub process_dir { remove_tree($dest_dir); make_path($dest_dir); - my $htmldocs = PODtoHTML->new( - source_root => $source_dir, - dest_root => $dest_dir, - template_dir => "$webwork_root/bin/dev_scripts/pod-templates", - dest_url => $base_url, - verbose => $verbose + my $htmldocs = WeBWorK::Utils::PODtoHTML->new( + source_root => $source_dir, + dest_root => $dest_dir, + template_dir => "$pg_root/assets/pod-templates", + dest_url => $base_url, + home_url => $base_url, + home_url_link_name => 'WeBWorK POD Home', + page_url => $base_url . ($source_dir =~ s|^.*/||r), + verbose => $verbose ); $htmldocs->convert_pods; diff --git a/bin/dev_scripts/pod-templates/category-index.mt b/bin/dev_scripts/pod-templates/category-index.mt deleted file mode 100644 index a5d980d599..0000000000 --- a/bin/dev_scripts/pod-templates/category-index.mt +++ /dev/null @@ -1,95 +0,0 @@ - - -% - - - - <%= $title %> - - - - - - -% - - - % - % my ($index, $macro_index, $content, $macro_content) = ('', '', '', ''); - % for my $macro (@$macros_order) { - % my $new_index = begin - <%= $macro_names->{$macro} // $macro %> - % end - % $macro_index .= $new_index->(); - % my $new_content = begin -

<%= $macro_names->{$macro} // $macro %>

-
- % for my $file (sort { $a->[1] cmp $b->[1] } @{ $macros->{$macro} }) { - <%= $file->[1] %> - % } -
- % end - % $macro_content .= $new_content->(); - % } - % for my $section (@$section_order) { - % next unless defined $pod_index->{$section}; - % my $new_index = begin - <%= $sections->{$section} %> - % if ($section eq 'macros') { - - % } - % end - % $index .= $new_index->(); - % my $new_content = begin -

<%= $sections->{$section} %>

-
- % if ($section eq 'macros') { - <%= $macro_content =%> - % } else { - % for my $file (sort { $a->[1] cmp $b->[1] } @{ $pod_index->{$section} }) { - - <%= $file->[1] %> - - % } - % } -
- % end - % $content .= $new_content->(); - % } - % - -
-
- <%= $content =%> -

Generated <%= $date %>

-
-
- -% - diff --git a/bin/dev_scripts/pod-templates/main-index.mt b/bin/dev_scripts/pod-templates/main-index.mt index d9bfe39e8d..9c8c711588 100644 --- a/bin/dev_scripts/pod-templates/main-index.mt +++ b/bin/dev_scripts/pod-templates/main-index.mt @@ -4,8 +4,7 @@ - - + WeBWorK/PG POD diff --git a/bin/dev_scripts/pod-templates/pod.mt b/bin/dev_scripts/pod-templates/pod.mt deleted file mode 100644 index 0ea8e6c5b4..0000000000 --- a/bin/dev_scripts/pod-templates/pod.mt +++ /dev/null @@ -1,55 +0,0 @@ - - -% - - - - <%= $title %> - - - - - - -% - - - -
-
- <%= $content =%> -
-
- -% - diff --git a/htdocs/js/PODViewer/podviewer.css b/htdocs/js/PODViewer/podviewer.css deleted file mode 100644 index e4f17811d2..0000000000 --- a/htdocs/js/PODViewer/podviewer.css +++ /dev/null @@ -1,66 +0,0 @@ -.main-index-header, -.pod-header { - height: 65px; - top: 0; - left: 0; - right: 0; - z-index: 2; -} - -#sidebar { - --bs-offcanvas-width: 300px; - overflow-y: auto; -} - -#sidebar ul.nav ul.nav li { - border-left: 1px solid #e1e4e8; - padding-left: 10px; -} - -#sidebar ul.nav ul.nav li:hover { - border-left: 6px solid #e1e4e8; - padding-left: 5px; -} - -.main-index-container, -.pod-page-container { - margin-top: 65px; -} - -@media only screen and (min-width: 768px) { - #sidebar { - height: calc(100vh - 65px); - width: 300px; - } - - .pod-page-container { - margin-left: 300px; - } -} - -#_podtop_ pre { - border: 1px solid #ccc; - border-radius: 5px; - background: #f6f6f6; - padding: 0.75rem; -} - -#_podtop_, -#_podtop_ *[id] { - scroll-margin-top: calc(65px + 1rem); -} - -@media only screen and (max-width: 768px) { - .pod-header { - height: 100px; - } - - .pod-page-container { - margin-top: 100px; - } - - #_podtop_, - #_podtop_ *[id] { - scroll-margin-top: calc(100px + 1rem); - } -} diff --git a/htdocs/js/PODViewer/podviewer.js b/htdocs/js/PODViewer/podviewer.js deleted file mode 100644 index 795093205a..0000000000 --- a/htdocs/js/PODViewer/podviewer.js +++ /dev/null @@ -1,8 +0,0 @@ -(() => { - const offcanvas = bootstrap.Offcanvas.getOrCreateInstance(document.getElementById('sidebar')); - for (const link of document.querySelectorAll('#sidebar .nav-link')) { - // The timeout is to workaround an issue in Chrome. If the offcanvas hides before the window scrolls to the - // fragment in the page, scrolling stops before it gets there. - link.addEventListener('click', () => setTimeout(() => offcanvas.hide(), 500)); - } -})(); diff --git a/htdocs/js/SampleProblemViewer/documentation-search.js b/htdocs/js/SampleProblemViewer/documentation-search.js deleted file mode 100644 index 5e61bc64ec..0000000000 --- a/htdocs/js/SampleProblemViewer/documentation-search.js +++ /dev/null @@ -1,76 +0,0 @@ -(async () => { - const searchBox = document.getElementById('search-box'); - const resultList = document.getElementById('result-list'); - if (!resultList || !searchBox) return; - - const webwork2URL = webworkConfig?.webwork_url ?? '/webwork2'; - - let searchData; - try { - const result = await fetch(`${webwork2URL}/sampleproblems/search_data`); - searchData = await result.json(); - } catch (e) { - console.log(e); - return; - } - - const miniSearch = new MiniSearch({ - fields: ['filename', 'name', 'description', 'terms', 'macros', 'subjects'], - storeFields: ['type', 'filename', 'dir', 'description'] - }); - miniSearch.addAll(searchData); - - const searchMacrosCheck = document.getElementById('search-macros'); - const searchSampleProblemsCheck = document.getElementById('search-sample-problems'); - - document.getElementById('clear-search-button')?.addEventListener('click', () => { - searchBox.value = ''; - while (resultList.firstChild) resultList.firstChild.remove(); - }); - - const searchDocumentation = () => { - const searchMacros = searchMacrosCheck?.checked; - const searchSampleProblems = searchSampleProblemsCheck?.checked; - - while (resultList.firstChild) resultList.firstChild.remove(); - - if (!searchBox.value) return; - - for (const result of miniSearch.search(searchBox.value, { prefix: true })) { - if ( - (searchSampleProblems && result.type === 'sample problem') || - (searchMacros && result.type === 'macro') - ) { - const link = document.createElement('a'); - link.classList.add('list-group-item', 'list-group-item-action'); - link.href = `${webwork2URL}/${ - result.type === 'sample problem' ? 'sampleproblems' : result.type === 'macro' ? 'pod' : '' - }/${result.dir}/${result.filename.replace('.pg', '')}`; - - const linkText = document.createElement('span'); - linkText.classList.add('h4'); - linkText.textContent = `${result.filename} (${result.type})`; - link.append(linkText); - - if (result.description) { - const summary = document.createElement('div'); - summary.textContent = result.description; - link.append(summary); - } - - resultList.append(link); - } - } - - if (resultList.children.length == 0) { - const item = document.createElement('div'); - item.classList.add('alert', 'alert-info'); - item.innerHTML = 'No results found'; - resultList.append(item); - } - }; - - searchBox.addEventListener('keyup', searchDocumentation); - searchMacrosCheck?.addEventListener('change', searchDocumentation); - searchSampleProblemsCheck?.addEventListener('change', searchDocumentation); -})(); diff --git a/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm b/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm index 0703a78623..76e5b3118e 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/PGProblemEditor.pm @@ -108,12 +108,12 @@ not exist. The path to the actual file being edited is stored in inputFilePath. use Mojo::File; use XML::LibXML; -use WeBWorK::Utils qw(not_blank x max); -use WeBWorK::Utils::Files qw(surePathToFile readFile path_is_subdir); -use WeBWorK::Utils::Instructor qw(assignProblemToAllSetUsers addProblemToSet); -use WeBWorK::Utils::JITAR qw(seq_to_jitar_id jitar_id_to_seq); -use WeBWorK::Utils::Sets qw(format_set_name_display); -use SampleProblemParser qw(getSampleProblemCode generateMetadata); +use WeBWorK::Utils qw(not_blank x max); +use WeBWorK::Utils::Files qw(surePathToFile readFile path_is_subdir); +use WeBWorK::Utils::Instructor qw(assignProblemToAllSetUsers addProblemToSet); +use WeBWorK::Utils::JITAR qw(seq_to_jitar_id jitar_id_to_seq); +use WeBWorK::Utils::Sets qw(format_set_name_display); +use WeBWorK::PG::SampleProblemParser qw(getSampleProblemCode generateMetadata); use constant DEFAULT_SEED => 123456; diff --git a/lib/WeBWorK/ContentGenerator/SampleProblemViewer.pm b/lib/WeBWorK/ContentGenerator/SampleProblemViewer.pm index 3ee248c2b0..d4d4ef44a0 100644 --- a/lib/WeBWorK/ContentGenerator/SampleProblemViewer.pm +++ b/lib/WeBWorK/ContentGenerator/SampleProblemViewer.pm @@ -2,14 +2,10 @@ package WeBWorK::ContentGenerator::SampleProblemViewer; use Mojo::Base 'WeBWorK::ContentGenerator', -signatures; use File::Basename qw(basename); -use Mojo::File; -use Mojo::JSON qw(decode_json encode_json); -use File::Find; use Pod::Simple::Search; -use Pod::Simple::SimpleTree; -use WeBWorK::Utils::Files qw(path_is_subdir); -use SampleProblemParser qw(parseSampleProblem generateMetadata getSampleProblemCode); +use WeBWorK::Utils::Files qw(path_is_subdir); +use WeBWorK::PG::SampleProblemParser qw(parseSampleProblem generateMetadata getSampleProblemCode getSearchData); =head1 NAME @@ -92,10 +88,10 @@ sub renderSampleProblem ($c) { %{ parseSampleProblem( $problemFile, - metadata => $metadata, - pod_root => $c->url_for('pod_viewer', filePath => 'macros'), - pg_doc_home => $c->url_for('sample_problem_index'), - macro_locations => \%macro_locations, + metadata => $metadata, + pod_base_url => $c->url_for('pod_viewer', filePath => 'macros'), + sample_problem_base_url => $c->url_for('sample_problem_index'), + macro_locations => \%macro_locations, ) }, metadata => $metadata, @@ -106,195 +102,7 @@ sub renderSampleProblem ($c) { } sub searchData ($c) { - my $sampleProblemDir = $c->ce->{pg_dir} . '/tutorial/sample-problems'; - - my $searchDataFile = Mojo::File->new($c->ce->{webworkDirs}{DATA})->child('sample-problem-search-data.json'); - my %files = map { $_->{filename} => $_ } @{ (eval { decode_json($searchDataFile->slurp('UTF-8')) } // []) }; - my @updatedFiles; - - # Process the sample problems in the sample problem directory. - find( - { - wanted => sub { - return unless $_ =~ /\.pg$/; - - my $file = Mojo::File->new($File::Find::name); - my $lastModified = $file->stat->mtime; - - if ($files{$_}) { - push(@updatedFiles, $files{$_}); - return if $files{$_}{lastModified} >= $lastModified; - } - - my @fileContents = eval { split("\n", $file->slurp('UTF-8')) }; - return if $@; - - if (!$files{$_}) { - $files{$_} = { - type => 'sample problem', - filename => $_, - dir => $file->dirname->basename - }; - push(@updatedFiles, $files{$_}); - } - $files{$_}{lastModified} = $lastModified; - - my (%words, @kw, @macros, @subjects, $description); - - while (@fileContents) { - my $line = shift @fileContents; - if ($line =~ /^#:%\s*(\w+)\s*=\s*(.*)\s*$/) { - # Store the name and subjects. - $files{$_}{name} = $2 if $1 eq 'name'; - if ($1 eq 'subject') { - @subjects = split(',\s*', $2 =~ s/\[(.*)\]/$1/r); - } - } elsif ($line =~ /^#:\s*(.*)?/) { - my @newWords = $c->processLine($1); - @words{@newWords} = (1) x @newWords if @newWords; - } elsif ($line =~ /loadMacros\(/) { - my $macros = $line; - while ($line && $line !~ /\);\s*$/) { - $line = shift @fileContents; - $macros .= $line; - } - my @usedMacros = - map {s/['"\s]//gr} split(/\s*,\s*/, $macros =~ s/loadMacros\((.*)\)\;$/$1/r); - - # Get the macros other than PGML.pl, PGstandard.pl, and PGcourse.pl. - for my $m (@usedMacros) { - push(@macros, $m) unless $m =~ /^(PGML|PGstandard|PGcourse)\.pl$/; - } - } elsif ($line =~ /##\s*KEYWORDS\((.*)\)/) { - @kw = map {s/^'(.*)'$/$1/r} split(/,\s*/, $1); - } elsif ($line =~ /^##\s*DESCRIPTION/) { - $line = shift(@fileContents); - while ($line && $line !~ /^##\s*ENDDESCRIPTION/) { - $description .= ($line =~ s/^##\s+//r) . ' '; - $line = shift(@fileContents); - } - $description =~ s/\s+$//; - } - } - - $files{$_}{description} = $description; - $files{$_}{subjects} = \@subjects; - $files{$_}{terms} = [ keys %words ]; - $files{$_}{keywords} = \@kw; - $files{$_}{macros} = \@macros; - - return; - } - }, - $sampleProblemDir - ); - - # Process the POD in macros in the macros dir. - (undef, my $macro_files) = Pod::Simple::Search->new->inc(0)->survey($c->ce->{pg_dir} . "/macros"); - for my $macroFile (sort keys %$macro_files) { - next if $macroFile =~ /deprecated/; - - my $file = Mojo::File->new($macroFile); - my $fileName = $file->basename; - my $lastModified = $file->stat->mtime; - - if ($files{$fileName}) { - push(@updatedFiles, $files{$fileName}); - next if $files{$fileName}{lastModified} >= $lastModified; - } - - if (!$files{$fileName}) { - $files{$fileName} = { - type => 'macro', - id => scalar(keys %files) + 1, - filename => $fileName, - dir => $file->dirname->to_rel($c->ce->{pg_dir})->to_string - }; - push(@updatedFiles, $files{$fileName}); - } - $files{$fileName}{lastModified} = $lastModified; - - my $root = Pod::Simple::SimpleTree->new->parse_file($file->to_string)->root; - - $files{$fileName}{terms} = $c->extractHeaders($root); - - if (my $nameDescription = extractHeadText($root, 'NAME')) { - (undef, my $description) = split(/\s*-\s*/, $nameDescription, 2); - $files{$fileName}{description} = $description if $description; - } - } - - # Redindex in case files were added or removed. - my $count = 0; - $_->{id} = ++$count for @updatedFiles; - - $searchDataFile->spew(encode_json(\@updatedFiles), 'UTF-8'); - - return $c->render(json => \@updatedFiles); -} - -# Get the stop words. The stop words file is loaded the first time this method is called, -# and is stashed and returned in later calls. -sub stopWords ($c) { - return $c->stash->{stopWords} if $c->stash->{stopWords}; - $c->stash->{stopWords} = {}; - - my $contents = eval { $c->app->home->child('assets', 'stop-words-en.txt')->slurp('UTF-8') }; - return $c->stash->{stopWords} if $@; - - for my $line (split("\n", $contents)) { - chomp $line; - next if $line =~ /^#/ || !$line; - $c->stash->{stopWords}{$line} = 1; - } - - return $c->stash->{stopWords}; -} - -sub processLine ($c, $line) { - my %words; - - # Extract linked macros and problems. - my @linkedFiles = $line =~ /(?:PODLINK|PROBLINK)\('([\w.]+)'\)/g; - $words{$_} = 1 for @linkedFiles; - - # Replace any non-word characters with spaces. - $line =~ s/\W/ /g; - - for my $word (split(/\s+/, $line)) { - next if $word =~ /^\d*$/; - $word = lc($word); - $words{$word} = 1 if !$c->stopWords->{$word}; - } - return keys %words; -} - -# Extract the text for a section from the given POD with a section header title. -sub extractHeadText ($root, $title) { - my @index = grep { ref($root->[$_]) eq 'ARRAY' && $root->[$_][2] eq $title } 0 .. $#$root; - return unless @index == 1; - - my $node = $root->[ $index[0] + 1 ]; - my $str = ''; - for (2 .. $#$node) { - $str .= ref($node->[$_]) eq 'ARRAY' ? $node->[$_][2] : $node->[$_]; - } - return $str; -} - -# Extract terms form POD headers. -sub extractHeaders ($c, $root) { - my %terms = - map { $_ => 1 } - grep { $_ && !$c->stopWords->{$_} } - map { split(/\s+/, $_) } - map { lc($_) =~ s/\W/ /gr } - map { - grep { !ref($_) } - @$_[ 2 .. $#$_ ] - } - grep { ref($_) eq 'ARRAY' && $_->[0] =~ /^head\d+$/ } @$root; - return [ keys %terms ]; + return $c->render(json => getSearchData($c->ce->{webworkDirs}{DATA} . '/sample-problem-search-data.json')); } 1; diff --git a/lib/WeBWorK/Utils/PODParser.pm b/lib/WeBWorK/Utils/PODParser.pm deleted file mode 100644 index e11b5899a3..0000000000 --- a/lib/WeBWorK/Utils/PODParser.pm +++ /dev/null @@ -1,66 +0,0 @@ -package WeBWorK::Utils::PODParser; -use parent qw(Pod::Simple::XHTML); - -use strict; -use warnings; - -use Pod::Simple::XHTML; -use File::Basename qw(basename); - -# $podFiles must be provided in order for pod links to local files to work. It should be the -# first return value of the POD::Simple::Search survey method. -sub new { - my ($invocant, $podFiles) = @_; - my $class = ref $invocant || $invocant; - my $self = $class->SUPER::new(@_); - $self->perldoc_url_prefix("https://metacpan.org/pod/"); - $self->index(1); - $self->backlink(1); - $self->html_charset('UTF-8'); - $self->{podFiles} = $podFiles // {}; - return bless $self, $class; -} - -# Attempt to resolve links to local files. If a local file is not found, then -# let Pod::Simple::XHTML resolve to a cpan link. -sub resolve_pod_page_link { - my ($self, $target, $section) = @_; - - unless (defined $target) { - print "Using internal page link.\n" if $self->{verbose} > 2; - return $self->SUPER::resolve_pod_page_link($target, $section); - } - - my $podFound; - for (keys %{ $self->{podFiles} }) { - if ($target eq $_ =~ s/lib:://r || $target eq basename($self->{podFiles}{$_}) =~ s/\.pod$//r) { - $podFound = - $self->{assert_html_ext} ? ($self->{podFiles}{$_} =~ s/\.(pm|pl|pod)$/.html/r) : $self->{podFiles}{$_}; - last; - } - } - - if ($podFound) { - my $pod_url = $self->encode_entities($podFound =~ s/^$self->{source_root}/$self->{base_url}/r) - . ($section ? '#' . $self->idify($self->encode_entities($section), 1) : ''); - print "Resolved local pod link for $target" . ($section ? "/$section" : '') . " to $pod_url\n" - if $self->{verbose} > 2; - return $pod_url; - } - - print "Using cpan pod link for $target" . ($section ? "/$section" : '') . "\n" if $self->{verbose} > 2; - return $self->SUPER::resolve_pod_page_link($target, $section); -} - -# Trim spaces from the beginning of each line in code blocks. This attempts to -# trim spaces from all lines in the code block in the same amount as there are -# spaces at the beginning of the first line. Note that Pod::Simple::XHTML has -# already converted tab characters into 8 spaces. -sub handle_code { - my ($self, $code) = @_; - my $start_spaces = length(($code =~ /^( *)/)[0]) || ''; - $self->SUPER::handle_code($code =~ s/^( {1,$start_spaces})//gmr); - return; -} - -1; diff --git a/templates/ContentGenerator/SampleProblemViewer.html.ep b/templates/ContentGenerator/SampleProblemViewer.html.ep index 6837417146..1729c3fbc8 100644 --- a/templates/ContentGenerator/SampleProblemViewer.html.ep +++ b/templates/ContentGenerator/SampleProblemViewer.html.ep @@ -14,7 +14,14 @@ <%= javascript $c->url({ type => 'webwork', name => 'htdocs', file => 'node_modules/minisearch/dist/umd/index.js' }), defer => undef =%> - + <%= javascript $c->url({ type => 'webwork', name => 'htdocs', file => 'js/SampleProblemViewer/documentation-search.js' }), defer => undef =%> @@ -83,7 +90,9 @@ <%= link_to 'sample_problem_viewer', { filePath => 'macros' }, class => 'list-group-item list-group-item-action', begin =%> <%= maketext('Sample Problems by Macro') %> -
<%= maketext('For many macros, this lists all sample problems used by the macro.') %>
+
+ <%= maketext('For many macros, this lists all sample problems that use the macro.') %> +
% end