4 use List::Util qw(sum max first);
5 no if $] >= 5.018, warnings => 'experimental::smartmatch';
8 title => 'browser compatibility cheat sheet',
11 "Compatibility table of new web features (HTML5, CSS3, SVG, Javascript)",
12 "comparing support and usage share for all popular browser versions.",
15 web browser support compatibility usage matrix available feature
16 html html5 css css3 svg javascript js dom mobile
17 ie internet explorer firefox chrome safari webkit opera
19 stylesheet => [qw'circus dark mono red light'],
20 data => ['data/browser/support.inc.pl'],
23 say "<h1>Browser compatibility</h1>\n";
25 my $caniuse = do 'data/browser/support.inc.pl' or die $@ || $!;
27 # mark last three (future) versions as unreleased, ensure current isn't
29 $_->[-1] => 0, $_->[-2] => 0, $_->[-3] => 0,
32 } for values %{ $caniuse->{agents} };
53 p => 'plugin required',
54 j => 'javascript required',
57 d => 'disabled by default',
62 ($caniuse->{agents}->{$_[0]}->{prefix_exceptions} // {})->{$_[1]}
63 // $caniuse->{agents}->{$_[0]}->{prefix} // (),
67 my %PSTATS = ( # score percentage
69 a => .5, 'a x' => .5, 'a d' => .1,
70 j => .2, 'p j' => .2, 'n d' => .2, 'n x d' => .2,
71 p => .2, 'p p' => .2, 'p d' => .1,
74 unoff => 'l1', # unofficial
76 cr => 'l3', # candidate
77 pr => 'l3', # proposed
78 rec => 'l5', # recommendation
80 ietf => 'l0', # standard
81 other => 'l0', # non-w3
84 if (my ($somerow) = values %{ $caniuse->{data} }) {
85 while (my ($browser, $row) = each %{ $somerow->{stats} }) {
86 $versions{$browser} = [ sort { paddedver($a) cmp paddedver($b) } keys %$row ];
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} // .7;
95 given ($get{usage} // 'wm') {
99 when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
100 printf "<p>Invalid browser usage data request: <em>%s</em>",
101 'identifier must be alphanumeric name or <q>0</q>';
103 $canihas = do "data/browser/usage-$_.inc.pl" or do {
104 printf "<p>Browser usage data not found: <em>%s</em>", $@ || $!;
108 my $ref = $canihas->{-title} || 'unknown';
109 $ref = sprintf '<a href="%s">%s</a>', $_, $ref
110 for $canihas->{-site} || $canihas->{-source} || ();
111 $ref .= " $_" for $canihas->{-date} || ();
112 print "\nwith $ref browser usage statistics";
116 if ($usage) { # first() does not work inside given >:(
117 # adapt version usage to actual support data
118 my %engineuse; # prefix => usage sum
119 for my $browser (keys %versions) {
120 my $row = $canihas->{$browser} // {};
121 my $verlist = $versions{$browser} or next;
122 if ($minusage and sum(values %$row) < $minusage) {
123 delete $versions{$browser};
126 my %supported = map { $_ => 1 } @$verlist;
128 # cascade unknown versions
129 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
130 while (my ($version, $usage) = each %$row) {
131 next if defined $supported{$version};
132 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
133 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
134 $row->{$next} += $usage;
135 $row->{$version} = 0; # balance browser total
138 # build row list for each version
140 my @vershown; # $verlist replacement
141 my ($rowusage, @verrow) = (0); # replacement row tracking
143 push @verrow, $_; # queue each version
144 if (($rowusage += $row->{$_}) >= $minusage) {
145 push @vershown, [@verrow]; # add row
146 ($rowusage, @verrow) = (0); # reset row tracking
149 push @vershown, \@verrow if @verrow; # always add latest
150 @$verlist = @vershown;
153 @$verlist = map { [$_] } @$verlist;
156 # reusable aggregates (grouped by prefix (engine) and browser)
157 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
158 $row->{-total} = sum(values %$row);
161 # order browser columns by usage grouped by engine
163 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
164 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
166 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
170 # order browser columns by name grouped by engine
171 @{$_} = map { [$_] } @{$_} for values %versions;
173 $caniuse->{agents}->{$b}->{prefix} cmp
174 $caniuse->{agents}->{$a}->{prefix}
187 my $zero = $#$_ - 2; # baseline index
188 ($_->[$zero - 2] => .5), # past
189 ($_->[$zero - 1] => 10 ), # previous
190 ($_->[$zero + 2] => 0 ), # future
191 ($_->[$zero + 1] => .5), # next
192 ($_->[$zero ] => 30 ), # current
193 } $caniuse->{agents}->{$_}->{versions}
196 }; # fallback hash based on release semantics
197 my $usagemax = (max(map { ref $_ eq 'HASH' && sum(values %$_) } values %$canihas) // 1) / 100;
199 my $usagepct = 1; # score multiplier for 0..100 result
200 # normalise usage percentage to only include shown browsers
201 $usagepct = 100.01 / featurescore({ # yes for every possible version
202 map { $_ => { map {$_ => 'y'} map { @{$_} } @{$versions{$_}} } } keys %versions
205 print '<table class="mapped">';
206 print '<col span="3">'; # should match first thead row
207 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
208 say '</colgroup><col>';
210 my $header = join('',
212 '<th colspan="3" rowspan="2">feature',
214 my $name = $caniuse->{agents}->{$_}->{browser};
215 sprintf('<th colspan="%d" class="%s" title="%s">%s',
216 scalar @{ $versions{$_} },
217 join(' ', map {"b-a-$_"} grep {$_}
218 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
221 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
225 length $name <= (3 * @{ $versions{$_} }) ? $name
226 : $caniuse->{agents}->{$_}->{abbr};
232 print '<thead>', $header;
233 # preceding row without any colspan to work around gecko bug
235 for my $browser (@browsers) {
236 for (@{ $versions{$browser} }) {
237 my $lastver = $_->[-1];
238 my $release = $caniuse->{agents}->{$browser}->{verrelease}->{$lastver};
239 my $future = defined $release;
240 printf('<td title="%s"%s>%s',
242 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{@$_}) * $usagepct),
243 $future ? 'development' : (),
244 'version ' . join(', ', @{$_}),
246 $future && ' class="ex"',
247 showversions($lastver),
252 say '<tfoot>', $header;
254 # prefix indicates browser family; count adjacent families
255 my (@families, %familycount);
256 for my $browser (@browsers) {
257 my $family = $caniuse->{agents}->{$browser}->{prefix};
258 push @families, $family unless $familycount{$family};
259 $familycount{$family} += @{ $versions{$browser} };
262 print "\n", '<tr class="cat">';
263 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
268 # relative amount of support for given feature
270 if (my $row = shift) {
272 while (my ($browser, $versions) = each %$row) {
273 ref $versions eq 'HASH' or next;
274 while (my ($version, $status) = each %$versions) {
275 $status =~ s/\h\#\d+//;
276 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
282 while (my ($browser, $vercols) = each %versions) {
283 my $div = 0; # multiplier exponent (decreased to lower value)
284 my @vers = map { $row->{$browser}->{$_} } @$vercols;
285 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
286 my @future; # find upcoming releases (after current)
287 for (reverse @$vercols) {
288 last if $_ eq $current;
289 push @future, pop @vers;
290 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
292 splice @vers, -1, 0, @future; # move ahead to decrease precedence
294 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
303 s/\h* $//gmx; # trailing whitespace
304 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
306 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
307 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
314 my $row = $caniuse->{data}->{$id};
316 for ($row->{categories}) {
317 my $cell = $_ ? lc $_->[0] : '-';
318 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
319 printf '<th title="%s">%s', join(' + ', @$_), $cell;
323 sprintf('<a href="%s" onclick="%s">%s</a>',
325 sprintf("try { %s; return false } catch(err) { return true }",
326 "document.getElementById('$id').classList.toggle('target')",
331 print '<div class=aside>';
333 for formatnotes($row->{description}, $row->{notes} || ());
334 if (my %notes = %{ $row->{notes_by_num} }) {
335 say '<p>Browser-specific notes:';
336 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
339 printf 'Resources: %s.', join(', ', map {
340 sprintf '<a href="%s">%s</a>', EscapeHTML($_->{url}), $_->{title}
341 } @$_) for grep { @$_ } $row->{links} // ();
342 printf '<br>Parent feature: %s.', join(', ', map {
343 sprintf '<a href="%s">%s</a>', EscapeHTML("#$_"), $caniuse->{data}->{$_}->{title}
344 } $_) for $row->{parent} || ();
350 my $row = $caniuse->{data}->{$id};
352 for ($row->{status}) {
353 my $cell = $_ // '-';
354 $cell = sprintf '<a href="%s">%s</a>', $_, $cell for $row->{spec} // ();
355 printf '<td title="%s" class="l %s">%s',
356 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
361 my ($id, $browser) = @_;
362 my $feature = $caniuse->{data}->{$id};
363 my $data = $feature->{stats}->{$browser};
364 if (ref $data eq 'ARRAY') {
365 # special case for unsupported
366 my $release = $caniuse->{agents}->{$browser}->{verrelease};
368 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
373 for my $ver (@{ $versions{$browser} }, undef) {
375 !defined $ver ? undef : # last column if nameless
376 ref $data ne 'HASH' ? '' : # unclassified if no support hash
377 $data->{ $ver->[-1] } // $prev # known or inherit from predecessor
378 // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
379 ~~ 'n' && 'n' # first known version is unsupported
382 unless (!defined $prev or $prev ~~ $compare) {
383 my $usage = sum(@{ $canihas->{$browser} }{ map { @{$_} } @span });
385 # strip #\d note references from support class
387 push @notes, $feature->{notes_by_num}->{$1}
388 while $prev =~ s/\h \# (\d+) \b//x;
390 # prepare version hover details
391 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(', ',
392 map { ref $_ eq 'CODE' ? $_->($browser, $span[0]->[0]) : $_ }
393 map { $DSTATS{$_} // () }
394 map { split / /, $_ }
397 $title .= "\n".EscapeHTML($_) for @notes;
399 printf('<td class="%s" colspan="%d" title="%s">%s',
402 !$usage ? ('p0') : ('p',
403 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
404 sprintf('p%02d', $usage * ($usagepct - .0001)),
406 sprintf('pp%02d', $usage / $usagemax),
410 showversions($span[0]->[0], @span > 1 ? ($span[-1]->[-1]) : ()),
415 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
422 print '<td>', int featurescore($caniuse->{data}->{$id}->{stats}) * $usagepct;
427 featurescore($caniuse->{data}->{$b}->{stats})
428 <=> featurescore($caniuse->{data}->{$a}->{stats})
429 } keys %{ $caniuse->{data} }) {
430 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
431 printf '<tr id="%s">', $id;
434 saybrowsercols($id, $_) for @browsers;
442 # normalised version number comparable as string (cmp)
443 shift =~ /(?:.*-|^)(\d*)(.*)/;
444 # matched (major)(.minor) of last value in range (a-B)
445 return sprintf('%02d', $1 || 99) . $2;
449 my @span = ($_[0], @_>1 ? $_[-1] : ());
450 s/-.*// for $span[0];
456 return join('‒', @span);
463 <table class="glyphs"><tr>
464 <td class="X l5">supported
465 <td class="X l3">partial
466 <td class="X l2">optional
467 <td class="X l1">missing
468 <td class="X l0">unknown
469 <td class="X ex">prefixed
472 <p><: if ($usage) { :>
474 <span class=" p0">0</span> -
475 <span class="p p0 p00">.01</span> -
476 <span class="p p0 p05">1-9</span> -
477 <span class="p p1">10</span> -
478 <span class="p p2">20</span> -
479 <span class="p p5">majority</span>
481 <table class="glyphs"><tr>
482 <td class="p p1">previous version</td>
483 <td class="p p3">current</td>
484 <td class="p p0 p00">upcoming (within months)</td>
485 <td class=" p0">future (within a year)</td>
490 <ul class="legend legend-set">
491 <li>default <strong>style</strong> is
492 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
493 <li><strong>usage</strong> source is
494 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
495 <li>usage <strong>threshold</strong> is
496 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
501 <script type="text/javascript" src="/searchlocal.js"></script>
502 <script type="text/javascript"> prependsearch(document.getElementById('intro')) </script>