2 use List::Util qw(sum max first);
3 no if $] >= 5.018, warnings => 'experimental::smartmatch';
6 title => 'browser compatibility cheat sheet',
9 "Compatibility table of new web features (HTML5, CSS3, SVG, Javascript)",
10 "comparing support and usage share for all popular browser versions.",
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
17 stylesheet => [qw'circus dark mono red light'],
18 data => ['data/browser/support.inc.pl'],
21 say "<h1>Browser compatibility</h1>\n";
23 my $caniuse = do 'data/browser/support.inc.pl' or die $@ || $!;
44 p => 'plugin required',
47 d => '(disabled by default)',
52 $caniuse->{agents}->{$_[0]}->{version_list}->{$_[1]}->{prefix}
53 // $caniuse->{agents}->{$_[0]}->{prefix} // (),
57 my %PSTATS = ( # score percentage
59 a => .5, 'a x' => .5, 'a d' => .2,
61 n => 0, 'n d' => .2, 'n x d' => .2,
65 unoff => 'l1', # unofficial
67 cr => 'l3', # candidate
68 pr => 'l3', # proposed
69 rec => 'l5', # recommendation
71 ietf => 'l0', # standard
72 other => 'l0', # non-w3
75 while (my ($browser, $row) = each %{ $caniuse->{agents} }) {
76 $versions{$browser} = [
77 sort { paddedver($a) cmp paddedver($b) } grep { defined }
82 my $ref = showlink('Can I use', 'https://caniuse.com/');
83 $ref =~ s/(?=>)/ title="updated $_"/
84 for map { s/[\sT].*//r } $caniuse->{-date} || ();
85 $ref = "Fyrd's $ref page";
86 say '<p id="intro">Alternate rendition of '.$ref;
88 my ($canihas, $usage);
89 my $minusage = $get{threshold} // 1;
90 given ($get{usage} // 'wm') {
94 when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
96 'Invalid browser usage data request',
97 'Identifier must be alphanumeric name or <q>0</q>.',
100 $canihas = do "data/browser/usage-$_.inc.pl" or do {
101 Alert('Browser usage data not found', $@ || $!);
105 my $ref = $canihas->{-title} || 'unknown';
106 $ref = showlink($ref, $_)
107 for $canihas->{-site} || $canihas->{-source} || ();
108 $ref =~ s/(?=>)/ title="updated $_"/ for $canihas->{-date} || ();
109 print "\nwith $ref browser usage statistics";
113 if ($usage) { # first() does not work inside given >:(
114 # adapt version usage to actual support data
115 my %engineuse; # prefix => usage sum
116 for my $browser (keys %versions) {
117 my $row = $canihas->{$browser} // {};
118 my $verlist = $versions{$browser} or next;
119 if ($minusage and sum(values %$row) < $minusage) {
120 delete $versions{$browser};
123 my %supported = map { $_ => 1 } @$verlist;
125 # cascade unknown versions
126 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
127 while (my ($version, $usage) = each %$row) {
128 next if defined $supported{$version};
129 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
130 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
131 $row->{$next} += $usage;
132 $row->{$version} = 0; # balance browser total
135 # build row list for each version
137 my @vershown; # $verlist replacement
138 my ($rowusage, @verrow) = (0); # replacement row tracking
140 push @verrow, $_; # queue each version
141 if (($rowusage += $row->{$_}) >= $minusage) {
142 push @vershown, [@verrow]; # add row
143 ($rowusage, @verrow) = (0); # reset row tracking
146 push @vershown, \@verrow if @verrow; # always add latest
147 @$verlist = @vershown;
150 @$verlist = map { [$_] } @$verlist;
153 # reusable aggregates (grouped by prefix (engine) and browser)
154 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
155 $row->{-total} = sum(values %$row);
158 # order browser columns by usage grouped by engine
160 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
161 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
163 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
167 # order browser columns by name grouped by engine
168 @{$_} = map { [$_] } @{$_} for values %versions;
170 $caniuse->{agents}->{$b}->{prefix} cmp
171 $caniuse->{agents}->{$a}->{prefix}
184 my $zero = $#$_ - 2; # baseline index
185 ($_->[$zero - 2] => .5), # past
186 ($_->[$zero - 1] => 10 ), # previous
187 ($_->[$zero + 2] => 0 ), # future
188 ($_->[$zero + 1] => .5), # next
189 ($_->[$zero ] => 30 ), # current
190 } $caniuse->{agents}->{$_}->{versions}
193 }; # fallback hash based on release semantics
195 # score multiplier for percentage of all browser versions
196 my $usagepct = 99.99 / sum(
197 map { $_->{-total} // values %{$_} }
198 map { $canihas->{$_} }
203 $_->{usage} = featurescore($_->{stats}) * $usagepct
204 for values %{ $caniuse->{data} };
206 print '<table class="mapped">';
207 print '<col span="3">'; # should match first thead row
208 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
209 say '</colgroup><col>';
211 my $header = join('',
213 '<th colspan="3" rowspan="2">feature',
215 my $name = $caniuse->{agents}->{$_}->{browser};
216 sprintf('<th colspan="%d" class="%s" title="%s">%s',
217 scalar @{ $versions{$_} },
218 join(' ', map {"b-a-$_"} grep {$_}
219 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
222 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
226 length $name <= (3 * @{ $versions{$_} }) ? $name
227 : $caniuse->{agents}->{$_}->{abbr};
233 print '<thead>', $header;
234 # preceding row without any colspan to work around gecko bug
236 for my $browser (@browsers) {
237 for my $span (@{ $versions{$browser} }) {
238 my $lastver = first {
239 $caniuse->{agents}->{$browser}->{version_list}->{$_}->{release_date} # stable
241 printf('<td title="%s"%s>%s',
243 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{ @{$span} }) * $usagepct),
244 'version ' . showversions(@{$span}, undef),
246 $_ ? sprintf('(released %d)', $_/3600/24/365.25 + 1970) : '(development)'
247 } $caniuse->{agents}->{$browser}->{version_list}->{$lastver}->{release_date}),
249 !defined $lastver && ' class="ex"',
250 showversions($lastver // $span->[0]),
255 say '<tfoot>', $header;
257 # prefix indicates browser family; count adjacent families
258 my (@families, %familycount);
259 for my $browser (@browsers) {
260 my $family = $caniuse->{agents}->{$browser}->{prefix};
261 push @families, $family unless $familycount{$family};
262 $familycount{$family} += @{ $versions{$browser} };
265 print "\n", '<tr class="cat">';
266 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
271 # relative amount of support for given feature
273 if (my $row = shift) {
275 while (my ($browser, $versions) = each %$row) {
276 ref $versions eq 'HASH' or next;
278 for my $version (@{ $caniuse->{agents}->{$browser}->{versions} }) {
279 my $status = $versions->{$version} // $prev;
280 $status =~ s/\h\#\d+//g;
281 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
288 while (my ($browser, $vercols) = each %versions) {
289 my $div = 0; # multiplier exponent (decreased to lower value)
290 my @vers = map { $row->{$browser}->{$_} } @$vercols;
291 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
292 my @future; # find upcoming releases (after current)
293 for (reverse @$vercols) {
294 last if $_ eq $current;
295 push @future, pop @vers;
296 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
298 splice @vers, -1, 0, @future; # move ahead to decrease precedence
300 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
309 s/\r\n?/\n/g; # windows returns
310 s/\h* $//gmx; # trailing whitespace
311 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
313 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
314 s{ \(\K (?: \Qhttps://caniuse.com\E )? (?: /? \#feat= | / ) }{#}gx;
315 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
324 s{ \[ ([^]]*) \] \( [^)]* \) }{$1}gx; # strip link urls
331 my $row = $caniuse->{data}->{$id};
333 for ($row->{categories}) {
334 my $cell = $_ ? lc $_->[0] : '-';
335 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
336 printf '<th title="%s">%s', join(' + ', @$_), $cell;
340 sprintf('<a href="%s" onclick="%s">%s</a>',
342 sprintf("try { %s; return false } catch(err) { return true }",
343 "document.getElementById('$id').classList.toggle('target')",
348 print '<div class=aside>';
350 for formatnotes($row->{description}, $row->{notes} || ());
351 if (my %notes = %{ $row->{notes_by_num} }) {
352 say '<p>Browser-specific notes:';
353 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
356 printf 'Resources: %s.', join(', ', map {
357 showlink($_->{title}, $_->{url})
358 } @$_) for grep { @$_ } $row->{links} // ();
359 printf '<br>Parent feature: %s.', join(', ', map {
360 showlink($caniuse->{data}->{$_}->{title}, "#$_")
361 } $_) for $row->{parent} || ();
367 my $row = $caniuse->{data}->{$id};
369 for ($row->{status}) {
370 my $cell = $_ // '-';
371 $cell = showlink($cell, $_) for $row->{spec} // ();
372 printf '<td title="%s" class="l %s">%s',
373 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
378 my ($id, $browser) = @_;
379 my $feature = $caniuse->{data}->{$id};
380 my $data = $feature->{stats}->{$browser};
381 if (ref $data eq 'ARRAY') {
382 # special case for unsupported
385 keys %{ $caniuse->{agents}->{$browser}->{version_list} }
390 for my $ver (@{ $versions{$browser} }, undef) {
392 !defined $ver ? undef : # last column if nameless
393 ref $data ne 'HASH' ? '' : # unclassified if no support hash
394 (first { defined } @{$data}{ reverse @{$ver} }) # last known version
395 // $prev # inherit from predecessor
398 if (defined $prev and not $prev ~~ $compare) {
400 my @vercover = (map { @{$_} } @span); # accumulated conforming versions
401 for ($ver ? @{$ver} : ()) {
402 last if defined $data->{$_}; # until different
403 push @vercover, $_; # matches from next span start
405 my $usage = sum(@{ $canihas->{$browser} }{@vercover});
407 # strip #\d note references from support class
409 push @notes, $feature->{notes_by_num}->{$1}
410 while $prev =~ s/\h \# (\d+) \b//x;
412 # prepare version hover details
413 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(' ',
414 (map { ref $_ eq 'CODE' ? $_->($browser, $vercover[0]) : $_ }
415 map { $DSTATS{$_} // () }
416 map { split / /, $_ }
419 'in', $caniuse->{agents}->{$browser}->{abbr},
420 showversions(@vercover, undef),
422 $title .= "\n$_" for notestotitle(@notes);
424 $prev .= ' #' if @notes and $prev =~ /^y/;
425 printf('<td class="%s" colspan="%d" title="%s">%s',
428 !$usage ? ('p0') : ('p',
429 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
430 sprintf('p%02d', $usage * ($usagepct - .0001)),
435 showversions($span[0]->[0], @span > 1 && defined $ver ? $span[-1]->[-1] : ()),
441 my $startversion = first { defined $data->{ $ver->[$_] } }
442 reverse 0 .. $#{$ver}; # compare index
443 push @span, [ @{$ver}[ $startversion .. $#{$ver} ] ];
451 print '<td>', int $caniuse->{data}->{$id}->{usage};
456 $caniuse->{data}->{$b}->{usage} <=> $caniuse->{data}->{$a}->{usage}
457 } keys %{ $caniuse->{data} }) {
458 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
459 printf '<tr id="%s">', $id;
462 saybrowsercols($id, $_) for @browsers;
470 # normalised version number comparable as string (cmp)
471 $_[0] =~ m/(?:.*-|^)(\d*)(.*)/;
472 # matched (major)(.minor) of last value in range (a-B)
473 return sprintf('%02d', length $1 ? $1 : 99) . $2;
477 # title to describe minumum version and optional maximum for multiple cells
478 my @span = (map { split /-/ } grep { defined } @_);
479 return $span[0] =~ s/\.0\z//r if @_ <= 1;
481 return join('‒', @span);
488 <table class="glyphs"><tr>
489 <td class="X l5">supported
490 <td class="X l4">annotated
491 <td class="X l3">partial
492 <td class="X l2">optional
493 <td class="X l1">missing
494 <td class="X l0">unknown
495 <td class="X ex">prefixed
498 <p><: if ($usage) { :>
500 <span class=" p0">0</span> -
501 <span class="p p0 p00">.01</span> -
502 <span class="p p0 p05">1-9</span> -
503 <span class="p p1">10</span> -
504 <span class="p p2">20</span> -
505 <span class="p p5">majority</span>
507 <table class="glyphs"><tr>
508 <td class="p p1">previous version</td>
509 <td class="p p3">current</td>
510 <td class="p p0 p00">upcoming (within months)</td>
511 <td class=" p0">future (within a year)</td>
516 <ul class="legend legend-set">
517 <li>default <strong>style</strong> is
518 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
519 <li><strong>usage</strong> source is
520 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
521 <li>usage <strong>threshold</strong> is
522 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
527 <script type="text/javascript" src="/searchlocal.js"></script>
528 <script type="text/javascript"><!--
529 prependsearch(document.getElementById('intro'));