| 1 | package Plack::App::Gruntmaster::HTML; |
| 2 | use v5.14; |
| 3 | use parent qw/Exporter/; |
| 4 | our @EXPORT = qw/render render_article/; |
| 5 | |
| 6 | use File::Slurp qw/read_file/; |
| 7 | use HTML::Element::Library; |
| 8 | use HTML::TreeBuilder; |
| 9 | use POSIX qw//; |
| 10 | use Data::Dumper qw/Dumper/; |
| 11 | use Sort::ByExample |
| 12 | sorter => {-as => 'ct_sort', example => [qw/Running Pending Finished/], xform => sub {$_->{status}}}; |
| 13 | |
| 14 | my $optional_end_tags = {%HTML::Tagset::optionalEndTag, tr => 1, td => 1, th => 1}; |
| 15 | |
| 16 | sub ftime ($) { POSIX::strftime '%c', localtime shift } |
| 17 | sub literal ($) { |
| 18 | my ($html) = @_; |
| 19 | return '' unless $html; |
| 20 | my $b = HTML::TreeBuilder->new; |
| 21 | $b->ignore_unknown(0); |
| 22 | $b->parse($html); |
| 23 | HTML::Element::Library::super_literal $b->guts->as_HTML(undef, undef, $optional_end_tags); |
| 24 | } |
| 25 | |
| 26 | sub HTML::Element::edit_href { |
| 27 | my ($self, $sub) = @_; |
| 28 | local $_ = $self->attr('href'); |
| 29 | $sub->(); |
| 30 | $self->attr(href => $_); |
| 31 | } |
| 32 | |
| 33 | sub HTML::Element::iter3 { |
| 34 | my ($self, $data, $code) = @_; |
| 35 | my $orig = $self; |
| 36 | my $prev = $orig; |
| 37 | for my $el (@$data) { |
| 38 | my $current = $orig->clone; |
| 39 | $code->($el, $current); |
| 40 | $prev->postinsert($current); |
| 41 | $prev = $current; |
| 42 | } |
| 43 | $orig->detach; |
| 44 | } |
| 45 | |
| 46 | sub HTML::Element::fid { shift->look_down(id => shift) } |
| 47 | sub HTML::Element::fclass { shift->look_down(class => qr/\b$_[0]\b/) } |
| 48 | |
| 49 | sub HTML::Element::namedlink { |
| 50 | my ($self, $id, $name) = @_; |
| 51 | $name = $id unless $name && $name =~ /[[:graph:]]/; |
| 52 | $self = $self->find('a'); |
| 53 | $self->edit_href(sub {s/id/$id/}); |
| 54 | $self->replace_content($name); |
| 55 | } |
| 56 | |
| 57 | my %page_cache; |
| 58 | for (<tmpl/*>) { |
| 59 | my ($tmpl, $lang) = m,tmpl/(\w+)\.(\w+),; |
| 60 | my $builder = HTML::TreeBuilder->new; |
| 61 | $builder->ignore_unknown(0); |
| 62 | $page_cache{$tmpl, $lang} = $builder->parse_file($_); |
| 63 | } |
| 64 | |
| 65 | sub render { |
| 66 | my ($tmpl, $lang, %args) = @_; |
| 67 | $lang //= 'en'; |
| 68 | my $meat = _render($tmpl, $lang, %args); |
| 69 | my $html = _render('skel', $lang, %args, meat => $meat); |
| 70 | if ($tmpl eq 'pb_entry') { # Move sidebar to correct position |
| 71 | my $builder = HTML::TreeBuilder->new; |
| 72 | $builder->ignore_unknown(0); |
| 73 | my $tree = $builder->parse_content($html); |
| 74 | $tree->fid('content')->postinsert($tree->fid('sidebar')); |
| 75 | $html = $tree->as_HTML(undef, undef, $optional_end_tags) |
| 76 | } |
| 77 | $html |
| 78 | } |
| 79 | |
| 80 | sub render_article { |
| 81 | my ($art, $lang, %args) = @_; |
| 82 | $lang //= 'en'; |
| 83 | my $title = read_file "a/$art.$lang.title"; |
| 84 | chomp $title; |
| 85 | my $meat = read_file "a/$art.$lang"; |
| 86 | _render('skel', $lang, title => $title , meat => $meat, %args) |
| 87 | } |
| 88 | |
| 89 | sub _render { |
| 90 | my ($tmpl, $lang, %args) = @_; |
| 91 | my $tree = $page_cache{$tmpl, $lang}->clone or die "No such template/language combination: $tmpl/$lang\n"; |
| 92 | $tree = $tree->guts unless $tmpl eq 'skel'; |
| 93 | $tree->defmap(smap => \%args); |
| 94 | my $process = __PACKAGE__->can("process_$tmpl"); |
| 95 | $process->($tree, %args) if $process; |
| 96 | $_->attr('smap', undef) for $tree->look_down(sub {$_[0]->attr('smap')}); |
| 97 | $tree->as_HTML(undef, undef, $optional_end_tags); |
| 98 | } |
| 99 | |
| 100 | sub process_skel { |
| 101 | my ($tree, %args) = @_; |
| 102 | $tree->content_handler( |
| 103 | title => $args{title}, |
| 104 | content => literal $args{meat}); |
| 105 | } |
| 106 | |
| 107 | sub process_us_entry { |
| 108 | my ($tree, %args) = @_; |
| 109 | $tree->fid($_)->attr('href', "/$_/?owner=$args{id}") for qw/log pb/; |
| 110 | $tree->fid('track_user')->attr('data-user', $args{id}); |
| 111 | my @solved = map { $_->{solved} ? ($_->{problem}) : () } @{$args{problems}}; |
| 112 | my @attempted = map { !$_->{solved} ? ($_->{problem}) : () } @{$args{problems}}; |
| 113 | |
| 114 | my $pbiter = sub { |
| 115 | my ($data, $li) = @_; |
| 116 | $li->find('a')->namedlink($data); |
| 117 | }; |
| 118 | $tree->fid('solved')->find('li')->iter3(\@solved, $pbiter); |
| 119 | $tree->fid('attempted')->find('li')->iter3(\@attempted, $pbiter); |
| 120 | $tree->fid('solved_count')->replace_content(scalar @solved); |
| 121 | $tree->fid('attempted_count')->replace_content(scalar @attempted); |
| 122 | |
| 123 | my $ctiter = sub { |
| 124 | my ($data, $td) = @_; |
| 125 | $td->fclass('contest')->namedlink($data->{contest}, $data->{contest_name}); |
| 126 | $td->fclass('score')->replace_content($data->{score}); |
| 127 | $td->fclass('rank')->replace_content($data->{rank}); |
| 128 | }; |
| 129 | $tree->find('table')->find('tbody')->find('tr')->iter3($args{contests}, $ctiter); |
| 130 | } |
| 131 | |
| 132 | sub process_us { |
| 133 | my ($tree, %args) = @_; |
| 134 | my $iter = sub { |
| 135 | my ($data, $tr) = @_; |
| 136 | $tr->fclass('user')->namedlink($data->{id}, $data->{name}); |
| 137 | $tr->fclass($_)->replace_content($data->{$_}) for qw/solved attempted contests/; |
| 138 | }; |
| 139 | $tree->find('tbody')->find('tr')->iter3($args{us}, $iter); |
| 140 | } |
| 141 | |
| 142 | sub process_ct_entry { |
| 143 | my ($tree, %args) = @_; |
| 144 | $_->edit_href (sub {s/contest_id/$args{id}/}) for $tree->find('a'); |
| 145 | $tree->fid('editorial')->detach unless $args{finished}; |
| 146 | $tree->fid('links')->detach unless $args{started}; |
| 147 | my $status = ($args{time} < $args{start} ? 'starts' : 'ends'); |
| 148 | $tree->fclass('timer')->attr('data-stop', $status eq 'ends' ? $args{stop} : $args{start}); |
| 149 | $tree->content_handler( |
| 150 | start => ftime $args{start}, |
| 151 | stop => ftime $args{stop}, |
| 152 | status => $status, |
| 153 | description => literal $args{description}); |
| 154 | $tree->fid('ctcountdown')->detach if $args{time} >= $args{stop}; |
| 155 | } |
| 156 | |
| 157 | sub process_ct { |
| 158 | my ($tree, %args) = @_; |
| 159 | my $iter = sub { |
| 160 | my ($data, $tr) = @_; |
| 161 | $data->{$_} = ftime $data->{$_} for qw/start stop/; |
| 162 | $tr->hashmap(class => $data, [qw/name owner/]); |
| 163 | $tr->fclass('name')->namedlink($data->{id}, $data->{name}); |
| 164 | $tr->fclass('owner')->namedlink($data->{owner}, $data->{owner_name}); |
| 165 | }; |
| 166 | $_->{status} = $_->{finished} ? 'Finished' : $_->{started} ? 'Running' : 'Pending' for @{$args{ct}}; |
| 167 | $tree->find('tbody')->find('tr')->iter3([ct_sort @{$args{ct}}], $iter); |
| 168 | } |
| 169 | |
| 170 | sub process_pb_entry { |
| 171 | my ($tree, %args) = @_; |
| 172 | $tree->fid('owner')->edit_href(sub{s/owner_id/$args{owner}/}); |
| 173 | $tree->fid('job_log')->edit_href(sub{s/problem_id/$args{id}/}); |
| 174 | $tree->fid('solution')->edit_href(sub{s/problem_id/$args{id}/}); |
| 175 | $tree->content_handler( |
| 176 | statement => literal $args{statement}, |
| 177 | level => ucfirst $args{level}, |
| 178 | author => $args{author}, |
| 179 | owner => $args{owner_name} || $args{owner}); |
| 180 | if ($args{limits}) { |
| 181 | my @limits = (@{$args{limits}}, {format => 'Other', timeout => $args{timeout} }); |
| 182 | @limits = map { sprintf '%s (%s)', @{$_}{qw/timeout format/} } @limits; |
| 183 | $tree->look_down(smap => 'timeout')->replace_content(join ', ', @limits); |
| 184 | } |
| 185 | if ($args{contest_stop}) { |
| 186 | $tree->fid('solution')->detach; |
| 187 | $tree->fid('solution_modal')->detach; |
| 188 | my $score = $tree->fid('score'); |
| 189 | $score->attr('data-start' => $args{open_time}); |
| 190 | $score->attr('data-stop' => $args{contest_stop}); |
| 191 | $score->attr('data-value' => $args{value}); |
| 192 | $tree->fid('countdown')->attr('data-stop' => $args{contest_stop}); |
| 193 | } else { |
| 194 | $tree->fid('job_log')->edit_href(sub{$_ .= "&private=$args{private}"}) if $args{private}; |
| 195 | $tree->fid('solution')->detach unless $args{solution}; |
| 196 | $_->detach for $tree->fclass('rc'); # requires contest |
| 197 | $tree->fid('solution_modal')->replace_content(literal $args{solution}); |
| 198 | } |
| 199 | |
| 200 | $tree->look_down(name => 'problem')->attr(value => $args{id}); |
| 201 | my $contest = $tree->look_down(name => 'contest'); |
| 202 | $contest->attr(value => $args{args}{contest}) if $args{args}{contest}; |
| 203 | $contest->detach unless $args{args}{contest} |
| 204 | } |
| 205 | |
| 206 | sub process_sol { |
| 207 | my ($tree, %args) = @_; |
| 208 | $tree->content_handler(solution => literal $args{solution}); |
| 209 | } |
| 210 | |
| 211 | sub process_pb { |
| 212 | my ($tree, %args) = @_; |
| 213 | my $iter = sub { |
| 214 | my ($data, $tr) = @_; |
| 215 | $tr->set_child_content(class => 'author', $data->{author}); |
| 216 | $tr->set_child_content(class => 'level', ucfirst $data->{level}); |
| 217 | $tr->fclass('name')->namedlink($data->{id}, $data->{name}); |
| 218 | $tr->fclass('name')->find('a')->edit_href(sub {$_ .= "?contest=$args{args}{contest}"}) if $args{args}{contest}; |
| 219 | $tr->fclass('owner')->namedlink($data->{owner}, $data->{owner_name}); |
| 220 | $tr->find('td')->attr(class => $tr->find('td')->attr('class').' warning') if $data->{private} && !$args{args}{contest}; |
| 221 | }; |
| 222 | |
| 223 | $tree->find('tbody')->find('tr')->iter3([sort { $a->{value} <=> $b->{value} } @{$args{pb}}], $iter); |
| 224 | $tree->fid('open-alert')->detach unless $args{args}{contest}; |
| 225 | } |
| 226 | |
| 227 | sub process_log_entry { |
| 228 | my ($tree, %args) = @_; |
| 229 | $tree->fid('problem')->namedlink(@args{qw/problem problem_name/}); |
| 230 | $tree->fid('owner')->namedlink(@args{qw/owner owner_name/}); |
| 231 | $tree->fid('source')->namedlink("$args{id}.$args{extension}", sprintf '%.2fKB', $args{size}/1024); |
| 232 | if ($args{contest}) { |
| 233 | $tree->fid('contest')->namedlink(@args{qw/contest contest_name/}); |
| 234 | $tree->fid('problem')->find('a')->edit_href(sub {$_.="?contest=$args{contest}"}); |
| 235 | } else { |
| 236 | $tree->fid('contest')->left->detach; |
| 237 | $tree->fid('contest')->detach; |
| 238 | } |
| 239 | |
| 240 | $args{errors} ? $tree->fid('errors')->find('pre')->replace_content($args{errors}) : $tree->fid('errors')->detach; |
| 241 | my $iter = sub { |
| 242 | my ($data, $tr) = @_; |
| 243 | $data->{time} = sprintf '%.4fs', $data->{time}; |
| 244 | $tr->defmap(class => $data); |
| 245 | $tr->fclass('result_text')->attr(class => "r$data->{result}") |
| 246 | }; |
| 247 | $tree->fclass('result_text')->replace_content($args{result_text}); |
| 248 | $tree->fclass('result_text')->attr(class => "r$args{result}"); |
| 249 | $args{results} ? $tree->fid('results')->find('tbody')->find('tr')->iter3($args{results}, $iter) : $tree->fid('results')->detach; |
| 250 | $tree->fid('no_results')->detach if $tree->fid('results') || $tree->fid('errors'); |
| 251 | } |
| 252 | |
| 253 | sub process_log { |
| 254 | my ($tree, %args) = @_; |
| 255 | my $iter = sub { |
| 256 | my ($data, $tr) = @_; |
| 257 | $tr->fclass('id')->namedlink($data->{id}); |
| 258 | $tr->fclass('problem')->namedlink($data->{problem}, $data->{problem_name}); |
| 259 | $tr->fclass('problem')->find('a')->edit_href(sub{$_ .= '?contest='.$data->{contest}}) if $data->{contest}; |
| 260 | $tr->fclass('contest')->namedlink($data->{contest}, $data->{contest_name}) if $data->{contest}; |
| 261 | $tr->fclass('contest')->replace_content('None') unless $data->{contest}; |
| 262 | $tr->fclass('date')->replace_content(ftime $data->{date}); |
| 263 | $tr->fclass('source')->namedlink("$data->{id}.$data->{extension}", sprintf "%.2fKB %s", $data->{size}/1024, Plack::App::Gruntmaster::FORMAT_EXTENSION()->{$data->{format}}); |
| 264 | $tr->fclass('owner')->namedlink($data->{owner}, $data->{owner_name}); |
| 265 | $tr->fclass('result_text')->replace_content($data->{result_text}); |
| 266 | $tr->fclass('result_text')->attr(class => "r$data->{result}"); |
| 267 | $tr->find('td')->attr(class => $tr->find('td')->attr('class').' warning') if $data->{private}; |
| 268 | }; |
| 269 | $tree->find('table')->find('tbody')->find('tr')->iter3($args{log}, $iter); |
| 270 | $args{next_page} ? $tree->fclass('next')->namedlink($args{next_page}, 'Next') : $tree->fclass('next')->detach; |
| 271 | $args{previous_page} ? $tree->fclass('previous')->namedlink($args{previous_page}, 'Previous') : $tree->fclass('previous')->detach; |
| 272 | for my $cls (qw/next previous/) { |
| 273 | my $elem = $tree->fclass($cls); |
| 274 | next unless $elem; |
| 275 | delete $args{args}{page}; |
| 276 | my $str = join '&', map { $_ . '=' . $args{args}{$_} } keys %{$args{args}}; |
| 277 | $elem->find('a')->edit_href(sub{s/$/&$str/}) if $str; |
| 278 | } |
| 279 | $tree->fclass('current')->replace_content("Page $args{current_page} of $args{last_page}"); |
| 280 | } |
| 281 | |
| 282 | sub process_st { |
| 283 | my ($tree, %args) = @_; |
| 284 | $args{problems} //= []; |
| 285 | my $pbiter = sub { |
| 286 | my ($data, $th) = @_; |
| 287 | $th->attr(class => undef); |
| 288 | $th->namedlink(@$data); |
| 289 | $th->find('a')->edit_href(sub{s/$/?contest=$args{args}{contest}/}); |
| 290 | }; |
| 291 | $tree->fclass('problem')->iter3($args{problems}, $pbiter); |
| 292 | my $iter = sub { |
| 293 | my ($st, $tr) = @_; |
| 294 | $tr->set_child_content(class => 'rank', $st->{rank}); |
| 295 | $tr->set_child_content(class => 'score', $st->{score}); |
| 296 | $tr->fclass('user')->namedlink($st->{user}, $st->{user_name}); |
| 297 | my $pbscore = $tr->fclass('pbscore'); |
| 298 | $pbscore->iter($pbscore => @{$st->{scores}}); |
| 299 | }; |
| 300 | $tree->find('tbody')->find('tr')->iter3($args{st}, $iter); |
| 301 | } |
| 302 | |
| 303 | sub process_ed { |
| 304 | my ($tree, %args) = @_; |
| 305 | $tree->content_handler(editorial => literal $args{editorial}); |
| 306 | my $iter = sub { |
| 307 | my ($data, $div) = @_; |
| 308 | $div->set_child_content(class => 'value', $data->{value}); |
| 309 | $div->set_child_content(class => 'solution', literal $data->{solution}); |
| 310 | $div->fclass('problem')->namedlink($data->{id}, $data->{name}); |
| 311 | }; |
| 312 | my @pb = sort { $a->{value} <=> $b->{value} } @{$args{pb}}; |
| 313 | $tree->fclass('well')->iter3(\@pb, $iter); |
| 314 | } |
| 315 | |
| 316 | 1; |
| 317 | __END__ |