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 if (my ($somerow) = values %{ $caniuse->{data} }) {
84 while (my ($browser, $row) = each %{ $somerow->{stats} }) {
85 $versions{$browser} = [ sort { paddedver($a) cmp paddedver($b) } keys %$row ];
90 <p id="intro">Alternate rendition of Fyrd's <a href="http://caniuse.com/">when can I use...</a> page
92 my ($canihas, $usage);
93 my $minusage = $get{threshold} // .7;
94 given ($get{usage} // 'wm') {
98 when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
100 'Invalid browser usage data request',
101 'Identifier must be alphanumeric name or <q>0</q>.',
104 $canihas = do "data/browser/usage-$_.inc.pl" or do {
105 Alert('Browser usage data not found', $@ || $!);
109 my $ref = $canihas->{-title} || 'unknown';
110 $ref = sprintf '<a href="%s">%s</a>', $_, $ref
111 for $canihas->{-site} || $canihas->{-source} || ();
112 $ref .= " $_" for $canihas->{-date} || ();
113 print "\nwith $ref browser usage statistics";
117 if ($usage) { # first() does not work inside given >:(
118 # adapt version usage to actual support data
119 my %engineuse; # prefix => usage sum
120 for my $browser (keys %versions) {
121 my $row = $canihas->{$browser} // {};
122 my $verlist = $versions{$browser} or next;
123 if ($minusage and sum(values %$row) < $minusage) {
124 delete $versions{$browser};
127 my %supported = map { $_ => 1 } @$verlist;
129 # cascade unknown versions
130 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
131 while (my ($version, $usage) = each %$row) {
132 next if defined $supported{$version};
133 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
134 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
135 $row->{$next} += $usage;
136 $row->{$version} = 0; # balance browser total
139 # build row list for each version
141 my @vershown; # $verlist replacement
142 my ($rowusage, @verrow) = (0); # replacement row tracking
144 push @verrow, $_; # queue each version
145 if (($rowusage += $row->{$_}) >= $minusage) {
146 push @vershown, [@verrow]; # add row
147 ($rowusage, @verrow) = (0); # reset row tracking
150 push @vershown, \@verrow if @verrow; # always add latest
151 @$verlist = @vershown;
154 @$verlist = map { [$_] } @$verlist;
157 # reusable aggregates (grouped by prefix (engine) and browser)
158 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
159 $row->{-total} = sum(values %$row);
162 # order browser columns by usage grouped by engine
164 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
165 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
167 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
171 # order browser columns by name grouped by engine
172 @{$_} = map { [$_] } @{$_} for values %versions;
174 $caniuse->{agents}->{$b}->{prefix} cmp
175 $caniuse->{agents}->{$a}->{prefix}
188 my $zero = $#$_ - 2; # baseline index
189 ($_->[$zero - 2] => .5), # past
190 ($_->[$zero - 1] => 10 ), # previous
191 ($_->[$zero + 2] => 0 ), # future
192 ($_->[$zero + 1] => .5), # next
193 ($_->[$zero ] => 30 ), # current
194 } $caniuse->{agents}->{$_}->{versions}
197 }; # fallback hash based on release semantics
198 my $usagemax = (max(map { ref $_ eq 'HASH' && sum(values %$_) } values %$canihas) // 1) / 100;
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
206 print '<table class="mapped">';
207 print '<col span="3">'; # should match first thead row
208 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
209 say '</colgroup><col>';
211 my $header = join('',
213 '<th colspan="3" rowspan="2">feature',
215 my $name = $caniuse->{agents}->{$_}->{browser};
216 sprintf('<th colspan="%d" class="%s" title="%s">%s',
217 scalar @{ $versions{$_} },
218 join(' ', map {"b-a-$_"} grep {$_}
219 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
222 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
226 length $name <= (3 * @{ $versions{$_} }) ? $name
227 : $caniuse->{agents}->{$_}->{abbr};
233 print '<thead>', $header;
234 # preceding row without any colspan to work around gecko bug
236 for my $browser (@browsers) {
237 for (@{ $versions{$browser} }) {
238 my $lastver = $_->[-1];
239 my $release = $caniuse->{agents}->{$browser}->{verrelease}->{$lastver};
240 my $future = defined $release;
241 printf('<td title="%s"%s>%s',
243 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{@$_}) * $usagepct),
244 $future ? 'development' : (),
245 'version ' . join(', ', @{$_}),
247 $future && ' class="ex"',
248 showversions($lastver),
253 say '<tfoot>', $header;
255 # prefix indicates browser family; count adjacent families
256 my (@families, %familycount);
257 for my $browser (@browsers) {
258 my $family = $caniuse->{agents}->{$browser}->{prefix};
259 push @families, $family unless $familycount{$family};
260 $familycount{$family} += @{ $versions{$browser} };
263 print "\n", '<tr class="cat">';
264 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
269 # relative amount of support for given feature
271 if (my $row = shift) {
273 while (my ($browser, $versions) = each %$row) {
274 ref $versions eq 'HASH' or next;
275 while (my ($version, $status) = each %$versions) {
276 $status =~ s/\h\#\d+//g;
277 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
283 while (my ($browser, $vercols) = each %versions) {
284 my $div = 0; # multiplier exponent (decreased to lower value)
285 my @vers = map { $row->{$browser}->{$_} } @$vercols;
286 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
287 my @future; # find upcoming releases (after current)
288 for (reverse @$vercols) {
289 last if $_ eq $current;
290 push @future, pop @vers;
291 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
293 splice @vers, -1, 0, @future; # move ahead to decrease precedence
295 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
304 s/\r\n?/\n/g; # windows returns
305 s/\h* $//gmx; # trailing whitespace
306 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
308 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
309 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
316 my $row = $caniuse->{data}->{$id};
318 for ($row->{categories}) {
319 my $cell = $_ ? lc $_->[0] : '-';
320 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
321 printf '<th title="%s">%s', join(' + ', @$_), $cell;
325 sprintf('<a href="%s" onclick="%s">%s</a>',
327 sprintf("try { %s; return false } catch(err) { return true }",
328 "document.getElementById('$id').classList.toggle('target')",
333 print '<div class=aside>';
335 for formatnotes($row->{description}, $row->{notes} || ());
336 if (my %notes = %{ $row->{notes_by_num} }) {
337 say '<p>Browser-specific notes:';
338 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
341 printf 'Resources: %s.', join(', ', map {
342 sprintf '<a href="%s">%s</a>', EscapeHTML($_->{url}), EscapeHTML($_->{title})
343 } @$_) for grep { @$_ } $row->{links} // ();
344 printf '<br>Parent feature: %s.', join(', ', map {
345 sprintf '<a href="%s">%s</a>', EscapeHTML("#$_"), $caniuse->{data}->{$_}->{title}
346 } $_) for $row->{parent} || ();
352 my $row = $caniuse->{data}->{$id};
354 for ($row->{status}) {
355 my $cell = $_ // '-';
356 $cell = sprintf '<a href="%s">%s</a>', $_, $cell for $row->{spec} // ();
357 printf '<td title="%s" class="l %s">%s',
358 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
363 my ($id, $browser) = @_;
364 my $feature = $caniuse->{data}->{$id};
365 my $data = $feature->{stats}->{$browser};
366 if (ref $data eq 'ARRAY') {
367 # special case for unsupported
368 my $release = $caniuse->{agents}->{$browser}->{verrelease};
370 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
375 for my $ver (@{ $versions{$browser} }, undef) {
377 !defined $ver ? undef : # last column if nameless
378 ref $data ne 'HASH' ? '' : # unclassified if no support hash
379 $data->{ $ver->[-1] } // $prev # known or inherit from predecessor
380 // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
381 ~~ 'n' && 'n' # first known version is unsupported
384 unless (!defined $prev or $prev ~~ $compare) {
385 my $usage = sum(@{ $canihas->{$browser} }{ map { @{$_} } @span });
387 # strip #\d note references from support class
389 push @notes, $feature->{notes_by_num}->{$1}
390 while $prev =~ s/\h \# (\d+) \b//x;
392 # prepare version hover details
393 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(', ',
394 map { ref $_ eq 'CODE' ? $_->($browser, $span[0]->[0]) : $_ }
395 map { $DSTATS{$_} // () }
396 map { split / /, $_ }
399 $title .= "\n".EscapeHTML($_) for @notes;
401 printf('<td class="%s" colspan="%d" title="%s">%s',
404 !$usage ? ('p0') : ('p',
405 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
406 sprintf('p%02d', $usage * ($usagepct - .0001)),
408 sprintf('pp%02d', $usage / $usagemax),
412 showversions($span[0]->[0], @span > 1 ? ($span[-1]->[-1]) : ()),
417 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
424 print '<td>', int featurescore($caniuse->{data}->{$id}->{stats}) * $usagepct;
429 featurescore($caniuse->{data}->{$b}->{stats})
430 <=> featurescore($caniuse->{data}->{$a}->{stats})
431 } keys %{ $caniuse->{data} }) {
432 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
433 printf '<tr id="%s">', $id;
436 saybrowsercols($id, $_) for @browsers;
444 # normalised version number comparable as string (cmp)
445 shift =~ /(?:.*-|^)(\d*)(.*)/;
446 # matched (major)(.minor) of last value in range (a-B)
447 return sprintf('%02d', $1 || 99) . $2;
451 my @span = ($_[0], @_>1 ? $_[-1] : ());
452 s/-.*// for $span[0];
458 return join('‒', @span);
465 <table class="glyphs"><tr>
466 <td class="X l5">supported
467 <td class="X l3">partial
468 <td class="X l2">optional
469 <td class="X l1">missing
470 <td class="X l0">unknown
471 <td class="X ex">prefixed
474 <p><: if ($usage) { :>
476 <span class=" p0">0</span> -
477 <span class="p p0 p00">.01</span> -
478 <span class="p p0 p05">1-9</span> -
479 <span class="p p1">10</span> -
480 <span class="p p2">20</span> -
481 <span class="p p5">majority</span>
483 <table class="glyphs"><tr>
484 <td class="p p1">previous version</td>
485 <td class="p p3">current</td>
486 <td class="p p0 p00">upcoming (within months)</td>
487 <td class=" p0">future (within a year)</td>
492 <ul class="legend legend-set">
493 <li>default <strong>style</strong> is
494 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
495 <li><strong>usage</strong> source is
496 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
497 <li>usage <strong>threshold</strong> is
498 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
503 <script type="text/javascript" src="/searchlocal.js"></script>
504 <script type="text/javascript"> prependsearch(document.getElementById('intro')) </script>