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/\r\n?/\n/g; # windows returns
304 s/\h* $//gmx; # trailing whitespace
305 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
307 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
308 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
315 my $row = $caniuse->{data}->{$id};
317 for ($row->{categories}) {
318 my $cell = $_ ? lc $_->[0] : '-';
319 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
320 printf '<th title="%s">%s', join(' + ', @$_), $cell;
324 sprintf('<a href="%s" onclick="%s">%s</a>',
326 sprintf("try { %s; return false } catch(err) { return true }",
327 "document.getElementById('$id').classList.toggle('target')",
332 print '<div class=aside>';
334 for formatnotes($row->{description}, $row->{notes} || ());
335 if (my %notes = %{ $row->{notes_by_num} }) {
336 say '<p>Browser-specific notes:';
337 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
340 printf 'Resources: %s.', join(', ', map {
341 sprintf '<a href="%s">%s</a>', EscapeHTML($_->{url}), $_->{title}
342 } @$_) for grep { @$_ } $row->{links} // ();
343 printf '<br>Parent feature: %s.', join(', ', map {
344 sprintf '<a href="%s">%s</a>', EscapeHTML("#$_"), $caniuse->{data}->{$_}->{title}
345 } $_) for $row->{parent} || ();
351 my $row = $caniuse->{data}->{$id};
353 for ($row->{status}) {
354 my $cell = $_ // '-';
355 $cell = sprintf '<a href="%s">%s</a>', $_, $cell for $row->{spec} // ();
356 printf '<td title="%s" class="l %s">%s',
357 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
362 my ($id, $browser) = @_;
363 my $feature = $caniuse->{data}->{$id};
364 my $data = $feature->{stats}->{$browser};
365 if (ref $data eq 'ARRAY') {
366 # special case for unsupported
367 my $release = $caniuse->{agents}->{$browser}->{verrelease};
369 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
374 for my $ver (@{ $versions{$browser} }, undef) {
376 !defined $ver ? undef : # last column if nameless
377 ref $data ne 'HASH' ? '' : # unclassified if no support hash
378 $data->{ $ver->[-1] } // $prev # known or inherit from predecessor
379 // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
380 ~~ 'n' && 'n' # first known version is unsupported
383 unless (!defined $prev or $prev ~~ $compare) {
384 my $usage = sum(@{ $canihas->{$browser} }{ map { @{$_} } @span });
386 # strip #\d note references from support class
388 push @notes, $feature->{notes_by_num}->{$1}
389 while $prev =~ s/\h \# (\d+) \b//x;
391 # prepare version hover details
392 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(', ',
393 map { ref $_ eq 'CODE' ? $_->($browser, $span[0]->[0]) : $_ }
394 map { $DSTATS{$_} // () }
395 map { split / /, $_ }
398 $title .= "\n".EscapeHTML($_) for @notes;
400 printf('<td class="%s" colspan="%d" title="%s">%s',
403 !$usage ? ('p0') : ('p',
404 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
405 sprintf('p%02d', $usage * ($usagepct - .0001)),
407 sprintf('pp%02d', $usage / $usagemax),
411 showversions($span[0]->[0], @span > 1 ? ($span[-1]->[-1]) : ()),
416 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
423 print '<td>', int featurescore($caniuse->{data}->{$id}->{stats}) * $usagepct;
428 featurescore($caniuse->{data}->{$b}->{stats})
429 <=> featurescore($caniuse->{data}->{$a}->{stats})
430 } keys %{ $caniuse->{data} }) {
431 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
432 printf '<tr id="%s">', $id;
435 saybrowsercols($id, $_) for @browsers;
443 # normalised version number comparable as string (cmp)
444 shift =~ /(?:.*-|^)(\d*)(.*)/;
445 # matched (major)(.minor) of last value in range (a-B)
446 return sprintf('%02d', $1 || 99) . $2;
450 my @span = ($_[0], @_>1 ? $_[-1] : ());
451 s/-.*// for $span[0];
457 return join('‒', @span);
464 <table class="glyphs"><tr>
465 <td class="X l5">supported
466 <td class="X l3">partial
467 <td class="X l2">optional
468 <td class="X l1">missing
469 <td class="X l0">unknown
470 <td class="X ex">prefixed
473 <p><: if ($usage) { :>
475 <span class=" p0">0</span> -
476 <span class="p p0 p00">.01</span> -
477 <span class="p p0 p05">1-9</span> -
478 <span class="p p1">10</span> -
479 <span class="p p2">20</span> -
480 <span class="p p5">majority</span>
482 <table class="glyphs"><tr>
483 <td class="p p1">previous version</td>
484 <td class="p p3">current</td>
485 <td class="p p0 p00">upcoming (within months)</td>
486 <td class=" p0">future (within a year)</td>
491 <ul class="legend legend-set">
492 <li>default <strong>style</strong> is
493 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
494 <li><strong>usage</strong> source is
495 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
496 <li>usage <strong>threshold</strong> is
497 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
502 <script type="text/javascript" src="/searchlocal.js"></script>
503 <script type="text/javascript"> prependsearch(document.getElementById('intro')) </script>