4 use List::Util qw(sum max first);
7 title => 'browser compatibility cheat sheet',
10 "Compatibility table of new web features (HTML5, CSS3, SVG, Javascript)",
11 "comparing support and usage share for all popular browser versions.",
14 web browser support compatibility usage available feature
15 html html5 css css3 svg javascript js dom mobile
16 ie internet explorer firefox chrome safari webkit opera
18 stylesheet => [qw'circus dark mono red light'],
19 data => ['browser-support.inc.pl'],
22 say "<h1>Browser compatibility</h1>\n";
24 my $caniuse = do 'browser-support.inc.pl' or die $! || $@;
26 # mark last three (future) versions as unreleased, ensure current isn't
28 $_->[-1] => 0, $_->[-2] => 0, $_->[-3] => 0,
31 } for values %{ $caniuse->{agents} };
52 p => 'plugin required',
53 j => 'javascript required',
56 d => 'disabled by default',
60 (map "-$_-", $caniuse->{agents}->{$_[0]}->{prefix} // ()),
65 unoff => 'l1', # unofficial
67 cr => 'l4', # candidate
68 pr => 'l4', # proposed
69 rec => 'l5', # recommendation
70 other => 'l2', # non-w3
71 ietf => 'l5', # standard
74 if (my ($somerow) = values %{ $caniuse->{data} }) {
75 while (my ($browser, $row) = each %{ $somerow->{stats} }) {
76 $versions{$browser} = [ sort { paddedver($a) cmp paddedver($b) } keys %$row ];
81 <p id="intro">Alternate rendition of Fyrd's <a href="http://caniuse.com/">when can I use...</a> page
83 my ($canihas, $usage);
84 my $minusage = $get{threshold} // .5;
85 given ($get{usage} // 'wm') {
89 when (!/^[a-z][\w-]+$/) {
90 printf "<p>Invalid browser usage data request: <em>%s</em>",
91 'identifier must be alphanumeric name or <q>0</q>';
93 $canihas = do "browser-usage-$_.inc.pl" or do {
94 printf "<p>Browser usage data not found: <em>%s</em>", $! || $@;
98 my $ref = $canihas->{-title} || 'unknown';
99 $ref = sprintf '<a href="%s">%s</a>', $_, $ref
100 for $canihas->{-site} || $canihas->{-source} || ();
101 $ref .= " $_" for $canihas->{-date} || ();
102 print "\nwith $ref browser usage statistics";
106 if ($usage) { # first() does not work inside given >:(
107 # adapt version usage to actual support data
108 my %engineuse; # prefix => usage sum
109 for my $browser (keys %versions) {
110 my $row = $canihas->{$browser} // {};
111 my $verlist = $versions{$browser} or next;
112 if ($minusage and sum(values %$row) < $minusage) {
113 delete $versions{$browser};
116 my %supported = map { $_ => 1 } @$verlist;
118 # cascade unknown versions
119 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
120 while (my ($version, $usage) = each %$row) {
121 next if defined $supported{$version};
122 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
123 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
124 $row->{$next} += $usage;
125 $row->{$version} = 0; # balance browser total
128 # build row list for each version
130 my @vershown; # $verlist replacement
131 my ($rowusage, @verrow) = (0); # replacement row tracking
133 push @verrow, $_; # queue each version
134 if (($rowusage += $row->{$_}) >= $minusage) {
135 push @vershown, [@verrow]; # add row
136 ($rowusage, @verrow) = (0); # reset row tracking
139 push @vershown, \@verrow if @verrow; # always add latest
140 @$verlist = @vershown;
143 @$verlist = map { [$_] } @$verlist;
146 # reusable aggregates (grouped by prefix (engine) and browser)
147 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
148 $row->{-total} = sum(values %$row);
151 # order browser columns by usage grouped by engine
153 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
154 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
156 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
160 # order browser columns by name grouped by engine
162 $caniuse->{agents}->{$b}->{prefix} cmp
163 $caniuse->{agents}->{$a}->{prefix}
176 my $zero = $#$_ - 2; # baseline index
177 ($_->[$zero - 2] => .5), # past
178 ($_->[$zero - 1] => 10 ), # previous
179 ($_->[$zero + 2] => 0 ), # future
180 ($_->[$zero + 1] => .5), # next
181 ($_->[$zero ] => 30 ), # current
182 } $caniuse->{agents}->{$_}->{versions}
185 }; # fallback hash based on release semantics
186 my $usagemax = (max(map { ref $_ eq 'HASH' && sum(values %$_) } values %$canihas) // 1) / 100;
188 my $usagepct = 1; # score multiplier for 0..100 result
189 # normalise usage percentage to only include shown browsers
190 $usagepct = 100.01 / featurescore({ # yes for every possible version
191 map { $_ => { map {$_ => 'y'} map { @{$_} } @{$versions{$_}} } } keys %versions
194 print '<table class="mapped">';
195 print '<col span="3">'; # should match first thead row
196 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
197 say '</colgroup><col>';
199 my $header = join('',
201 '<th colspan="3">feature',
203 my $name = $caniuse->{agents}->{$_}->{browser};
204 sprintf('<th colspan="%d" class="%s" title="%s">%s',
205 scalar @{ $versions{$_} },
206 join(' ', map {"b-a-$_"} grep {$_}
207 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
210 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
214 length $name < 3 + @{ $versions{$_} }*2 ? $name
215 : $caniuse->{agents}->{$_}->{abbr};
221 print '<thead>', $header;
222 # preceding row without any colspan to work around gecko bug
225 for my $browser (@browsers) {
226 for (@{ $versions{$browser} }) {
227 my $lastver = $_->[-1];
228 my $release = $caniuse->{agents}->{$browser}->{verrelease}->{$lastver};
229 my $future = defined $release;
230 printf('<td title="%s"%s>%s',
232 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{@$_}) * $usagepct),
233 $future ? 'development' : (),
234 'version ' . join(', ', @{$_}),
236 $future && ' class="ex"',
237 showversions($lastver),
243 say '<tfoot>', $header, '</tfoot>';
246 # relative amount of support for given feature
247 state $statspts = { y=>1, 'y x'=>1, a=>.5, 'a x'=>.5, j=>.2, 'p j'=>.2, 'p p'=>.2, p=>.1 };
249 if (my $row = shift) {
251 while (my ($browser, $versions) = each %$row) {
252 ref $versions eq 'HASH' or next;
253 while (my ($version, $_) = each %$versions) {
254 $rank += ($canihas->{$browser}->{$version} || .001) * $statspts->{$_};
260 while (my ($browser, $vercols) = each %versions) {
261 my $div = 0; # multiplier exponent (decreased to lower value)
262 my @vers = map { $row->{$browser}->{$_} } @$vercols;
263 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
264 my @future; # find upcoming releases (after current)
265 for (reverse @$vercols) {
266 last if $_ eq $current;
267 push @future, pop @vers;
268 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
270 splice @vers, -1, 0, @future; # move ahead to decrease precedence
272 $rank += $statspts->{$_} * 2**($div--) for reverse @vers;
280 my $row = $caniuse->{data}->{$id};
282 for ($row->{categories}) {
283 my $cell = $_ ? lc $_->[0] : '-';
284 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
285 printf '<th title="%s">%s', join(' + ', @$_), $cell;
289 sprintf('<a href="%s" onclick="%s">%s</a>',
291 sprintf("try { %s; return false } catch(err) { return true }",
292 "document.getElementById('$id').classList.toggle('target')",
297 print '<div class=aside>';
298 s/\.?$/./, print "<p>$_</p>" for map { ref $_ ? @$_ : $_ || () }
299 Entity($row->{description}),
300 map { s/\s*\n/\n<br>/g; $_ } $row->{notes};
301 printf 'Resources: %s.', join(', ', map {
302 sprintf '<a href="%s">%s</a>', EscapeHTML($_->{url}), $_->{title}
303 } @$_) for grep { @$_ } $row->{links} // ();
304 printf '<br>Parent feature: %s.', join(', ', map {
305 sprintf '<a href="%s">%s</a>', EscapeHTML("#$_"), $caniuse->{data}->{$_}->{title}
306 } $_) for $row->{parent} || ();
312 my $row = $caniuse->{data}->{$id};
314 for ($row->{status}) {
315 my $cell = $_ // '-';
316 $cell = sprintf '<a href="%s">%s</a>', $_, $cell for $row->{spec} // ();
317 printf '<td title="%s" class="l %s">%s',
318 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
323 my ($id, $browser) = @_;
324 my $feature = $caniuse->{data}->{$id};
325 my $data = $feature->{stats}->{$browser};
326 if (ref $data eq 'ARRAY') {
327 # special case for unsupported
328 my $release = $caniuse->{agents}->{$browser}->{verrelease};
330 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
335 for my $ver (@{ $versions{$browser} }, undef) {
337 !defined $ver ? undef : # last column if nameless
338 ref $data ne 'HASH' ? '' : # unclassified if no support hash
339 $data->{ $ver->[-1] } // $prev # known or inherit from predecessor
340 // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
341 ~~ 'n' && 'n' # first known version is unsupported
344 unless (!defined $prev or $prev ~~ $compare) {
345 my $usage = sum(@{ $canihas->{$browser} }{ map { @{$_} } @span });
347 # strip #\d note references from support class
349 push @notes, $feature->{notes_by_num}->{$1}
350 while $prev =~ s/\h \# (\d+) \b//x;
352 # prepare version hover details
353 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(', ',
354 map { ref $_ eq 'CODE' ? $_->($browser) : $_ }
355 map { $DSTATS{$_} // () }
356 map { split / /, $_ }
359 $title .= "\n".EscapeHTML($_) for @notes;
361 printf('<td class="%s" colspan="%d" title="%s">%s',
364 !$usage ? ('p0') : ('p',
365 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
366 sprintf('p%02d', $usage * ($usagepct - .0001)),
368 sprintf('pp%02d', $usage / $usagemax),
372 showversions($span[0]->[0], @span > 1 ? ($span[-1]->[-1]) : ()),
377 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
384 print '<td>', int featurescore($caniuse->{data}->{$id}->{stats}) * $usagepct;
389 featurescore($caniuse->{data}->{$b}->{stats})
390 <=> featurescore($caniuse->{data}->{$a}->{stats})
391 } keys %{ $caniuse->{data} }) {
392 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
393 printf '<tr id="%s">', $id;
396 saybrowsercols($id, $_) for @browsers;
404 # normalised version number comparable as string (cmp)
405 shift =~ /(?:.*-|^)(\d*)(.*)/;
406 # matched (major)(.minor) of last value in range (a-B)
407 return sprintf('%02d', $1 || 99) . $2;
411 my @span = ($_[0], @_>1 ? $_[-1] : ());
412 s/-.*// for $span[0];
418 return join('‒', @span);
425 <table class="glyphs"><tr>
426 <td class="X l5">supported
427 <td class="X l3">partial
428 <td class="X l2">optional
429 <td class="X l1">missing
430 <td class="X l0">unknown
431 <td class="X ex">prefixed
434 <p><: if ($usage) { :>
436 <span class=" p0">0</span> -
437 <span class="p p0 p00">.01</span> -
438 <span class="p p0 p05">1-9</span> -
439 <span class="p p1">10</span> -
440 <span class="p p2">20</span> -
441 <span class="p p5">majority</span>
443 <table class="glyphs"><tr>
444 <td class="p p1">previous version</td>
445 <td class="p p3">current</td>
446 <td class="p p0 p00">upcoming (within months)</td>
447 <td class=" p0">future (within a year)</td>
452 <ul class="legend legend-set">
453 <li>default <strong>style</strong> is
454 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
455 <li><strong>usage</strong> source is
456 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
457 <li>usage <strong>threshold</strong> is
458 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
463 <script type="text/javascript" src="/searchlocal.js"></script>
464 <script type="text/javascript"> prependsearch(document.getElementById('intro')) </script>