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} };
51 p => 'plugin required',
52 j => 'javascript required',
55 d => 'disabled by default',
60 ($caniuse->{agents}->{$_[0]}->{prefix_exceptions} // {})->{$_[1]}
61 // $caniuse->{agents}->{$_[0]}->{prefix} // (),
65 my %PSTATS = ( # score percentage
67 a => .5, 'a x' => .5, 'a d' => .1,
68 j => .2, 'p j' => .2, 'n d' => .2, 'n x d' => .2,
69 p => .2, 'p p' => .2, 'p d' => .1,
73 unoff => 'l1', # unofficial
75 cr => 'l3', # candidate
76 pr => 'l3', # proposed
77 rec => 'l5', # recommendation
79 ietf => 'l0', # standard
80 other => 'l0', # non-w3
83 while (my ($browser, $row) = each %{ $caniuse->{agents} }) {
84 $versions{$browser} = [
85 sort { paddedver($a) cmp paddedver($b) } grep { defined }
91 <p id="intro">Alternate rendition of Fyrd's <a href="http://caniuse.com/">when can I use...</a> page
93 my ($canihas, $usage);
94 my $minusage = $get{threshold} // 1;
95 given ($get{usage} // 'wm') {
99 when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
101 'Invalid browser usage data request',
102 'Identifier must be alphanumeric name or <q>0</q>.',
105 $canihas = do "data/browser/usage-$_.inc.pl" or do {
106 Alert('Browser usage data not found', $@ || $!);
110 my $ref = $canihas->{-title} || 'unknown';
111 $ref = showlink($ref, $_)
112 for $canihas->{-site} || $canihas->{-source} || ();
113 $ref .= " $_" for $canihas->{-date} || ();
114 print "\nwith $ref browser usage statistics";
118 if ($usage) { # first() does not work inside given >:(
119 # adapt version usage to actual support data
120 my %engineuse; # prefix => usage sum
121 for my $browser (keys %versions) {
122 my $row = $canihas->{$browser} // {};
123 my $verlist = $versions{$browser} or next;
124 if ($minusage and sum(values %$row) < $minusage) {
125 delete $versions{$browser};
128 my %supported = map { $_ => 1 } @$verlist;
130 # cascade unknown versions
131 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
132 while (my ($version, $usage) = each %$row) {
133 next if defined $supported{$version};
134 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
135 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
136 $row->{$next} += $usage;
137 $row->{$version} = 0; # balance browser total
140 # build row list for each version
142 my @vershown; # $verlist replacement
143 my ($rowusage, @verrow) = (0); # replacement row tracking
145 push @verrow, $_; # queue each version
146 if (($rowusage += $row->{$_}) >= $minusage) {
147 push @vershown, [@verrow]; # add row
148 ($rowusage, @verrow) = (0); # reset row tracking
151 push @vershown, \@verrow if @verrow; # always add latest
152 @$verlist = @vershown;
155 @$verlist = map { [$_] } @$verlist;
158 # reusable aggregates (grouped by prefix (engine) and browser)
159 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
160 $row->{-total} = sum(values %$row);
163 # order browser columns by usage grouped by engine
165 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
166 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
168 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
172 # order browser columns by name grouped by engine
173 @{$_} = map { [$_] } @{$_} for values %versions;
175 $caniuse->{agents}->{$b}->{prefix} cmp
176 $caniuse->{agents}->{$a}->{prefix}
189 my $zero = $#$_ - 2; # baseline index
190 ($_->[$zero - 2] => .5), # past
191 ($_->[$zero - 1] => 10 ), # previous
192 ($_->[$zero + 2] => 0 ), # future
193 ($_->[$zero + 1] => .5), # next
194 ($_->[$zero ] => 30 ), # current
195 } $caniuse->{agents}->{$_}->{versions}
198 }; # fallback hash based on release semantics
200 my $usagepct = 1; # score multiplier for 0..100 result
201 # normalise usage percentage to only include shown browsers
202 $usagepct = 100.01 / featurescore({ # yes for every possible version
203 map { $_ => { map {$_ => 'y'} map { @{$_} } @{$versions{$_}} } } keys %versions
205 $_->{usage} = featurescore($_->{stats}) * $usagepct
206 for values %{ $caniuse->{data} };
208 print '<table class="mapped">';
209 print '<col span="3">'; # should match first thead row
210 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
211 say '</colgroup><col>';
213 my $header = join('',
215 '<th colspan="3" rowspan="2">feature',
217 my $name = $caniuse->{agents}->{$_}->{browser};
218 sprintf('<th colspan="%d" class="%s" title="%s">%s',
219 scalar @{ $versions{$_} },
220 join(' ', map {"b-a-$_"} grep {$_}
221 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
224 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
228 length $name <= (3 * @{ $versions{$_} }) ? $name
229 : $caniuse->{agents}->{$_}->{abbr};
235 print '<thead>', $header;
236 # preceding row without any colspan to work around gecko bug
238 for my $browser (@browsers) {
239 for (@{ $versions{$browser} }) {
240 my $lastver = first {
241 !defined $caniuse->{agents}->{$browser}->{verrelease}->{$_} # stable
243 printf('<td title="%s"%s>%s',
245 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{@$_}) * $usagepct),
246 'version ' . showversions(@{$_}, undef),
247 $_->[-1] eq $lastver ? () : '(development)',
249 !defined $lastver && ' class="ex"',
250 showversions($lastver),
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;
277 while (my ($version, $status) = each %$versions) {
278 $status =~ s/\h\#\d+//g;
279 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
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
295 splice @vers, -1, 0, @future; # move ahead to decrease precedence
297 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
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
310 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
311 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
320 s{ \[ ([^]]*) \] \( [^)]* \) }{$1}gx; # strip link urls
327 my $row = $caniuse->{data}->{$id};
329 for ($row->{categories}) {
330 my $cell = $_ ? lc $_->[0] : '-';
331 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
332 printf '<th title="%s">%s', join(' + ', @$_), $cell;
336 sprintf('<a href="%s" onclick="%s">%s</a>',
338 sprintf("try { %s; return false } catch(err) { return true }",
339 "document.getElementById('$id').classList.toggle('target')",
344 print '<div class=aside>';
346 for formatnotes($row->{description}, $row->{notes} || ());
347 if (my %notes = %{ $row->{notes_by_num} }) {
348 say '<p>Browser-specific notes:';
349 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
352 printf 'Resources: %s.', join(', ', map {
353 showlink($_->{title}, $_->{url})
354 } @$_) for grep { @$_ } $row->{links} // ();
355 printf '<br>Parent feature: %s.', join(', ', map {
356 showlink($caniuse->{data}->{$_}->{title}, "#$_")
357 } $_) for $row->{parent} || ();
363 my $row = $caniuse->{data}->{$id};
365 for ($row->{status}) {
366 my $cell = $_ // '-';
367 $cell = showlink($cell, $_) for $row->{spec} // ();
368 printf '<td title="%s" class="l %s">%s',
369 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
374 my ($id, $browser) = @_;
375 my $feature = $caniuse->{data}->{$id};
376 my $data = $feature->{stats}->{$browser};
377 if (ref $data eq 'ARRAY') {
378 # special case for unsupported
379 my $release = $caniuse->{agents}->{$browser}->{verrelease};
381 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
386 for my $ver (@{ $versions{$browser} }, undef) {
388 !defined $ver ? undef : # last column if nameless
389 ref $data ne 'HASH' ? '' : # unclassified if no support hash
390 $data->{ $ver->[-1] } // $prev # known or inherit from predecessor
391 // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
392 ~~ 'n' && 'n' # first known version is unsupported
395 unless (!defined $prev or $prev ~~ $compare) {
396 my $usage = sum(@{ $canihas->{$browser} }{ map { @{$_} } @span });
398 # strip #\d note references from support class
400 push @notes, $feature->{notes_by_num}->{$1}
401 while $prev =~ s/\h \# (\d+) \b//x;
403 # prepare version hover details
404 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(', ',
405 map { ref $_ eq 'CODE' ? $_->($browser, $span[0]->[0]) : $_ }
406 map { $DSTATS{$_} // () }
407 map { split / /, $_ }
410 $title .= "\n$_" for notestotitle(@notes);
412 printf('<td class="%s" colspan="%d" title="%s">%s',
415 !$usage ? ('p0') : ('p',
416 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
417 sprintf('p%02d', $usage * ($usagepct - .0001)),
422 showversions($span[0]->[0], @span > 1 ? $span[-1]->[-1] : ()),
427 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
434 print '<td>', int $caniuse->{data}->{$id}->{usage};
439 $caniuse->{data}->{$b}->{usage} <=> $caniuse->{data}->{$a}->{usage}
440 } keys %{ $caniuse->{data} }) {
441 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
442 printf '<tr id="%s">', $id;
445 saybrowsercols($id, $_) for @browsers;
453 # normalised version number comparable as string (cmp)
454 shift =~ /(?:.*-|^)(\d*)(.*)/;
455 # matched (major)(.minor) of last value in range (a-B)
456 return sprintf('%02d', $1 || 99) . $2;
460 # title to describe minumum version and optional maximum for multiple cells
461 my @span = (map { split /-/ } grep { defined } @_);
462 return $span[0] if @_ <= 1;
464 return join('‒', @span);
471 <table class="glyphs"><tr>
472 <td class="X l5">supported
473 <td class="X l3">partial
474 <td class="X l2">optional
475 <td class="X l1">missing
476 <td class="X l0">unknown
477 <td class="X ex">prefixed
480 <p><: if ($usage) { :>
482 <span class=" p0">0</span> -
483 <span class="p p0 p00">.01</span> -
484 <span class="p p0 p05">1-9</span> -
485 <span class="p p1">10</span> -
486 <span class="p p2">20</span> -
487 <span class="p p5">majority</span>
489 <table class="glyphs"><tr>
490 <td class="p p1">previous version</td>
491 <td class="p p3">current</td>
492 <td class="p p0 p00">upcoming (within months)</td>
493 <td class=" p0">future (within a year)</td>
498 <ul class="legend legend-set">
499 <li>default <strong>style</strong> is
500 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
501 <li><strong>usage</strong> source is
502 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
503 <li>usage <strong>threshold</strong> is
504 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
509 <script type="text/javascript" src="/searchlocal.js"></script>
510 <script type="text/javascript"> prependsearch(document.getElementById('intro')) </script>