effda377a2f9bbf1e5ce3564c2010238ac2c2923
[sheet.git] / browser.plp
1 <(common.inc.plp)><:
2 use List::Util qw(sum max first);
3 no if $] >= 5.018, warnings => 'experimental::smartmatch';
4
5 Html({
6         title => 'browser compatibility cheat sheet',
7         version => '1.4',
8         description => [
9                 "Compatibility table of new web features (HTML5, CSS3, SVG, Javascript)",
10                 "comparing support and usage share for all popular browser versions.",
11         ],
12         keywords => [qw'
13                 web browser support compatibility usage matrix available feature
14                 html html5 css css3 svg javascript js dom mobile
15                 ie internet explorer firefox chrome safari webkit opera
16         '],
17         stylesheet => [qw'circus dark mono red light'],
18         data => ['data/browser/support.inc.pl'],
19 });
20
21 say "<h1>Browser compatibility</h1>\n";
22
23 my $caniuse = do 'data/browser/support.inc.pl' or die $@ || $!;
24 $_->{verrelease} = {
25         # mark last three (future) versions as unreleased, ensure current isn't
26         map {
27                 $_->[-1] => 0, $_->[-2] => 0, $_->[-3] => 0,
28                 $_->[-4] => undef,
29         } $_->{versions}
30 } for values %{ $caniuse->{agents} };
31
32 my %CSTATS = (
33         'n'   => 'l1',
34         'n d' => 'l2',
35         'n x d' => 'l2 ex',
36         'p d' => 'l2',
37         'a d' => 'l2',
38         'y'   => 'l5',
39         'y x' => 'l5 ex',
40         'a'   => 'l3',
41         'a x' => 'l3 ex',
42         'p'   => 'l2',
43         'u'   => 'l0',
44 );
45 my %DSTATS = (
46         u => 'unknown',
47         n => 'unsupported',
48         p => 'plugin required',
49         a => 'partial',
50         y => 'supported',
51         d => '(disabled by default)',
52         x => sub {
53                 join(' ',
54                         'with prefix',
55                         map {"-$_-"}
56                         ($caniuse->{agents}->{$_[0]}->{prefix_exceptions} // {})->{$_[1]}
57                         // $caniuse->{agents}->{$_[0]}->{prefix} // (),
58                 );
59         },
60 );
61 my %PSTATS = (  # score percentage
62         y => 1,  'y x' => .9,
63         a => .5, 'a x' => .5, 'a d' => .2,
64         p => .2, 'p d' => .1,
65         n => 0,  'n d' => .2, 'n x d' => .2,
66         u => 0,
67 );
68 my %CSTATUS = (
69         unoff => 'l1', # unofficial
70         wd    => 'l2', # draft
71         cr    => 'l3', # candidate
72         pr    => 'l3', # proposed
73         rec   => 'l5', # recommendation
74         ls    => 'l4', # whatwg
75         ietf  => 'l0', # standard
76         other => 'l0', # non-w3
77 );
78 my %versions;
79 while (my ($browser, $row) = each %{ $caniuse->{agents} }) {
80         $versions{$browser} = [
81                 sort { paddedver($a) cmp paddedver($b) } grep { defined }
82                 @{ $row->{versions} }
83         ];
84 }
85
86 print <<'';
87 <p id="intro">Alternate rendition of Fyrd's <a href="http://caniuse.com/">when can I use...</a> page
88
89 my ($canihas, $usage);
90 my $minusage = $get{threshold} // 1;
91 given ($get{usage} // 'wm') {
92         when (!$_) {
93                 # none
94         }
95         when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
96                 Alert([
97                         'Invalid browser usage data request',
98                         'Identifier must be alphanumeric name or <q>0</q>.',
99                 ]);
100         }
101         $canihas = do "data/browser/usage-$_.inc.pl" or do {
102                 Alert('Browser usage data not found', $@ || $!);
103                 break;
104         };
105         $usage = $_;
106         my $ref = $canihas->{-title} || 'unknown';
107         $ref = showlink($ref, $_)
108                 for $canihas->{-site} || $canihas->{-source} || ();
109         $ref .= " $_" for $canihas->{-date} || ();
110         print "\nwith $ref browser usage statistics";
111 }
112
113 my @browsers;
114 if ($usage) { # first() does not work inside given >:(
115         # adapt version usage to actual support data
116         my %engineuse;  # prefix => usage sum
117         for my $browser (keys %versions) {
118                 my $row = $canihas->{$browser} // {};
119                 my $verlist = $versions{$browser} or next;
120                 if ($minusage and sum(values %$row) < $minusage) {
121                         delete $versions{$browser};
122                         next;
123                 }
124                 my %supported = map { $_ => 1 } @$verlist;
125
126                 # cascade unknown versions
127                 $row->{$_} //= undef for @$verlist;  # ensure stable keys during iteration
128                 while (my ($version, $usage) = each %$row) {
129                         next if defined $supported{$version};
130                         my $next = first { paddedver($_) ge paddedver($version) } @$verlist
131                                 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
132                         $row->{$next} += $usage;
133                         $row->{$version} = 0;  # balance browser total
134                 }
135
136                 # build row list for each version
137                 if ($minusage) {
138                         my @vershown;  # $verlist replacement
139                         my ($rowusage, @verrow) = (0);  # replacement row tracking
140                         for (@$verlist) {
141                                 push @verrow, $_;  # queue each version
142                                 if (($rowusage += $row->{$_}) >= $minusage) {
143                                         push @vershown, [@verrow];   # add row
144                                         ($rowusage, @verrow) = (0);  # reset row tracking
145                                 }
146                         }
147                         push @vershown, \@verrow if @verrow;  # always add latest
148                         @$verlist = @vershown;
149                 }
150                 else {
151                         @$verlist = map { [$_] } @$verlist;
152                 }
153
154                 # reusable aggregates (grouped by prefix (engine) and browser)
155                 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
156                 $row->{-total} = sum(values %$row);
157         }
158
159         # order browser columns by usage grouped by engine
160         @browsers = sort {
161                 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
162                 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
163                         ||
164                 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
165         } keys %versions;
166 }
167 else {
168         # order browser columns by name grouped by engine
169         @{$_} = map { [$_] } @{$_} for values %versions;
170         @browsers = sort {
171                 $caniuse->{agents}->{$b}->{prefix} cmp
172                 $caniuse->{agents}->{$a}->{prefix}
173                         ||
174                 $a cmp $b
175         } keys %versions;
176 }
177 :>.
178 </p>
179
180 <:
181 $canihas ||= {
182         map {
183                 $_ => +{
184                         map {
185                                 my $zero = $#$_ - 2;  # baseline index
186                                 ($_->[$zero - 2] =>  .5), # past
187                                 ($_->[$zero - 1] => 10 ), # previous
188                                 ($_->[$zero + 2] =>  0 ), # future
189                                 ($_->[$zero + 1] =>  .5), # next
190                                 ($_->[$zero    ] => 30 ), # current
191                         } $caniuse->{agents}->{$_}->{versions}
192                 }
193         } @browsers
194 }; # fallback hash based on release semantics
195
196 my $usagepct = 1;  # score multiplier for 0..100 result
197 # normalise usage percentage to only include shown browsers
198 $usagepct = 100.01 / featurescore({  # yes for every possible version
199         map { $_ => { map {$_ => 'y'} map { @{$_} } @{$versions{$_}} } } keys %versions
200 });
201 $_->{usage} = featurescore($_->{stats}) * $usagepct
202         for values %{ $caniuse->{data} };
203
204 print '<table class="mapped">';
205 print '<col span="3">';  # should match first thead row
206 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
207 say '</colgroup><col>';
208
209 my $header = join('',
210         '<tr>',
211         '<th colspan="3" rowspan="2">feature',
212         (map {
213                 my $name = $caniuse->{agents}->{$_}->{browser};
214                 sprintf('<th colspan="%d" class="%s" title="%s">%s',
215                         scalar @{ $versions{$_} },
216                         join(' ', map {"b-a-$_"} grep {$_}
217                                 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
218                         ),
219                         join(' ',
220                                 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
221                                 $name,
222                         ),
223                         do {
224                                 length $name <= (3 * @{ $versions{$_} }) ? $name
225                                         : $caniuse->{agents}->{$_}->{abbr};
226                         },
227                 )
228         } @browsers),
229         '<th rowspan="2">%',
230 );
231 print '<thead>', $header;
232 # preceding row without any colspan to work around gecko bug
233 print "\n<tr>";
234 for my $browser (@browsers) {
235         for (@{ $versions{$browser} }) {
236                 my $lastver = first {
237                         !defined $caniuse->{agents}->{$browser}->{verrelease}->{$_} # stable
238                 } reverse @{$_};
239                 printf('<td title="%s"%s>%s',
240                         join(' ',
241                                 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{@$_}) * $usagepct),
242                                 'version ' . showversions(@{$_}, undef),
243                                 $_->[-1] eq $lastver ? () : '(development)',
244                         ),
245                         !defined $lastver && ' class="ex"',
246                         showversions($lastver),
247                 );
248         }
249 }
250 say '</thead>';
251 say '<tfoot>', $header;
252 {
253         # prefix indicates browser family; count adjacent families
254         my (@families, %familycount);
255         for my $browser (@browsers) {
256                 my $family = $caniuse->{agents}->{$browser}->{prefix};
257                 push @families, $family unless $familycount{$family};
258                 $familycount{$family} += @{ $versions{$browser} };
259         }
260
261         print "\n", '<tr class="cat">';
262         printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
263 }
264 say '</tfoot>';
265
266 sub featurescore {
267         # relative amount of support for given feature
268         my $rank = 0;
269         if (my $row = shift) {
270                 if ($canihas) {
271                         while (my ($browser, $versions) = each %$row) {
272                                 ref $versions eq 'HASH' or next;
273                                 while (my ($version, $status) = each %$versions) {
274                                         $status =~ s/\h\#\d+//g;
275                                         $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
276                                 }
277                         }
278                         return $rank;
279                 }
280
281                 while (my ($browser, $vercols) = each %versions) {
282                         my $div = 0;  # multiplier exponent (decreased to lower value)
283                         my @vers = map { $row->{$browser}->{$_} } @$vercols;
284                         if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
285                                 my @future;  # find upcoming releases (after current)
286                                 for (reverse @$vercols) {
287                                         last if $_ eq $current;
288                                         push @future, pop @vers;
289                                         $_ eq 'u' and $_ = $vers[-1] for $future[-1];  # inherit latest value if unknown
290                                 }
291                                 splice @vers, -1, 0, @future;  # move ahead to decrease precedence
292                         }
293                         $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
294                 }
295         }
296         return $rank;
297 }
298
299 sub formatnotes {
300         my @html = @_;
301         for (@html) {
302                 s/\r\n?/\n/g;  # windows returns
303                 s/\h* $//gmx;  # trailing whitespace
304                 s/(?<= [^.\n]) $/./gmx;  # consistently end each line by a period
305                 Entity($_);
306                 s{  ` ([^`]*)  ` }{<code>$1</code>}gx;
307                 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
308         }
309         return @html;
310 }
311
312 sub notestotitle {
313         my @notes = @_;
314         for (@notes) {
315                 EscapeHTML($_);
316                 s{ \[ ([^]]*) \] \( [^)]* \) }{$1}gx;  # strip link urls
317         }
318         return @notes;
319 }
320
321 sub saytitlecol {
322         my ($id) = @_;
323         my $row = $caniuse->{data}->{$id};
324
325         for ($row->{categories}) {
326                 my $cell = $_ ? lc $_->[0] : '-';
327                 $cell =~ s/ api$//;  # trim unessential fluff in 'js api'
328                 printf '<th title="%s">%s', join(' + ', @$_), $cell;
329         }
330
331         print '<td>', map {
332                 sprintf('<a href="%s" onclick="%s">%s</a>',
333                         "#$id",
334                         sprintf("try { %s; return false } catch(err) { return true }",
335                                 "document.getElementById('$id').classList.toggle('target')",
336                         ),
337                         Entity($_),
338                 );
339         } $row->{title};
340         print '<div class=aside>';
341         print "<p>$_</p>"
342                 for formatnotes($row->{description}, $row->{notes} || ());
343         if (my %notes = %{ $row->{notes_by_num} }) {
344                 say '<p>Browser-specific notes:';
345                 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
346                 say '</p>';
347         }
348         printf 'Resources: %s.', join(', ', map {
349                 showlink($_->{title}, $_->{url})
350         } @$_) for grep { @$_ } $row->{links} // ();
351         printf '<br>Parent feature: %s.', join(', ', map {
352                 showlink($caniuse->{data}->{$_}->{title}, "#$_")
353         } $_) for $row->{parent} || ();
354         print '</div>';
355 }
356
357 sub saystatuscol {
358         my ($id) = @_;
359         my $row = $caniuse->{data}->{$id};
360
361         for ($row->{status}) {
362                 my $cell = $_ // '-';
363                 $cell = showlink($cell, $_) for $row->{spec} // ();
364                 printf '<td title="%s" class="l %s">%s',
365                         $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
366         }
367 }
368
369 sub saybrowsercols {
370         my ($id, $browser) = @_;
371         my $feature = $caniuse->{data}->{$id};
372         my $data = $feature->{stats}->{$browser};
373         if (ref $data eq 'ARRAY') {
374                 # special case for unsupported
375                 my $release = $caniuse->{agents}->{$browser}->{verrelease};
376                 $data = {
377                         map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
378                 };
379         }
380
381         my ($prev, @span);
382         for my $ver (@{ $versions{$browser} }, undef) {
383                 my $compare = (
384                         !defined $ver ? undef :      # last column if nameless
385                         ref $data ne 'HASH' ? '' :   # unclassified if no support hash
386                         $data->{ $ver->[-1] } // $prev  # known or inherit from predecessor
387                         // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
388                            ~~ 'n' && 'n'             # first known version is unsupported
389                         || 'u'                       # unsure
390                 );
391                 unless (!defined $prev or $prev ~~ $compare) {
392                         my $usage = sum(@{ $canihas->{$browser} }{ map { @{$_} } @span });
393
394                         # strip #\d note references from support class
395                         my @notes;
396                         push @notes, $feature->{notes_by_num}->{$1}
397                                 while $prev =~ s/\h \# (\d+) \b//x;
398
399                         # prepare version hover details
400                         my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(' ',
401                                 (map { ref $_ eq 'CODE' ? $_->($browser, $span[0]->[0]) : $_ }
402                                  map { $DSTATS{$_} // () }
403                                  map { split / /, $_ }
404                                  $prev
405                                 ),
406                                 'in', $caniuse->{agents}->{$browser}->{abbr},
407                                 showversions((map { @{$_} } @span), undef),
408                         ));
409                         $title .= "\n$_" for notestotitle(@notes);
410
411                         printf('<td class="%s" colspan="%d" title="%s">%s',
412                                 join(' ',
413                                         X => $CSTATS{$prev},
414                                         !$usage ? ('p0') : ('p',
415                                                 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
416                                                 sprintf('p%02d', $usage * ($usagepct - .0001)),
417                                         ),
418                                 ),
419                                 scalar @span,
420                                 $title,
421                                 showversions($span[0]->[0], @span > 1 ? $span[-1]->[-1] : ()),
422                         );
423                         undef $prev;
424                         @span = ();
425                 }
426                 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
427                 $prev = $compare;
428         }
429 }
430
431 sub sayusagecol {
432         my ($id) = @_;
433         print '<td>', int $caniuse->{data}->{$id}->{usage};
434 }
435
436 say '<tbody>';
437 for my $id (sort {
438         $caniuse->{data}->{$b}->{usage} <=> $caniuse->{data}->{$a}->{usage}
439 } keys %{ $caniuse->{data} }) {
440         $caniuse->{data}->{$id}->{stats} or next;  # skip metadata [summary]
441         printf '<tr id="%s">', $id;
442         saytitlecol($id);
443         saystatuscol($id);
444         saybrowsercols($id, $_) for @browsers;
445         sayusagecol($id);
446         say '</tr>';
447 }
448 say '</tbody>';
449 say '</table>';
450
451 sub paddedver {
452         # normalised version number comparable as string (cmp)
453         shift =~ /(?:.*-|^)(\d*)(.*)/;
454         # matched (major)(.minor) of last value in range (a-B)
455         return sprintf('%02d', $1 || 99) . $2;
456 }
457
458 sub showversions {
459         # title to describe minumum version and optional maximum for multiple cells
460         my @span = (map { split /-/ } grep { defined } @_);
461         return $span[0] =~ s/\.0\z//r if @_ <= 1;
462         splice @span, 1, -1;
463         return join('‒', @span);
464 }
465
466 :>
467 <hr>
468
469 <div class="legend">
470         <table class="glyphs"><tr>
471         <td class="X l5">supported
472         <td class="X l3">partial
473         <td class="X l2">optional
474         <td class="X l1">missing
475         <td class="X l0">unknown
476         <td class="X ex">prefixed
477         </table>
478
479         <p><: if ($usage) { :>
480                 Usage percentage:
481                 <span class="  p0">0</span> -
482                 <span class="p p0 p00">.01</span> -
483                 <span class="p p0 p05">1-9</span> -
484                 <span class="p p1">10</span> -
485                 <span class="p p2">20</span> -
486                 <span class="p p5">majority</span>
487 <: } else { :>
488                 <table class="glyphs"><tr>
489                         <td class="p p1">previous version</td>
490                         <td class="p p3">current</td>
491                         <td class="p p0 p00">upcoming (within months)</td>
492                         <td class="  p0">future (within a year)</td>
493                 </table>
494 <: } :> </p>
495
496         <div class="right">
497                 <ul class="legend legend-set">
498                 <li>default <strong>style</strong> is
499                         <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
500                 <li><strong>usage</strong> source is
501                         <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
502                 <li>usage <strong>threshold</strong> is
503                         <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
504                 </ul>
505         </div>
506 </div>
507
508 <script type="text/javascript" src="/searchlocal.js"></script>
509 <script type="text/javascript"> prependsearch(document.getElementById('intro')) </script>
510