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 two (future) versions as unreleased, ensure current isn't
27 map { $_->[-1] => 0, $_->[-2] => 0, $_->[-3] => undef } $_->{versions}
28 } for values %{ $caniuse->{agents} };
49 p => 'plugin required',
50 j => 'javascript required',
53 d => 'disabled by default',
57 (map "-$_-", $caniuse->{agents}->{$_[0]}->{prefix} // ()),
62 unoff => 'l1', # unofficial
64 cr => 'l4', # candidate
65 pr => 'l4', # proposed
66 rec => 'l5', # recommendation
67 other => 'l2', # non-w3
68 ietf => 'l5', # standard
71 if (my ($somerow) = values %{ $caniuse->{data} }) {
72 while (my ($browser, $row) = each %{ $somerow->{stats} }) {
73 $versions{$browser} = [ sort { paddedver($a) cmp paddedver($b) } keys %$row ];
76 my @browsers = keys %versions;
79 <p id="intro">Alternate rendition of Fyrd's <a href="http://caniuse.com/">when can I use...</a> page
81 my ($canihas, $usage, $minusage);
82 given ($get{usage} // 'wm') {
86 when (!/^[a-z][\w-]+$/) {
87 printf "<p>Invalid browser usage data request: <em>%s</em>",
88 'identifier must be alphanumeric name or <q>0</q>';
90 $canihas = do "browser-usage-$_.inc.pl" or do {
91 printf "<p>Browser usage data not found: <em>%s</em>", $! || $@;
95 my $ref = $canihas->{-title} || 'unknown';
96 $ref = sprintf '<a href="%s">%s</a>', $_, $ref
97 for $canihas->{-site} || $canihas->{-source} || ();
98 $ref .= " $_" for $canihas->{-date} || ();
99 print "\nwith $ref browser usage statistics";
101 if ($usage) { # first() does not work inside given >:(
102 # adapt version usage to actual support data
103 my %engineuse; # prefix => usage sum
104 for my $browser (keys %versions) {
105 my $row = $canihas->{$browser} // {};
106 my $verlist = $versions{$browser} or next;
107 my %supported = map { $_ => 1 } @$verlist;
109 # cascade unknown versions
110 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
111 while (my ($version, $usage) = each %$row) {
112 next if defined $supported{$version};
113 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
114 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
115 $row->{$next} += $usage;
116 $row->{$version} = 0; # balance browser total
119 # build row list for each version
120 if ($minusage = $get{threshold} // .5) {
121 my @vershown; # $verlist replacement
122 my ($rowusage, @verrow) = (0); # replacement row tracking
124 push @verrow, $_; # queue each version
125 if (($rowusage += $row->{$_}) >= $minusage) {
126 push @vershown, [@verrow]; # add row
127 ($rowusage, @verrow) = (0); # reset row tracking
130 push @vershown, \@verrow if @verrow; # always add latest
131 @$verlist = @vershown;
134 @$verlist = map { [$_] } @$verlist;
137 # reusable aggregates (grouped by prefix (engine) and browser)
138 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
139 $row->{-total} = sum(values %$row);
142 # order browser columns by usage grouped by engine
144 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
145 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
147 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
151 # order browser columns by name grouped by engine
153 $caniuse->{agents}->{$b}->{prefix} cmp
154 $caniuse->{agents}->{$a}->{prefix}
167 my $zero = $#$_ - 2; # baseline index
168 ($_->[$zero - 2] => .5), # past
169 ($_->[$zero - 1] => 10 ), # previous
170 ($_->[$zero + 2] => 0 ), # future
171 ($_->[$zero + 1] => .5), # next
172 ($_->[$zero ] => 30 ), # current
173 } $caniuse->{agents}->{$_}->{versions}
176 }; # fallback hash based on release semantics
177 my $usagemax = (max(map { ref $_ eq 'HASH' && sum(values %$_) } values %$canihas) // 1) / 100;
179 my $usagepct = 1; # score multiplier for 0..100 result
180 # normalise usage percentage to only include shown browsers
181 $usagepct = 100.01 / featurescore({ # yes for every possible version
182 map { $_ => { map {$_ => 'y'} map { @{$_} } @{$versions{$_}} } } keys %versions
185 print '<table class="mapped">';
186 print '<col span="3">'; # should match first thead row
187 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
188 say '</colgroup><col>';
190 my $header = join('',
192 '<th colspan="3">feature',
194 my $name = $caniuse->{agents}->{$_}->{browser};
195 sprintf('<th colspan="%d" class="%s" title="%s">%s',
196 scalar @{ $versions{$_} },
197 join(' ', map {"b-a-$_"} grep {$_}
198 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
201 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
205 length $name < 3 + @{ $versions{$_} }*2 ? $name
206 : $caniuse->{agents}->{$_}->{abbr};
212 print '<thead>', $header;
213 # preceding row without any colspan to work around gecko bug
216 for my $browser (@browsers) {
217 for (@{ $versions{$browser} }) {
218 my $lastver = $_->[-1];
219 my $release = $caniuse->{agents}->{$browser}->{verrelease}->{$lastver};
220 my $future = defined $release;
221 printf('<td title="%s"%s>%s',
223 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{@$_}) * $usagepct),
224 $future ? 'development' : (),
225 'version ' . join(', ', @{$_}),
227 $future && ' class="ex"',
228 showversions($lastver),
234 say '<tfoot>', $header, '</tfoot>';
237 # relative amount of support for given feature
238 state $statspts = { y=>1, 'y x'=>1, a=>.5, 'a x'=>.5, j=>.2, 'p j'=>.2, 'p p'=>.2, p=>.1 };
240 if (my $row = shift) {
242 while (my ($browser, $versions) = each %$row) {
243 ref $versions eq 'HASH' or next;
244 while (my ($version, $_) = each %$versions) {
245 $rank += ($canihas->{$browser}->{$version} || .001) * $statspts->{$_};
251 while (my ($browser, $vercols) = each %versions) {
252 my $div = 0; # multiplier exponent (decreased to lower value)
253 my @vers = map { $row->{$browser}->{$_} } @$vercols;
254 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
255 my @future; # find upcoming releases (after current)
256 for (reverse @$vercols) {
257 last if $_ eq $current;
258 push @future, pop @vers;
259 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
261 splice @vers, -1, 0, @future; # move ahead to decrease precedence
263 $rank += $statspts->{$_} * 2**($div--) for reverse @vers;
271 my $row = $caniuse->{data}->{$id};
273 for ($row->{categories}) {
274 my $cell = $_ ? lc $_->[0] : '-';
275 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
276 printf '<th title="%s">%s', join(' + ', @$_), $cell;
280 sprintf('<a href="%s" onclick="%s">%s</a>',
282 sprintf("try { %s; return false } catch(err) { return true }",
283 "document.getElementById('$id').classList.toggle('target')",
288 print '<div class=aside>';
289 s/\.?$/./, print "<p>$_</p>" for map { ref $_ ? @$_ : $_ || () }
290 Entity($row->{description}),
291 map { s/\s*\n/\n<br>/g; $_ } $row->{notes};
292 printf 'Resources: %s.', join(', ', map {
293 sprintf '<a href="%s">%s</a>', EscapeHTML($_->{url}), $_->{title}
294 } @$_) for grep { @$_ } $row->{links} // ();
295 printf '<br>Parent feature: %s.', join(', ', map {
296 sprintf '<a href="%s">%s</a>', EscapeHTML("#$_"), $caniuse->{data}->{$_}->{title}
297 } $_) for $row->{parent} || ();
303 my $row = $caniuse->{data}->{$id};
305 for ($row->{status}) {
306 my $cell = $_ // '-';
307 $cell = sprintf '<a href="%s">%s</a>', $_, $cell for $row->{spec} // ();
308 printf '<td title="%s" class="l %s">%s',
309 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
314 my ($id, $browser) = @_;
315 my $feature = $caniuse->{data}->{$id};
316 my $data = $feature->{stats}->{$browser};
317 if (ref $data eq 'ARRAY') {
318 # special case for unsupported
319 my $release = $caniuse->{agents}->{$browser}->{verrelease};
321 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
326 for my $ver (@{ $versions{$browser} }, undef) {
328 !defined $ver ? undef : # last column if nameless
329 ref $data ne 'HASH' ? '' : # unclassified if no support hash
330 $data->{ $ver->[-1] } // $prev # known or inherit from predecessor
331 // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
332 ~~ 'n' && 'n' # first known version is unsupported
335 unless (!defined $prev or $prev ~~ $compare) {
336 my $usage = sum(@{ $canihas->{$browser} }{ map { @{$_} } @span });
338 # strip #\d note references from support class
340 push @notes, $feature->{notes_by_num}->{$1}
341 while $prev =~ s/\h \# (\d+) \b//x;
343 # prepare version hover details
344 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(', ',
345 map { ref $_ eq 'CODE' ? $_->($browser) : $_ }
346 map { $DSTATS{$_} // () }
347 map { split / /, $_ }
350 $title .= "\n".EscapeHTML($_) for @notes;
352 printf('<td class="%s" colspan="%d" title="%s">%s',
355 !$usage ? ('p0') : ('p',
356 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
357 sprintf('p%02d', $usage * ($usagepct - .0001)),
359 sprintf('pp%02d', $usage / $usagemax),
363 showversions($span[0]->[0], @span > 1 ? ($span[-1]->[-1]) : ()),
368 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
375 print '<td>', int featurescore($caniuse->{data}->{$id}->{stats}) * $usagepct;
380 featurescore($caniuse->{data}->{$b}->{stats})
381 <=> featurescore($caniuse->{data}->{$a}->{stats})
382 } keys %{ $caniuse->{data} }) {
383 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
384 printf '<tr id="%s">', $id;
387 saybrowsercols($id, $_) for @browsers;
395 # normalised version number comparable as string (cmp)
396 shift =~ /(?:.*-|^)(\d*)(.*)/;
397 # matched (major)(.minor) of last value in range (a-B)
398 return sprintf('%02d', $1 || 99) . $2;
402 my @span = ($_[0], @_>1 ? $_[-1] : ());
403 s/-.*// for $span[0];
409 return join('‒', @span);
416 <table class="glyphs"><tr>
417 <td class="X l5">supported
418 <td class="X l3">partial
419 <td class="X l2">optional
420 <td class="X l1">missing
421 <td class="X l0">unknown
422 <td class="X ex">prefixed
425 <p><: if ($usage) { :>
427 <span class=" p0">0</span> -
428 <span class="p p0 p00">.01</span> -
429 <span class="p p0 p05">1-9</span> -
430 <span class="p p1">10</span> -
431 <span class="p p2">20</span> -
432 <span class="p p5">majority</span>
434 <table class="glyphs"><tr>
435 <td class="p p1">previous version</td>
436 <td class="p p3">current</td>
437 <td class="p p0 p00">upcoming (within months)</td>
438 <td class=" p0">future (within a year)</td>
443 <ul class="legend legend-set">
444 <li>default <strong>style</strong> is
445 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
446 <li><strong>usage</strong> source is
447 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
448 <li>usage <strong>threshold</strong> is
449 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
454 <script type="text/javascript" src="/searchlocal.js"></script>
455 <script type="text/javascript"> prependsearch(document.getElementById('intro')) </script>