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',
61 (map "-$_-", $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,
72 unoff => 'l1', # unofficial
74 cr => 'l3', # candidate
75 pr => 'l3', # proposed
76 rec => 'l5', # recommendation
78 ietf => 'l0', # standard
79 other => 'l0', # non-w3
82 if (my ($somerow) = values %{ $caniuse->{data} }) {
83 while (my ($browser, $row) = each %{ $somerow->{stats} }) {
84 $versions{$browser} = [ sort { paddedver($a) cmp paddedver($b) } keys %$row ];
89 <p id="intro">Alternate rendition of Fyrd's <a href="http://caniuse.com/">when can I use...</a> page
91 my ($canihas, $usage);
92 my $minusage = $get{threshold} // .7;
93 given ($get{usage} // 'wm') {
97 when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
98 printf "<p>Invalid browser usage data request: <em>%s</em>",
99 'identifier must be alphanumeric name or <q>0</q>';
101 $canihas = do "data/browser/usage-$_.inc.pl" or do {
102 printf "<p>Browser usage data not found: <em>%s</em>", $@ || $!;
106 my $ref = $canihas->{-title} || 'unknown';
107 $ref = sprintf '<a href="%s">%s</a>', $_, $ref
108 for $canihas->{-site} || $canihas->{-source} || ();
109 $ref .= " $_" for $canihas->{-date} || ();
110 print "\nwith $ref browser usage statistics";
114 if ($usage) { # first() does not work inside given >:(
115 # adapt version usage to actual support data
116 my %engineuse; # prefix => usage sum
117 for my $browser (keys %versions) {
118 my $row = $canihas->{$browser} // {};
119 my $verlist = $versions{$browser} or next;
120 if ($minusage and sum(values %$row) < $minusage) {
121 delete $versions{$browser};
124 my %supported = map { $_ => 1 } @$verlist;
126 # cascade unknown versions
127 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
128 while (my ($version, $usage) = each %$row) {
129 next if defined $supported{$version};
130 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
131 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
132 $row->{$next} += $usage;
133 $row->{$version} = 0; # balance browser total
136 # build row list for each version
138 my @vershown; # $verlist replacement
139 my ($rowusage, @verrow) = (0); # replacement row tracking
141 push @verrow, $_; # queue each version
142 if (($rowusage += $row->{$_}) >= $minusage) {
143 push @vershown, [@verrow]; # add row
144 ($rowusage, @verrow) = (0); # reset row tracking
147 push @vershown, \@verrow if @verrow; # always add latest
148 @$verlist = @vershown;
151 @$verlist = map { [$_] } @$verlist;
154 # reusable aggregates (grouped by prefix (engine) and browser)
155 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
156 $row->{-total} = sum(values %$row);
159 # order browser columns by usage grouped by engine
161 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
162 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
164 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
168 # order browser columns by name grouped by engine
169 @{$_} = map { [$_] } @{$_} for values %versions;
171 $caniuse->{agents}->{$b}->{prefix} cmp
172 $caniuse->{agents}->{$a}->{prefix}
185 my $zero = $#$_ - 2; # baseline index
186 ($_->[$zero - 2] => .5), # past
187 ($_->[$zero - 1] => 10 ), # previous
188 ($_->[$zero + 2] => 0 ), # future
189 ($_->[$zero + 1] => .5), # next
190 ($_->[$zero ] => 30 ), # current
191 } $caniuse->{agents}->{$_}->{versions}
194 }; # fallback hash based on release semantics
195 my $usagemax = (max(map { ref $_ eq 'HASH' && sum(values %$_) } values %$canihas) // 1) / 100;
197 my $usagepct = 1; # score multiplier for 0..100 result
198 # normalise usage percentage to only include shown browsers
199 $usagepct = 100.01 / featurescore({ # yes for every possible version
200 map { $_ => { map {$_ => 'y'} map { @{$_} } @{$versions{$_}} } } keys %versions
203 print '<table class="mapped">';
204 print '<col span="3">'; # should match first thead row
205 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
206 say '</colgroup><col>';
208 my $header = join('',
210 '<th colspan="3" rowspan="2">feature',
212 my $name = $caniuse->{agents}->{$_}->{browser};
213 sprintf('<th colspan="%d" class="%s" title="%s">%s',
214 scalar @{ $versions{$_} },
215 join(' ', map {"b-a-$_"} grep {$_}
216 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
219 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
223 length $name <= (3 * @{ $versions{$_} }) ? $name
224 : $caniuse->{agents}->{$_}->{abbr};
230 print '<thead>', $header;
231 # preceding row without any colspan to work around gecko bug
233 for my $browser (@browsers) {
234 for (@{ $versions{$browser} }) {
235 my $lastver = $_->[-1];
236 my $release = $caniuse->{agents}->{$browser}->{verrelease}->{$lastver};
237 my $future = defined $release;
238 printf('<td title="%s"%s>%s',
240 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{@$_}) * $usagepct),
241 $future ? 'development' : (),
242 'version ' . join(', ', @{$_}),
244 $future && ' class="ex"',
245 showversions($lastver),
250 say '<tfoot>', $header;
252 # prefix indicates browser family; count adjacent families
253 my (@families, %familycount);
254 for my $browser (@browsers) {
255 my $family = $caniuse->{agents}->{$browser}->{prefix};
256 push @families, $family unless $familycount{$family};
257 $familycount{$family} += @{ $versions{$browser} };
260 print "\n", '<tr class="cat">';
261 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
266 # relative amount of support for given feature
268 if (my $row = shift) {
270 while (my ($browser, $versions) = each %$row) {
271 ref $versions eq 'HASH' or next;
272 while (my ($version, $status) = each %$versions) {
273 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
279 while (my ($browser, $vercols) = each %versions) {
280 my $div = 0; # multiplier exponent (decreased to lower value)
281 my @vers = map { $row->{$browser}->{$_} } @$vercols;
282 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
283 my @future; # find upcoming releases (after current)
284 for (reverse @$vercols) {
285 last if $_ eq $current;
286 push @future, pop @vers;
287 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
289 splice @vers, -1, 0, @future; # move ahead to decrease precedence
291 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
300 s/\h* $//gmx; # trailing whitespace
301 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
303 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
304 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
311 my $row = $caniuse->{data}->{$id};
313 for ($row->{categories}) {
314 my $cell = $_ ? lc $_->[0] : '-';
315 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
316 printf '<th title="%s">%s', join(' + ', @$_), $cell;
320 sprintf('<a href="%s" onclick="%s">%s</a>',
322 sprintf("try { %s; return false } catch(err) { return true }",
323 "document.getElementById('$id').classList.toggle('target')",
328 print '<div class=aside>';
330 for formatnotes($row->{description}, $row->{notes} || ());
331 if (my %notes = %{ $row->{notes_by_num} }) {
332 say '<p>Browser-specific notes:';
333 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
336 printf 'Resources: %s.', join(', ', map {
337 sprintf '<a href="%s">%s</a>', EscapeHTML($_->{url}), $_->{title}
338 } @$_) for grep { @$_ } $row->{links} // ();
339 printf '<br>Parent feature: %s.', join(', ', map {
340 sprintf '<a href="%s">%s</a>', EscapeHTML("#$_"), $caniuse->{data}->{$_}->{title}
341 } $_) for $row->{parent} || ();
347 my $row = $caniuse->{data}->{$id};
349 for ($row->{status}) {
350 my $cell = $_ // '-';
351 $cell = sprintf '<a href="%s">%s</a>', $_, $cell for $row->{spec} // ();
352 printf '<td title="%s" class="l %s">%s',
353 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
358 my ($id, $browser) = @_;
359 my $feature = $caniuse->{data}->{$id};
360 my $data = $feature->{stats}->{$browser};
361 if (ref $data eq 'ARRAY') {
362 # special case for unsupported
363 my $release = $caniuse->{agents}->{$browser}->{verrelease};
365 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
370 for my $ver (@{ $versions{$browser} }, undef) {
372 !defined $ver ? undef : # last column if nameless
373 ref $data ne 'HASH' ? '' : # unclassified if no support hash
374 $data->{ $ver->[-1] } // $prev # known or inherit from predecessor
375 // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
376 ~~ 'n' && 'n' # first known version is unsupported
379 unless (!defined $prev or $prev ~~ $compare) {
380 my $usage = sum(@{ $canihas->{$browser} }{ map { @{$_} } @span });
382 # strip #\d note references from support class
384 push @notes, $feature->{notes_by_num}->{$1}
385 while $prev =~ s/\h \# (\d+) \b//x;
387 # prepare version hover details
388 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(', ',
389 map { ref $_ eq 'CODE' ? $_->($browser) : $_ }
390 map { $DSTATS{$_} // () }
391 map { split / /, $_ }
394 $title .= "\n".EscapeHTML($_) for @notes;
396 printf('<td class="%s" colspan="%d" title="%s">%s',
399 !$usage ? ('p0') : ('p',
400 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
401 sprintf('p%02d', $usage * ($usagepct - .0001)),
403 sprintf('pp%02d', $usage / $usagemax),
407 showversions($span[0]->[0], @span > 1 ? ($span[-1]->[-1]) : ()),
412 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
419 print '<td>', int featurescore($caniuse->{data}->{$id}->{stats}) * $usagepct;
424 featurescore($caniuse->{data}->{$b}->{stats})
425 <=> featurescore($caniuse->{data}->{$a}->{stats})
426 } keys %{ $caniuse->{data} }) {
427 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
428 printf '<tr id="%s">', $id;
431 saybrowsercols($id, $_) for @browsers;
439 # normalised version number comparable as string (cmp)
440 shift =~ /(?:.*-|^)(\d*)(.*)/;
441 # matched (major)(.minor) of last value in range (a-B)
442 return sprintf('%02d', $1 || 99) . $2;
446 my @span = ($_[0], @_>1 ? $_[-1] : ());
447 s/-.*// for $span[0];
453 return join('‒', @span);
460 <table class="glyphs"><tr>
461 <td class="X l5">supported
462 <td class="X l3">partial
463 <td class="X l2">optional
464 <td class="X l1">missing
465 <td class="X l0">unknown
466 <td class="X ex">prefixed
469 <p><: if ($usage) { :>
471 <span class=" p0">0</span> -
472 <span class="p p0 p00">.01</span> -
473 <span class="p p0 p05">1-9</span> -
474 <span class="p p1">10</span> -
475 <span class="p p2">20</span> -
476 <span class="p p5">majority</span>
478 <table class="glyphs"><tr>
479 <td class="p p1">previous version</td>
480 <td class="p p3">current</td>
481 <td class="p p0 p00">upcoming (within months)</td>
482 <td class=" p0">future (within a year)</td>
487 <ul class="legend legend-set">
488 <li>default <strong>style</strong> is
489 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
490 <li><strong>usage</strong> source is
491 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
492 <li>usage <strong>threshold</strong> is
493 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
498 <script type="text/javascript" src="/searchlocal.js"></script>
499 <script type="text/javascript"> prependsearch(document.getElementById('intro')) </script>