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 $@ || $!;
25 # mark last three (future) versions as unreleased, ensure current isn't
27 $_->[-1] => 0, $_->[-2] => 0, $_->[-3] => 0,
30 } for values %{ $caniuse->{agents} };
48 p => 'plugin required',
51 d => '(disabled by default)',
56 ($caniuse->{agents}->{$_[0]}->{prefix_exceptions} // {})->{$_[1]}
57 // $caniuse->{agents}->{$_[0]}->{prefix} // (),
61 my %PSTATS = ( # score percentage
63 a => .5, 'a x' => .5, 'a d' => .2,
65 n => 0, 'n d' => .2, 'n x d' => .2,
69 unoff => 'l1', # unofficial
71 cr => 'l3', # candidate
72 pr => 'l3', # proposed
73 rec => 'l5', # recommendation
75 ietf => 'l0', # standard
76 other => 'l0', # non-w3
79 while (my ($browser, $row) = each %{ $caniuse->{agents} }) {
80 $versions{$browser} = [
81 sort { paddedver($a) cmp paddedver($b) } grep { defined }
86 my $ref = showlink('Can I use', 'https://caniuse.com/');
87 $ref =~ s/(?=>)/ title="updated $_"/
88 for map { s/[\sT].*//r } $caniuse->{-date} || ();
89 $ref = "Fyrd's $ref page";
90 say '<p id="intro">Alternate rendition of '.$ref;
92 my ($canihas, $usage);
93 my $minusage = $get{threshold} // 1;
94 given ($get{usage} // 'wm') {
98 when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
100 'Invalid browser usage data request',
101 'Identifier must be alphanumeric name or <q>0</q>.',
104 $canihas = do "data/browser/usage-$_.inc.pl" or do {
105 Alert('Browser usage data not found', $@ || $!);
109 my $ref = $canihas->{-title} || 'unknown';
110 $ref = showlink($ref, $_)
111 for $canihas->{-site} || $canihas->{-source} || ();
112 $ref =~ s/(?=>)/ title="updated $_"/ for $canihas->{-date} || ();
113 print "\nwith $ref browser usage statistics";
117 if ($usage) { # first() does not work inside given >:(
118 # adapt version usage to actual support data
119 my %engineuse; # prefix => usage sum
120 for my $browser (keys %versions) {
121 my $row = $canihas->{$browser} // {};
122 my $verlist = $versions{$browser} or next;
123 if ($minusage and sum(values %$row) < $minusage) {
124 delete $versions{$browser};
127 my %supported = map { $_ => 1 } @$verlist;
129 # cascade unknown versions
130 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
131 while (my ($version, $usage) = each %$row) {
132 next if defined $supported{$version};
133 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
134 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
135 $row->{$next} += $usage;
136 $row->{$version} = 0; # balance browser total
139 # build row list for each version
141 my @vershown; # $verlist replacement
142 my ($rowusage, @verrow) = (0); # replacement row tracking
144 push @verrow, $_; # queue each version
145 if (($rowusage += $row->{$_}) >= $minusage) {
146 push @vershown, [@verrow]; # add row
147 ($rowusage, @verrow) = (0); # reset row tracking
150 push @vershown, \@verrow if @verrow; # always add latest
151 @$verlist = @vershown;
154 @$verlist = map { [$_] } @$verlist;
157 # reusable aggregates (grouped by prefix (engine) and browser)
158 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
159 $row->{-total} = sum(values %$row);
162 # order browser columns by usage grouped by engine
164 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
165 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
167 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
171 # order browser columns by name grouped by engine
172 @{$_} = map { [$_] } @{$_} for values %versions;
174 $caniuse->{agents}->{$b}->{prefix} cmp
175 $caniuse->{agents}->{$a}->{prefix}
188 my $zero = $#$_ - 2; # baseline index
189 ($_->[$zero - 2] => .5), # past
190 ($_->[$zero - 1] => 10 ), # previous
191 ($_->[$zero + 2] => 0 ), # future
192 ($_->[$zero + 1] => .5), # next
193 ($_->[$zero ] => 30 ), # current
194 } $caniuse->{agents}->{$_}->{versions}
197 }; # fallback hash based on release semantics
199 # score multiplier for percentage of all browser versions
200 my $usagepct = 99.99 / sum(
201 map { $_->{-total} // values %{$_} } values %{$canihas}
204 $_->{usage} = featurescore($_->{stats}) * $usagepct
205 for values %{ $caniuse->{data} };
207 print '<table class="mapped">';
208 print '<col span="3">'; # should match first thead row
209 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
210 say '</colgroup><col>';
212 my $header = join('',
214 '<th colspan="3" rowspan="2">feature',
216 my $name = $caniuse->{agents}->{$_}->{browser};
217 sprintf('<th colspan="%d" class="%s" title="%s">%s',
218 scalar @{ $versions{$_} },
219 join(' ', map {"b-a-$_"} grep {$_}
220 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
223 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
227 length $name <= (3 * @{ $versions{$_} }) ? $name
228 : $caniuse->{agents}->{$_}->{abbr};
234 print '<thead>', $header;
235 # preceding row without any colspan to work around gecko bug
237 for my $browser (@browsers) {
238 for my $span (@{ $versions{$browser} }) {
239 my $lastver = first {
240 !defined $caniuse->{agents}->{$browser}->{verrelease}->{$_} # stable
242 printf('<td title="%s"%s>%s',
244 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{ @{$span} }) * $usagepct),
245 'version ' . showversions(@{$span}, undef),
246 $span->[-1] eq $lastver ? () : '(development)',
248 !defined $lastver && ' class="ex"',
249 showversions($lastver // $span->[0]),
254 say '<tfoot>', $header;
256 # prefix indicates browser family; count adjacent families
257 my (@families, %familycount);
258 for my $browser (@browsers) {
259 my $family = $caniuse->{agents}->{$browser}->{prefix};
260 push @families, $family unless $familycount{$family};
261 $familycount{$family} += @{ $versions{$browser} };
264 print "\n", '<tr class="cat">';
265 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
270 # relative amount of support for given feature
272 if (my $row = shift) {
274 while (my ($browser, $versions) = each %$row) {
275 ref $versions eq 'HASH' or next;
276 while (my ($version, $status) = each %$versions) {
277 $status =~ s/\h\#\d+//g;
278 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
284 while (my ($browser, $vercols) = each %versions) {
285 my $div = 0; # multiplier exponent (decreased to lower value)
286 my @vers = map { $row->{$browser}->{$_} } @$vercols;
287 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
288 my @future; # find upcoming releases (after current)
289 for (reverse @$vercols) {
290 last if $_ eq $current;
291 push @future, pop @vers;
292 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
294 splice @vers, -1, 0, @future; # move ahead to decrease precedence
296 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
305 s/\r\n?/\n/g; # windows returns
306 s/\h* $//gmx; # trailing whitespace
307 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
309 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
310 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
319 s{ \[ ([^]]*) \] \( [^)]* \) }{$1}gx; # strip link urls
326 my $row = $caniuse->{data}->{$id};
328 for ($row->{categories}) {
329 my $cell = $_ ? lc $_->[0] : '-';
330 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
331 printf '<th title="%s">%s', join(' + ', @$_), $cell;
335 sprintf('<a href="%s" onclick="%s">%s</a>',
337 sprintf("try { %s; return false } catch(err) { return true }",
338 "document.getElementById('$id').classList.toggle('target')",
343 print '<div class=aside>';
345 for formatnotes($row->{description}, $row->{notes} || ());
346 if (my %notes = %{ $row->{notes_by_num} }) {
347 say '<p>Browser-specific notes:';
348 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
351 printf 'Resources: %s.', join(', ', map {
352 showlink($_->{title}, $_->{url})
353 } @$_) for grep { @$_ } $row->{links} // ();
354 printf '<br>Parent feature: %s.', join(', ', map {
355 showlink($caniuse->{data}->{$_}->{title}, "#$_")
356 } $_) for $row->{parent} || ();
362 my $row = $caniuse->{data}->{$id};
364 for ($row->{status}) {
365 my $cell = $_ // '-';
366 $cell = showlink($cell, $_) for $row->{spec} // ();
367 printf '<td title="%s" class="l %s">%s',
368 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
373 my ($id, $browser) = @_;
374 my $feature = $caniuse->{data}->{$id};
375 my $data = $feature->{stats}->{$browser};
376 if (ref $data eq 'ARRAY') {
377 # special case for unsupported
378 my $release = $caniuse->{agents}->{$browser}->{verrelease};
380 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
385 for my $ver (@{ $versions{$browser} }, undef) {
387 !defined $ver ? undef : # last column if nameless
388 ref $data ne 'HASH' ? '' : # unclassified if no support hash
389 $data->{ $ver->[-1] } // $prev # known or inherit from predecessor
390 // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
391 ~~ 'n' && 'n' # first known version is unsupported
394 unless (!defined $prev or $prev ~~ $compare) {
395 my @vercover = (map { @{$_} } @span);
396 for ($ver ? @{$ver} : ()) {
397 $data->{$_} eq $data->{$vercover[-1]} or last;
398 push @vercover, $_; # matches from next span start
400 my $usage = sum(@{ $canihas->{$browser} }{@vercover});
402 # strip #\d note references from support class
404 push @notes, $feature->{notes_by_num}->{$1}
405 while $prev =~ s/\h \# (\d+) \b//x;
407 # prepare version hover details
408 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(' ',
409 (map { ref $_ eq 'CODE' ? $_->($browser, $vercover[0]) : $_ }
410 map { $DSTATS{$_} // () }
411 map { split / /, $_ }
414 'in', $caniuse->{agents}->{$browser}->{abbr},
415 showversions(@vercover, undef),
417 $title .= "\n$_" for notestotitle(@notes);
419 printf('<td class="%s" colspan="%d" title="%s">%s',
422 !$usage ? ('p0') : ('p',
423 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
424 sprintf('p%02d', $usage * ($usagepct - .0001)),
429 showversions($span[0]->[0], @span > 1 && defined $ver ? $span[-1]->[-1] : ()),
434 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
441 print '<td>', int $caniuse->{data}->{$id}->{usage};
446 $caniuse->{data}->{$b}->{usage} <=> $caniuse->{data}->{$a}->{usage}
447 } keys %{ $caniuse->{data} }) {
448 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
449 printf '<tr id="%s">', $id;
452 saybrowsercols($id, $_) for @browsers;
460 # normalised version number comparable as string (cmp)
461 $_[0] =~ m/(?:.*-|^)(\d*)(.*)/;
462 # matched (major)(.minor) of last value in range (a-B)
463 return sprintf('%02d', length $1 ? $1 : 99) . $2;
467 # title to describe minumum version and optional maximum for multiple cells
468 my @span = (map { split /-/ } grep { defined } @_);
469 return $span[0] =~ s/\.0\z//r if @_ <= 1;
471 return join('‒', @span);
478 <table class="glyphs"><tr>
479 <td class="X l5">supported
480 <td class="X l3">partial
481 <td class="X l2">optional
482 <td class="X l1">missing
483 <td class="X l0">unknown
484 <td class="X ex">prefixed
487 <p><: if ($usage) { :>
489 <span class=" p0">0</span> -
490 <span class="p p0 p00">.01</span> -
491 <span class="p p0 p05">1-9</span> -
492 <span class="p p1">10</span> -
493 <span class="p p2">20</span> -
494 <span class="p p5">majority</span>
496 <table class="glyphs"><tr>
497 <td class="p p1">previous version</td>
498 <td class="p p3">current</td>
499 <td class="p p0 p00">upcoming (within months)</td>
500 <td class=" p0">future (within a year)</td>
505 <ul class="legend legend-set">
506 <li>default <strong>style</strong> is
507 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
508 <li><strong>usage</strong> source is
509 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
510 <li>usage <strong>threshold</strong> is
511 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
516 <script type="text/javascript" src="/searchlocal.js"></script>
517 <script type="text/javascript"><!--
518 prependsearch(document.getElementById('intro'));