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) {
99 printf "<p>Invalid browser usage data request: <em>%s</em>",
100 'identifier must be alphanumeric name or <q>0</q>';
102 $canihas = do "data/browser/usage-$_.inc.pl" or do {
103 printf "<p>Browser usage data not found: <em>%s</em>", $@ || $!;
107 my $ref = $canihas->{-title} || 'unknown';
108 $ref = sprintf '<a href="%s">%s</a>', $_, $ref
109 for $canihas->{-site} || $canihas->{-source} || ();
110 $ref .= " $_" for $canihas->{-date} || ();
111 print "\nwith $ref browser usage statistics";
115 if ($usage) { # first() does not work inside given >:(
116 # adapt version usage to actual support data
117 my %engineuse; # prefix => usage sum
118 for my $browser (keys %versions) {
119 my $row = $canihas->{$browser} // {};
120 my $verlist = $versions{$browser} or next;
121 if ($minusage and sum(values %$row) < $minusage) {
122 delete $versions{$browser};
125 my %supported = map { $_ => 1 } @$verlist;
127 # cascade unknown versions
128 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
129 while (my ($version, $usage) = each %$row) {
130 next if defined $supported{$version};
131 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
132 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
133 $row->{$next} += $usage;
134 $row->{$version} = 0; # balance browser total
137 # build row list for each version
139 my @vershown; # $verlist replacement
140 my ($rowusage, @verrow) = (0); # replacement row tracking
142 push @verrow, $_; # queue each version
143 if (($rowusage += $row->{$_}) >= $minusage) {
144 push @vershown, [@verrow]; # add row
145 ($rowusage, @verrow) = (0); # reset row tracking
148 push @vershown, \@verrow if @verrow; # always add latest
149 @$verlist = @vershown;
152 @$verlist = map { [$_] } @$verlist;
155 # reusable aggregates (grouped by prefix (engine) and browser)
156 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
157 $row->{-total} = sum(values %$row);
160 # order browser columns by usage grouped by engine
162 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
163 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
165 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
169 # order browser columns by name grouped by engine
170 @{$_} = map { [$_] } @{$_} for values %versions;
172 $caniuse->{agents}->{$b}->{prefix} cmp
173 $caniuse->{agents}->{$a}->{prefix}
186 my $zero = $#$_ - 2; # baseline index
187 ($_->[$zero - 2] => .5), # past
188 ($_->[$zero - 1] => 10 ), # previous
189 ($_->[$zero + 2] => 0 ), # future
190 ($_->[$zero + 1] => .5), # next
191 ($_->[$zero ] => 30 ), # current
192 } $caniuse->{agents}->{$_}->{versions}
195 }; # fallback hash based on release semantics
196 my $usagemax = (max(map { ref $_ eq 'HASH' && sum(values %$_) } values %$canihas) // 1) / 100;
198 my $usagepct = 1; # score multiplier for 0..100 result
199 # normalise usage percentage to only include shown browsers
200 $usagepct = 100.01 / featurescore({ # yes for every possible version
201 map { $_ => { map {$_ => 'y'} map { @{$_} } @{$versions{$_}} } } keys %versions
204 print '<table class="mapped">';
205 print '<col span="3">'; # should match first thead row
206 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
207 say '</colgroup><col>';
209 my $header = join('',
211 '<th colspan="3" rowspan="2">feature',
213 my $name = $caniuse->{agents}->{$_}->{browser};
214 sprintf('<th colspan="%d" class="%s" title="%s">%s',
215 scalar @{ $versions{$_} },
216 join(' ', map {"b-a-$_"} grep {$_}
217 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
220 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
224 length $name <= (3 * @{ $versions{$_} }) ? $name
225 : $caniuse->{agents}->{$_}->{abbr};
231 print '<thead>', $header;
232 # preceding row without any colspan to work around gecko bug
234 for my $browser (@browsers) {
235 for (@{ $versions{$browser} }) {
236 my $lastver = $_->[-1];
237 my $release = $caniuse->{agents}->{$browser}->{verrelease}->{$lastver};
238 my $future = defined $release;
239 printf('<td title="%s"%s>%s',
241 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{@$_}) * $usagepct),
242 $future ? 'development' : (),
243 'version ' . join(', ', @{$_}),
245 $future && ' class="ex"',
246 showversions($lastver),
251 say '<tfoot>', $header;
253 # prefix indicates browser family; count adjacent families
254 my (@families, %familycount);
255 for my $browser (@browsers) {
256 my $family = $caniuse->{agents}->{$browser}->{prefix};
257 push @families, $family unless $familycount{$family};
258 $familycount{$family} += @{ $versions{$browser} };
261 print "\n", '<tr class="cat">';
262 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
267 # relative amount of support for given feature
269 if (my $row = shift) {
271 while (my ($browser, $versions) = each %$row) {
272 ref $versions eq 'HASH' or next;
273 while (my ($version, $status) = each %$versions) {
274 $status =~ s/\h\#\d+//g;
275 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
281 while (my ($browser, $vercols) = each %versions) {
282 my $div = 0; # multiplier exponent (decreased to lower value)
283 my @vers = map { $row->{$browser}->{$_} } @$vercols;
284 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
285 my @future; # find upcoming releases (after current)
286 for (reverse @$vercols) {
287 last if $_ eq $current;
288 push @future, pop @vers;
289 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
291 splice @vers, -1, 0, @future; # move ahead to decrease precedence
293 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
302 s/\r\n?/\n/g; # windows returns
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}), EscapeHTML($_->{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>