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 matrix 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 => ['data/browser/support.inc.pl'],
22 say "<h1>Browser compatibility</h1>\n";
24 my $caniuse = do 'data/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} // ()),
64 my %PSTATS = ( # score percentage
66 a => .5, 'a x' => .5, 'a d' => .1,
67 j => .2, 'p j' => .2, 'n d' => .2, 'n x d' => .2,
68 p => .2, 'p p' => .2, 'p d' => .1,
71 unoff => 'l1', # unofficial
73 cr => 'l3', # candidate
74 pr => 'l3', # proposed
75 rec => 'l5', # recommendation
77 ietf => 'l0', # standard
78 other => 'l0', # non-w3
81 if (my ($somerow) = values %{ $caniuse->{data} }) {
82 while (my ($browser, $row) = each %{ $somerow->{stats} }) {
83 $versions{$browser} = [ sort { paddedver($a) cmp paddedver($b) } keys %$row ];
88 <p id="intro">Alternate rendition of Fyrd's <a href="http://caniuse.com/">when can I use...</a> page
90 my ($canihas, $usage);
91 my $minusage = $get{threshold} // .7;
92 given ($get{usage} // 'wm') {
96 when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
97 printf "<p>Invalid browser usage data request: <em>%s</em>",
98 'identifier must be alphanumeric name or <q>0</q>';
100 $canihas = do "data/browser/usage-$_.inc.pl" or do {
101 printf "<p>Browser usage data not found: <em>%s</em>", $! || $@;
105 my $ref = $canihas->{-title} || 'unknown';
106 $ref = sprintf '<a href="%s">%s</a>', $_, $ref
107 for $canihas->{-site} || $canihas->{-source} || ();
108 $ref .= " $_" 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
194 my $usagemax = (max(map { ref $_ eq 'HASH' && sum(values %$_) } values %$canihas) // 1) / 100;
196 my $usagepct = 1; # score multiplier for 0..100 result
197 # normalise usage percentage to only include shown browsers
198 $usagepct = 100.01 / featurescore({ # yes for every possible version
199 map { $_ => { map {$_ => 'y'} map { @{$_} } @{$versions{$_}} } } keys %versions
202 print '<table class="mapped">';
203 print '<col span="3">'; # should match first thead row
204 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
205 say '</colgroup><col>';
207 my $header = join('',
209 '<th colspan="3" rowspan="2">feature',
211 my $name = $caniuse->{agents}->{$_}->{browser};
212 sprintf('<th colspan="%d" class="%s" title="%s">%s',
213 scalar @{ $versions{$_} },
214 join(' ', map {"b-a-$_"} grep {$_}
215 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
218 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
222 length $name <= (3 * @{ $versions{$_} }) ? $name
223 : $caniuse->{agents}->{$_}->{abbr};
229 print '<thead>', $header;
230 # preceding row without any colspan to work around gecko bug
232 for my $browser (@browsers) {
233 for (@{ $versions{$browser} }) {
234 my $lastver = $_->[-1];
235 my $release = $caniuse->{agents}->{$browser}->{verrelease}->{$lastver};
236 my $future = defined $release;
237 printf('<td title="%s"%s>%s',
239 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{@$_}) * $usagepct),
240 $future ? 'development' : (),
241 'version ' . join(', ', @{$_}),
243 $future && ' class="ex"',
244 showversions($lastver),
249 say '<tfoot>', $header;
251 # prefix indicates browser family; count adjacent families
252 my (@families, %familycount);
253 for my $browser (@browsers) {
254 my $family = $caniuse->{agents}->{$browser}->{prefix};
255 push @families, $family unless $familycount{$family};
256 $familycount{$family} += @{ $versions{$browser} };
259 print "\n", '<tr class="cat">';
260 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
265 # relative amount of support for given feature
267 if (my $row = shift) {
269 while (my ($browser, $versions) = each %$row) {
270 ref $versions eq 'HASH' or next;
271 while (my ($version, $_) = each %$versions) {
272 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$_};
278 while (my ($browser, $vercols) = each %versions) {
279 my $div = 0; # multiplier exponent (decreased to lower value)
280 my @vers = map { $row->{$browser}->{$_} } @$vercols;
281 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
282 my @future; # find upcoming releases (after current)
283 for (reverse @$vercols) {
284 last if $_ eq $current;
285 push @future, pop @vers;
286 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
288 splice @vers, -1, 0, @future; # move ahead to decrease precedence
290 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
299 s/\h* $//gmx; # trailing whitespace
300 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
302 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
303 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
310 my $row = $caniuse->{data}->{$id};
312 for ($row->{categories}) {
313 my $cell = $_ ? lc $_->[0] : '-';
314 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
315 printf '<th title="%s">%s', join(' + ', @$_), $cell;
319 sprintf('<a href="%s" onclick="%s">%s</a>',
321 sprintf("try { %s; return false } catch(err) { return true }",
322 "document.getElementById('$id').classList.toggle('target')",
327 print '<div class=aside>';
329 for formatnotes($row->{description}, $row->{notes} || ());
330 if (my %notes = %{ $row->{notes_by_num} }) {
331 say '<p>Browser-specific notes:';
332 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
335 printf 'Resources: %s.', join(', ', map {
336 sprintf '<a href="%s">%s</a>', EscapeHTML($_->{url}), $_->{title}
337 } @$_) for grep { @$_ } $row->{links} // ();
338 printf '<br>Parent feature: %s.', join(', ', map {
339 sprintf '<a href="%s">%s</a>', EscapeHTML("#$_"), $caniuse->{data}->{$_}->{title}
340 } $_) for $row->{parent} || ();
346 my $row = $caniuse->{data}->{$id};
348 for ($row->{status}) {
349 my $cell = $_ // '-';
350 $cell = sprintf '<a href="%s">%s</a>', $_, $cell for $row->{spec} // ();
351 printf '<td title="%s" class="l %s">%s',
352 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
357 my ($id, $browser) = @_;
358 my $feature = $caniuse->{data}->{$id};
359 my $data = $feature->{stats}->{$browser};
360 if (ref $data eq 'ARRAY') {
361 # special case for unsupported
362 my $release = $caniuse->{agents}->{$browser}->{verrelease};
364 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
369 for my $ver (@{ $versions{$browser} }, undef) {
371 !defined $ver ? undef : # last column if nameless
372 ref $data ne 'HASH' ? '' : # unclassified if no support hash
373 $data->{ $ver->[-1] } // $prev # known or inherit from predecessor
374 // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
375 ~~ 'n' && 'n' # first known version is unsupported
378 unless (!defined $prev or $prev ~~ $compare) {
379 my $usage = sum(@{ $canihas->{$browser} }{ map { @{$_} } @span });
381 # strip #\d note references from support class
383 push @notes, $feature->{notes_by_num}->{$1}
384 while $prev =~ s/\h \# (\d+) \b//x;
386 # prepare version hover details
387 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(', ',
388 map { ref $_ eq 'CODE' ? $_->($browser) : $_ }
389 map { $DSTATS{$_} // () }
390 map { split / /, $_ }
393 $title .= "\n".EscapeHTML($_) for @notes;
395 printf('<td class="%s" colspan="%d" title="%s">%s',
398 !$usage ? ('p0') : ('p',
399 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
400 sprintf('p%02d', $usage * ($usagepct - .0001)),
402 sprintf('pp%02d', $usage / $usagemax),
406 showversions($span[0]->[0], @span > 1 ? ($span[-1]->[-1]) : ()),
411 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
418 print '<td>', int featurescore($caniuse->{data}->{$id}->{stats}) * $usagepct;
423 featurescore($caniuse->{data}->{$b}->{stats})
424 <=> featurescore($caniuse->{data}->{$a}->{stats})
425 } keys %{ $caniuse->{data} }) {
426 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
427 printf '<tr id="%s">', $id;
430 saybrowsercols($id, $_) for @browsers;
438 # normalised version number comparable as string (cmp)
439 shift =~ /(?:.*-|^)(\d*)(.*)/;
440 # matched (major)(.minor) of last value in range (a-B)
441 return sprintf('%02d', $1 || 99) . $2;
445 my @span = ($_[0], @_>1 ? $_[-1] : ());
446 s/-.*// for $span[0];
452 return join('‒', @span);
459 <table class="glyphs"><tr>
460 <td class="X l5">supported
461 <td class="X l3">partial
462 <td class="X l2">optional
463 <td class="X l1">missing
464 <td class="X l0">unknown
465 <td class="X ex">prefixed
468 <p><: if ($usage) { :>
470 <span class=" p0">0</span> -
471 <span class="p p0 p00">.01</span> -
472 <span class="p p0 p05">1-9</span> -
473 <span class="p p1">10</span> -
474 <span class="p p2">20</span> -
475 <span class="p p5">majority</span>
477 <table class="glyphs"><tr>
478 <td class="p p1">previous version</td>
479 <td class="p p3">current</td>
480 <td class="p p0 p00">upcoming (within months)</td>
481 <td class=" p0">future (within a year)</td>
486 <ul class="legend legend-set">
487 <li>default <strong>style</strong> is
488 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
489 <li><strong>usage</strong> source is
490 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
491 <li>usage <strong>threshold</strong> is
492 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
497 <script type="text/javascript" src="/searchlocal.js"></script>
498 <script type="text/javascript"> prependsearch(document.getElementById('intro')) </script>