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