]>
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/; | |
7 | use HTML::Seamstress; | |
8 | use POSIX qw//; | |
9 | use Data::Dumper qw/Dumper/; | |
10 | ||
11 | sub ftime ($) { POSIX::strftime '%c', localtime shift } | |
12 | sub literal ($) { HTML::Element::Library::super_literal shift // '' } | |
13 | ||
14 | sub HTML::Element::edit_href { | |
15 | my ($self, $sub) = @_; | |
16 | local $_ = $self->attr('href'); | |
17 | $sub->(); | |
18 | $self->attr(href => $_); | |
19 | } | |
20 | ||
21 | sub HTML::Element::iter3 { | |
22 | my ($self, $data, $code) = @_; | |
23 | my $orig = $self; | |
24 | my $prev = $orig; | |
25 | for my $el (@$data) { | |
26 | my $current = $orig->clone; | |
27 | $code->($el, $current); | |
28 | $prev->postinsert($current); | |
29 | $prev = $current; | |
30 | } | |
31 | $orig->detach; | |
32 | } | |
33 | ||
34 | sub HTML::Element::fid { shift->look_down(id => shift) } | |
35 | sub HTML::Element::fclass { shift->look_down(class => shift) } | |
36 | ||
37 | sub HTML::Element::namedlink { | |
38 | my ($self, $id, $name) = @_; | |
39 | $name = $id unless $name =~ /[[:graph:]]/; | |
40 | $self = $self->find('a'); | |
41 | $self->edit_href(sub {s/id/$id/}); | |
42 | $self->replace_content($name); | |
43 | } | |
44 | ||
45 | sub render { | |
46 | my ($tmpl, $lang, %args) = @_; | |
47 | $lang //= 'en'; | |
48 | my $meat = _render($tmpl, $lang, %args); | |
49 | _render('skel', $lang, %args, meat => $meat) | |
50 | } | |
51 | ||
52 | sub render_article { | |
53 | my ($art, $lang) = @_; | |
54 | $lang //= 'en'; | |
55 | my $title = read_file "a/$art.$lang.title"; | |
56 | my $meat = read_file "a/$art.$lang"; | |
57 | _render('skel', $lang, title => $title , meat => $meat) | |
58 | } | |
59 | ||
60 | sub _render { | |
61 | my ($tmpl, $lang, %args) = @_; | |
62 | my $builder = HTML::Seamstress->new; | |
63 | $builder->ignore_unknown(0); | |
64 | my $tree = $builder->parse_file("tmpl/$tmpl.$lang"); | |
65 | $tree = $tree->guts unless $tmpl eq 'skel'; | |
66 | $tree->defmap(smap => \%args); | |
67 | my $process = __PACKAGE__->can("process_$tmpl"); | |
68 | $process->($tree, %args) if $process; | |
69 | $tree->as_HTML; | |
70 | } | |
71 | ||
72 | sub process_skel { | |
73 | my ($tree, %args) = @_; | |
74 | $tree->content_handler( | |
75 | title => $args{title}, | |
76 | content => literal $args{meat}); | |
77 | } | |
78 | ||
79 | sub process_us_entry { | |
80 | my ($tree, %args) = @_; | |
81 | $tree->fid($_)->attr('href', "/$_/?owner=$args{id}") for qw/log pb/; | |
f1c090e7 | 82 | $tree->fid('track_user')->attr('data-user', $args{id}); |
9a8a3012 MG |
83 | my @solved = map { $_->{solved} ? ($_->{problem}) : () } @{$args{problems}}; |
84 | my @attempted = map { !$_->{solved} ? ($_->{problem}) : () } @{$args{problems}}; | |
85 | ||
86 | my $pbiter = sub { | |
87 | my ($data, $li) = @_; | |
88 | $li->find('a')->namedlink($data); | |
89 | }; | |
90 | $tree->fid('solved')->find('li')->iter3(\@solved, $pbiter); | |
91 | $tree->fid('attempted')->find('li')->iter3(\@attempted, $pbiter); | |
92 | $tree->fid('solved_count')->replace_content(scalar @solved); | |
93 | $tree->fid('attempted_count')->replace_content(scalar @attempted); | |
94 | ||
95 | my $ctiter = sub { | |
96 | my ($data, $td) = @_; | |
dc6ca3bc | 97 | $td->fclass('contest')->namedlink($data->{contest}, $data->{contest_name}); |
9a8a3012 MG |
98 | $td->fclass('score')->replace_content($data->{score}); |
99 | $td->fclass('rank')->replace_content($data->{rank}); | |
100 | }; | |
101 | $tree->find('table')->find('tbody')->find('tr')->iter3($args{contests}, $ctiter); | |
3b69df7a MG |
102 | } |
103 | ||
104 | sub process_us { | |
105 | my ($tree, %args) = @_; | |
c5ff0b09 MG |
106 | my $iter = sub { |
107 | my ($data, $tr) = @_; | |
108 | $tr->fclass('user')->namedlink($data->{id}, $data->{name}); | |
109 | $tr->fclass($_)->replace_content($data->{$_}) for qw/solved attempted contests/; | |
110 | }; | |
111 | $tree->find('tbody')->find('tr')->iter3($args{us}, $iter); | |
3b69df7a MG |
112 | } |
113 | ||
114 | sub process_ct_entry { | |
115 | my ($tree, %args) = @_; | |
116 | $_->edit_href (sub {s/contest_id/$args{id}/}) for $tree->find('a'); | |
117 | $tree->fid('links')->detach unless $args{started}; | |
118 | $tree->content_handler( | |
119 | start => ftime $args{start}, | |
120 | stop => ftime $args{stop}, | |
121 | description => literal $args{description}); | |
122 | } | |
123 | ||
124 | sub process_ct { | |
125 | my ($tree, %args) = @_; | |
126 | my $iter = sub { | |
127 | my ($data, $tr) = @_; | |
128 | $data->{$_} = ftime $data->{$_} for qw/start stop/; | |
129 | $tr->hashmap(class => $data, [qw/name owner/]); | |
130 | $tr->fclass('name')->namedlink($data->{id}, $data->{name}); | |
131 | $tr->fclass('owner')->namedlink($data->{owner}, $data->{owner_name}); | |
132 | }; | |
133 | $args{$_} ? $tree->fid($_)->find('tbody')->find('tr')->iter3($args{$_}, $iter) : $tree->fid($_)->detach for qw/running pending finished/; | |
134 | } | |
135 | ||
136 | sub process_pb_entry { | |
137 | my ($tree, %args) = @_; | |
138 | $tree->fid('owner')->edit_href(sub{s/owner_id/$args{owner}/}); | |
139 | $tree->fid('job_log')->edit_href(sub{s/problem_id/$args{id}/}); | |
e4d5bdf5 | 140 | $tree->fid('solution')->edit_href(sub{s/problem_id/$args{id}/}); |
7b6c2d78 | 141 | $tree->fid('job_log')->edit_href(sub{$_ .= "&private=$args{private}"}) if $args{private}; |
3b69df7a MG |
142 | $tree->content_handler( |
143 | statement => literal $args{statement}, | |
6eb88ef9 | 144 | level => ucfirst $args{level}, |
3b69df7a MG |
145 | author => $args{author}, |
146 | owner => $args{owner_name} || $args{owner}); | |
9a4806b3 | 147 | if ($args{contest_stop}) { |
e4d5bdf5 MG |
148 | $tree->fid('solution')->detach; |
149 | $tree->fid('solution_modal')->detach; | |
9a4806b3 | 150 | my $countdown = $tree->fid('countdown'); |
12dd9340 | 151 | $countdown->attr('data-start' => $args{open_time}); |
9a4806b3 MG |
152 | $countdown->attr('data-stop' => $args{contest_stop}); |
153 | $countdown->attr('data-time' => $args{time}); | |
154 | my $left = $args{contest_stop} - $args{time}; | |
155 | $countdown->replace_content(sprintf '%02d:%02d:%02d', $left/60/60, $left/60%60, $left%60); | |
156 | $tree->fid('score')->attr('data-value' => $args{value}); | |
12dd9340 | 157 | $tree->fid('score')->replace_content(Gruntmaster::Data::calc_score($args{value}, $args{time} - $args{open_time}, 0, $args{contest_stop} - $args{contest_start})); |
9a4806b3 MG |
158 | } else { |
159 | $_->detach for $tree->fclass('rc'); # requires contest | |
e4d5bdf5 | 160 | $tree->fid('solution_modal')->fclass('modal-body')->replace_content(literal $args{solution}); |
9a4806b3 | 161 | } |
2dfeee94 | 162 | $tree->fid('solution')->detach unless $args{solution}; |
3b69df7a MG |
163 | if ($args{cansubmit}) { |
164 | $tree->look_down(name => 'problem')->attr(value => $args{id}); | |
165 | my $contest = $tree->look_down(name => 'contest'); | |
166 | $contest->attr(value => $args{contest}) if $args{contest}; | |
167 | $contest->detach unless $args{contest} | |
168 | } else { | |
169 | $tree->fid('submit')->detach | |
170 | } | |
171 | } | |
172 | ||
e4d5bdf5 MG |
173 | sub process_sol { |
174 | my ($tree, %args) = @_; | |
175 | $tree->content_handler(solution => literal $args{solution}); | |
176 | } | |
177 | ||
3b69df7a MG |
178 | sub process_pb { |
179 | my ($tree, %args) = @_; | |
180 | my $titer = sub { | |
181 | my ($data, $tr) = @_; | |
182 | $tr->set_child_content(class => 'author', $data->{author}); | |
183 | $tr->fclass('name')->namedlink($data->{id}, $data->{name}); | |
184 | $tr->fclass('name')->find('a')->edit_href(sub {$_ .= "?contest=$args{contest}"}) if $args{contest}; | |
185 | $tr->fclass('owner')->namedlink($data->{owner}, $data->{owner_name}); | |
186 | }; | |
187 | my $iter = sub { | |
188 | my ($data, $div) = @_; | |
189 | $div->attr(id => $data); | |
190 | $div->find('h2')->replace_content(ucfirst $data); | |
191 | $div->find('tbody')->iter3($args{$data}, $titer); | |
192 | }; | |
193 | $tree->fid('beginner')->iter3([grep {$args{$_}} qw/beginner easy medium hard/], $iter); | |
194 | } | |
195 | ||
196 | sub process_log_entry { | |
197 | my ($tree, %args) = @_; | |
435a869c MG |
198 | $tree->fid('problem')->namedlink(@args{qw/problem problem_name/}); |
199 | $tree->fid('owner')->namedlink(@args{qw/owner owner_name/}); | |
200 | $tree->fid('source')->namedlink("$args{id}.$args{extension}", sprintf '%.2fKB', $args{size}/1024); | |
201 | if ($args{contest}) { | |
202 | $tree->fid('contest')->namedlink(@args{qw/contest contest_name/}); | |
203 | $tree->fid('problem')->find('a')->edit_href(sub {$_.="?contest=$args{contest}"}); | |
204 | } else { | |
205 | $tree->fid('contest')->left->detach; | |
206 | $tree->fid('contest')->detach; | |
207 | } | |
208 | ||
3b69df7a MG |
209 | $args{errors} ? $tree->fid('errors')->find('pre')->replace_content($args{errors}) : $tree->fid('errors')->detach; |
210 | my $iter = sub { | |
211 | my ($data, $tr) = @_; | |
212 | $data->{time} = sprintf '%.4fs', $data->{time}; | |
213 | $tr->defmap(class => $data); | |
214 | $tr->fclass('result_text')->attr(class => "r$data->{result}") | |
215 | }; | |
435a869c | 216 | $args{results} ? $tree->fid('results')->find('tbody')->find('tr')->iter3($args{results}, $iter) : $tree->fid('results')->detach; |
0f67fb90 | 217 | $tree->fid('no_results')->detach if $tree->fid('results') || $tree->fid('errors'); |
3b69df7a MG |
218 | } |
219 | ||
220 | sub process_log { | |
221 | my ($tree, %args) = @_; | |
222 | my $iter = sub { | |
223 | my ($data, $tr) = @_; | |
224 | $tr->fclass('id')->namedlink($data->{id}); | |
225 | $tr->fclass('problem')->namedlink($data->{problem}, $data->{problem_name}); | |
226 | $tr->fclass('problem')->find('a')->edit_href(sub{$_ .= "?contest=$args{contest}"}) if $args{contest}; | |
227 | $tr->fclass('date')->replace_content(ftime $data->{date}); | |
228 | $tr->fclass('size')->namedlink("$data->{id}.$data->{extension}", sprintf "%.2fKB", $data->{size}/1024); | |
229 | $tr->fclass('size')->attr('data-private', '') if $data->{private}; | |
230 | $tr->fclass('owner')->namedlink($data->{owner}, $data->{owner_name}); | |
231 | $tr->fclass('result_text')->replace_content($data->{result_text}); | |
232 | $tr->fclass('result_text')->attr(class => "r$data->{result}"); | |
233 | }; | |
234 | $tree->find('table')->find('tbody')->find('tr')->iter3($args{log}, $iter); | |
fddf958b MG |
235 | $args{next_page} ? $tree->fclass('next')->namedlink($args{next_page}, 'Next') : $tree->fclass('next')->detach; |
236 | $args{previous_page} ? $tree->fclass('previous')->namedlink($args{previous_page}, 'Previous') : $tree->fclass('previous')->detach; | |
3b69df7a MG |
237 | $tree->fclass('current')->replace_content("Page $args{current_page} of $args{last_page}"); |
238 | } | |
239 | ||
240 | sub process_st { | |
241 | my ($tree, %args) = @_; | |
242 | $args{problems} //= []; | |
243 | my $pbiter = sub { | |
244 | my ($data, $th) = @_; | |
245 | $th->attr(class => undef); | |
246 | $th->namedlink($data->id, $data->name); | |
247 | }; | |
248 | $tree->fclass('problem')->iter3($args{problems}, $pbiter); | |
249 | my $iter = sub { | |
250 | my ($st, $tr) = @_; | |
251 | $tr->set_child_content(class => 'rank', $st->{rank}); | |
252 | $tr->set_child_content(class => 'score', $st->{score}); | |
dc7e3b7c | 253 | $tr->fclass('user')->namedlink($st->{user}, $st->{user_name}); |
3b69df7a | 254 | my $pbscore = $tr->fclass('pbscore'); |
3b69df7a MG |
255 | $pbscore->iter($pbscore => @{$st->{scores}}); |
256 | }; | |
257 | $tree->find('tbody')->find('tr')->iter3($args{st}, $iter); | |
258 | } |