]>
Commit | Line | Data |
---|---|---|
3b69df7a MG |
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/; | |
cc11db2f MG |
7 | use HTML::Element::Library; |
8 | use HTML::TreeBuilder; | |
3b69df7a MG |
9 | use POSIX qw//; |
10 | use Data::Dumper qw/Dumper/; | |
2d39838c | 11 | use Sort::ByExample |
2d39838c | 12 | sorter => {-as => 'ct_sort', example => [qw/Running Pending Finished/], xform => sub {$_->{status}}}; |
3b69df7a | 13 | |
127d5b70 MG |
14 | my $optional_end_tags = {%HTML::Tagset::optionalEndTag, tr => 1, td => 1, th => 1}; |
15 | ||
3b69df7a | 16 | sub ftime ($) { POSIX::strftime '%c', localtime shift } |
17a06386 | 17 | sub literal ($) { |
f8a4fa09 | 18 | my ($html) = @_; |
25ed3a34 | 19 | return '' unless $html; |
17a06386 MG |
20 | my $b = HTML::TreeBuilder->new; |
21 | $b->ignore_unknown(0); | |
f8a4fa09 | 22 | $b->parse($html); |
127d5b70 | 23 | HTML::Element::Library::super_literal $b->guts->as_HTML(undef, undef, $optional_end_tags); |
17a06386 | 24 | } |
3b69df7a MG |
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) } | |
28ec8060 | 47 | sub HTML::Element::fclass { shift->look_down(class => qr/\b$_[0]\b/) } |
3b69df7a MG |
48 | |
49 | sub HTML::Element::namedlink { | |
50 | my ($self, $id, $name) = @_; | |
10a4003f | 51 | $name = $id unless $name && $name =~ /[[:graph:]]/; |
3b69df7a MG |
52 | $self = $self->find('a'); |
53 | $self->edit_href(sub {s/id/$id/}); | |
54 | $self->replace_content($name); | |
55 | } | |
56 | ||
4b3f09fe MG |
57 | my %page_cache; |
58 | for (<tmpl/*>) { | |
59 | my ($tmpl, $lang) = m,tmpl/(\w+)\.(\w+),; | |
cc11db2f | 60 | my $builder = HTML::TreeBuilder->new; |
4b3f09fe MG |
61 | $builder->ignore_unknown(0); |
62 | $page_cache{$tmpl, $lang} = $builder->parse_file($_); | |
63 | } | |
64 | ||
3b69df7a MG |
65 | sub render { |
66 | my ($tmpl, $lang, %args) = @_; | |
67 | $lang //= 'en'; | |
68 | my $meat = _render($tmpl, $lang, %args); | |
e8147a94 MG |
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 | |
3b69df7a MG |
78 | } |
79 | ||
80 | sub render_article { | |
6533844f | 81 | my ($art, $lang, %args) = @_; |
3b69df7a MG |
82 | $lang //= 'en'; |
83 | my $title = read_file "a/$art.$lang.title"; | |
57566ce2 | 84 | chomp $title; |
3b69df7a | 85 | my $meat = read_file "a/$art.$lang"; |
6533844f | 86 | _render('skel', $lang, title => $title , meat => $meat, %args) |
3b69df7a MG |
87 | } |
88 | ||
89 | sub _render { | |
90 | my ($tmpl, $lang, %args) = @_; | |
4b3f09fe | 91 | my $tree = $page_cache{$tmpl, $lang}->clone or die "No such template/language combination: $tmpl/$lang\n"; |
3b69df7a MG |
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; | |
13bd6143 | 96 | $_->attr('smap', undef) for $tree->look_down(sub {$_[0]->attr('smap')}); |
127d5b70 | 97 | $tree->as_HTML(undef, undef, $optional_end_tags); |
3b69df7a MG |
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/; | |
f1c090e7 | 110 | $tree->fid('track_user')->attr('data-user', $args{id}); |
9a8a3012 MG |
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) = @_; | |
dc6ca3bc | 125 | $td->fclass('contest')->namedlink($data->{contest}, $data->{contest_name}); |
9a8a3012 MG |
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); | |
3b69df7a MG |
130 | } |
131 | ||
132 | sub process_us { | |
133 | my ($tree, %args) = @_; | |
c5ff0b09 MG |
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); | |
3b69df7a MG |
140 | } |
141 | ||
142 | sub process_ct_entry { | |
143 | my ($tree, %args) = @_; | |
144 | $_->edit_href (sub {s/contest_id/$args{id}/}) for $tree->find('a'); | |
f34b3eac | 145 | $tree->fid('editorial')->detach unless $args{finished}; |
3b69df7a | 146 | $tree->fid('links')->detach unless $args{started}; |
20777d84 MG |
147 | my $status = ($args{time} < $args{start} ? 'starts' : 'ends'); |
148 | $tree->fclass('timer')->attr('data-stop', $status eq 'ends' ? $args{stop} : $args{start}); | |
3b69df7a MG |
149 | $tree->content_handler( |
150 | start => ftime $args{start}, | |
151 | stop => ftime $args{stop}, | |
20777d84 | 152 | status => $status, |
3b69df7a | 153 | description => literal $args{description}); |
00398f5c | 154 | $tree->fid('ctcountdown')->detach if $args{time} >= $args{stop}; |
3b69df7a MG |
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 | }; | |
2d39838c MG |
166 | $_->{status} = $_->{finished} ? 'Finished' : $_->{started} ? 'Running' : 'Pending' for @{$args{ct}}; |
167 | $tree->find('tbody')->find('tr')->iter3([ct_sort @{$args{ct}}], $iter); | |
3b69df7a MG |
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}/}); | |
e4d5bdf5 | 174 | $tree->fid('solution')->edit_href(sub{s/problem_id/$args{id}/}); |
3b69df7a MG |
175 | $tree->content_handler( |
176 | statement => literal $args{statement}, | |
6eb88ef9 | 177 | level => ucfirst $args{level}, |
3b69df7a MG |
178 | author => $args{author}, |
179 | owner => $args{owner_name} || $args{owner}); | |
14582b6f MG |
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 | } | |
9a4806b3 | 185 | if ($args{contest_stop}) { |
e4d5bdf5 MG |
186 | $tree->fid('solution')->detach; |
187 | $tree->fid('solution_modal')->detach; | |
b3da3730 | 188 | $tree->fid('score')->replace_content($args{value}); |
20777d84 | 189 | $tree->fid('countdown')->attr('data-stop' => $args{contest_stop}); |
9a4806b3 | 190 | } else { |
bca1707c | 191 | $tree->fid('job_log')->edit_href(sub{$_ .= "&private=$args{private}"}) if $args{private}; |
c016b644 | 192 | $tree->fid('solution')->detach unless $args{solution}; |
9a4806b3 | 193 | $_->detach for $tree->fclass('rc'); # requires contest |
0f578ab0 | 194 | $tree->fid('solution_modal')->replace_content(literal $args{solution}); |
9a4806b3 | 195 | } |
dd962dd2 MG |
196 | |
197 | $tree->look_down(name => 'problem')->attr(value => $args{id}); | |
198 | my $contest = $tree->look_down(name => 'contest'); | |
199 | $contest->attr(value => $args{args}{contest}) if $args{args}{contest}; | |
200 | $contest->detach unless $args{args}{contest} | |
3b69df7a MG |
201 | } |
202 | ||
e4d5bdf5 MG |
203 | sub process_sol { |
204 | my ($tree, %args) = @_; | |
205 | $tree->content_handler(solution => literal $args{solution}); | |
206 | } | |
207 | ||
3b69df7a MG |
208 | sub process_pb { |
209 | my ($tree, %args) = @_; | |
2d39838c | 210 | my $iter = sub { |
3b69df7a MG |
211 | my ($data, $tr) = @_; |
212 | $tr->set_child_content(class => 'author', $data->{author}); | |
2d39838c | 213 | $tr->set_child_content(class => 'level', ucfirst $data->{level}); |
3b69df7a | 214 | $tr->fclass('name')->namedlink($data->{id}, $data->{name}); |
5dcb426c | 215 | $tr->fclass('name')->find('a')->edit_href(sub {$_ .= "?contest=$args{args}{contest}"}) if $args{args}{contest}; |
3b69df7a | 216 | $tr->fclass('owner')->namedlink($data->{owner}, $data->{owner_name}); |
5dcb426c | 217 | $tr->find('td')->attr(class => $tr->find('td')->attr('class').' warning') if $data->{private} && !$args{args}{contest}; |
3b69df7a | 218 | }; |
2d39838c | 219 | |
68b0f287 | 220 | $tree->find('tbody')->find('tr')->iter3([sort { $a->{value} <=> $b->{value} } @{$args{pb}}], $iter); |
5dcb426c | 221 | $tree->fid('open-alert')->detach unless $args{args}{contest}; |
3b69df7a MG |
222 | } |
223 | ||
224 | sub process_log_entry { | |
225 | my ($tree, %args) = @_; | |
435a869c MG |
226 | $tree->fid('problem')->namedlink(@args{qw/problem problem_name/}); |
227 | $tree->fid('owner')->namedlink(@args{qw/owner owner_name/}); | |
228 | $tree->fid('source')->namedlink("$args{id}.$args{extension}", sprintf '%.2fKB', $args{size}/1024); | |
229 | if ($args{contest}) { | |
230 | $tree->fid('contest')->namedlink(@args{qw/contest contest_name/}); | |
231 | $tree->fid('problem')->find('a')->edit_href(sub {$_.="?contest=$args{contest}"}); | |
232 | } else { | |
233 | $tree->fid('contest')->left->detach; | |
234 | $tree->fid('contest')->detach; | |
235 | } | |
236 | ||
3b69df7a MG |
237 | $args{errors} ? $tree->fid('errors')->find('pre')->replace_content($args{errors}) : $tree->fid('errors')->detach; |
238 | my $iter = sub { | |
239 | my ($data, $tr) = @_; | |
240 | $data->{time} = sprintf '%.4fs', $data->{time}; | |
241 | $tr->defmap(class => $data); | |
242 | $tr->fclass('result_text')->attr(class => "r$data->{result}") | |
243 | }; | |
e1fab7d7 MG |
244 | $tree->fclass('result_text')->replace_content($args{result_text}); |
245 | $tree->fclass('result_text')->attr(class => "r$args{result}"); | |
435a869c | 246 | $args{results} ? $tree->fid('results')->find('tbody')->find('tr')->iter3($args{results}, $iter) : $tree->fid('results')->detach; |
0f67fb90 | 247 | $tree->fid('no_results')->detach if $tree->fid('results') || $tree->fid('errors'); |
3b69df7a MG |
248 | } |
249 | ||
250 | sub process_log { | |
251 | my ($tree, %args) = @_; | |
252 | my $iter = sub { | |
253 | my ($data, $tr) = @_; | |
254 | $tr->fclass('id')->namedlink($data->{id}); | |
255 | $tr->fclass('problem')->namedlink($data->{problem}, $data->{problem_name}); | |
e2c74da6 | 256 | $tr->fclass('problem')->find('a')->edit_href(sub{$_ .= '?contest='.$data->{contest}}) if $data->{contest}; |
8d690af5 MG |
257 | $tr->fclass('contest')->namedlink($data->{contest}, $data->{contest_name}) if $data->{contest}; |
258 | $tr->fclass('contest')->replace_content('None') unless $data->{contest}; | |
3b69df7a | 259 | $tr->fclass('date')->replace_content(ftime $data->{date}); |
93ed6f84 | 260 | $tr->fclass('source')->namedlink("$data->{id}.$data->{extension}", sprintf "%.2fKB %s", $data->{size}/1024, Plack::App::Gruntmaster::FORMAT_EXTENSION()->{$data->{format}}); |
3b69df7a MG |
261 | $tr->fclass('owner')->namedlink($data->{owner}, $data->{owner_name}); |
262 | $tr->fclass('result_text')->replace_content($data->{result_text}); | |
263 | $tr->fclass('result_text')->attr(class => "r$data->{result}"); | |
a33e01dd | 264 | $tr->find('td')->attr(class => $tr->find('td')->attr('class').' warning') if $data->{private}; |
3b69df7a MG |
265 | }; |
266 | $tree->find('table')->find('tbody')->find('tr')->iter3($args{log}, $iter); | |
fddf958b MG |
267 | $args{next_page} ? $tree->fclass('next')->namedlink($args{next_page}, 'Next') : $tree->fclass('next')->detach; |
268 | $args{previous_page} ? $tree->fclass('previous')->namedlink($args{previous_page}, 'Previous') : $tree->fclass('previous')->detach; | |
4a3952b2 MG |
269 | for my $cls (qw/next previous/) { |
270 | my $elem = $tree->fclass($cls); | |
271 | next unless $elem; | |
272 | delete $args{args}{page}; | |
418101af | 273 | my $str = join '&', map { $_ . '=' . $args{args}{$_} } keys %{$args{args}}; |
8678e0dd | 274 | $elem->find('a')->edit_href(sub{s/$/&$str/}) if $str; |
4a3952b2 | 275 | } |
3b69df7a MG |
276 | $tree->fclass('current')->replace_content("Page $args{current_page} of $args{last_page}"); |
277 | } | |
278 | ||
279 | sub process_st { | |
280 | my ($tree, %args) = @_; | |
281 | $args{problems} //= []; | |
282 | my $pbiter = sub { | |
283 | my ($data, $th) = @_; | |
284 | $th->attr(class => undef); | |
ebca729d | 285 | $th->namedlink(@$data); |
b7aa9f38 | 286 | $th->find('a')->edit_href(sub{s/$/?contest=$args{args}{contest}/}); |
3b69df7a MG |
287 | }; |
288 | $tree->fclass('problem')->iter3($args{problems}, $pbiter); | |
289 | my $iter = sub { | |
290 | my ($st, $tr) = @_; | |
291 | $tr->set_child_content(class => 'rank', $st->{rank}); | |
292 | $tr->set_child_content(class => 'score', $st->{score}); | |
dc7e3b7c | 293 | $tr->fclass('user')->namedlink($st->{user}, $st->{user_name}); |
3b69df7a | 294 | my $pbscore = $tr->fclass('pbscore'); |
3b69df7a MG |
295 | $pbscore->iter($pbscore => @{$st->{scores}}); |
296 | }; | |
297 | $tree->find('tbody')->find('tr')->iter3($args{st}, $iter); | |
298 | } | |
645cfb7d MG |
299 | |
300 | sub process_ed { | |
301 | my ($tree, %args) = @_; | |
00c887ce | 302 | $tree->content_handler(editorial => literal $args{editorial}); |
645cfb7d MG |
303 | my $iter = sub { |
304 | my ($data, $div) = @_; | |
305 | $div->set_child_content(class => 'value', $data->{value}); | |
306 | $div->set_child_content(class => 'solution', literal $data->{solution}); | |
307 | $div->fclass('problem')->namedlink($data->{id}, $data->{name}); | |
308 | }; | |
25ed3a34 | 309 | my @pb = sort { $a->{value} <=> $b->{value} } @{$args{pb}}; |
645cfb7d MG |
310 | $tree->fclass('well')->iter3(\@pb, $iter); |
311 | } | |
4b3f09fe MG |
312 | |
313 | 1; | |
314 | __END__ |