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',
54 d => '(disabled by default)',
59 ($caniuse->{agents}->{$_[0]}->{prefix_exceptions} // {})->{$_[1]}
60 // $caniuse->{agents}->{$_[0]}->{prefix} // (),
64 my %PSTATS = ( # score percentage
66 a => .5, 'a x' => .5, 'a d' => .2,
68 n => 0, 'n d' => .2, 'n x d' => .2,
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 while (my ($browser, $row) = each %{ $caniuse->{agents} }) {
83 $versions{$browser} = [
84 sort { paddedver($a) cmp paddedver($b) } grep { defined }
89 my $ref = showlink('Can I use', 'https://caniuse.com/');
90 $ref =~ s/(?=>)/ title="updated $_"/
91 for map { s/[\sT].*//r } $caniuse->{-date} || ();
92 $ref = "Fyrd's $ref page";
93 say '<p id="intro">Alternate rendition of '.$ref;
95 my ($canihas, $usage);
96 my $minusage = $get{threshold} // 1;
97 given ($get{usage} // 'wm') {
101 when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
103 'Invalid browser usage data request',
104 'Identifier must be alphanumeric name or <q>0</q>.',
107 $canihas = do "data/browser/usage-$_.inc.pl" or do {
108 Alert('Browser usage data not found', $@ || $!);
112 my $ref = $canihas->{-title} || 'unknown';
113 $ref = showlink($ref, $_)
114 for $canihas->{-site} || $canihas->{-source} || ();
115 $ref =~ s/(?=>)/ title="updated $_"/ for $canihas->{-date} || ();
116 print "\nwith $ref browser usage statistics";
120 if ($usage) { # first() does not work inside given >:(
121 # adapt version usage to actual support data
122 my %engineuse; # prefix => usage sum
123 for my $browser (keys %versions) {
124 my $row = $canihas->{$browser} // {};
125 my $verlist = $versions{$browser} or next;
126 if ($minusage and sum(values %$row) < $minusage) {
127 delete $versions{$browser};
130 my %supported = map { $_ => 1 } @$verlist;
132 # cascade unknown versions
133 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
134 while (my ($version, $usage) = each %$row) {
135 next if defined $supported{$version};
136 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
137 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
138 $row->{$next} += $usage;
139 $row->{$version} = 0; # balance browser total
142 # build row list for each version
144 my @vershown; # $verlist replacement
145 my ($rowusage, @verrow) = (0); # replacement row tracking
147 push @verrow, $_; # queue each version
148 if (($rowusage += $row->{$_}) >= $minusage) {
149 push @vershown, [@verrow]; # add row
150 ($rowusage, @verrow) = (0); # reset row tracking
153 push @vershown, \@verrow if @verrow; # always add latest
154 @$verlist = @vershown;
157 @$verlist = map { [$_] } @$verlist;
160 # reusable aggregates (grouped by prefix (engine) and browser)
161 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
162 $row->{-total} = sum(values %$row);
165 # order browser columns by usage grouped by engine
167 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
168 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
170 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
174 # order browser columns by name grouped by engine
175 @{$_} = map { [$_] } @{$_} for values %versions;
177 $caniuse->{agents}->{$b}->{prefix} cmp
178 $caniuse->{agents}->{$a}->{prefix}
191 my $zero = $#$_ - 2; # baseline index
192 ($_->[$zero - 2] => .5), # past
193 ($_->[$zero - 1] => 10 ), # previous
194 ($_->[$zero + 2] => 0 ), # future
195 ($_->[$zero + 1] => .5), # next
196 ($_->[$zero ] => 30 ), # current
197 } $caniuse->{agents}->{$_}->{versions}
200 }; # fallback hash based on release semantics
202 # score multiplier for percentage of all browser versions
203 my $usagepct = 99.99 / sum(
204 map { $_->{-total} // values %{$_} }
205 map { $canihas->{$_} }
210 $_->{usage} = featurescore($_->{stats}) * $usagepct
211 for values %{ $caniuse->{data} };
213 print '<table class="mapped">';
214 print '<col span="3">'; # should match first thead row
215 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
216 say '</colgroup><col>';
218 my $header = join('',
220 '<th colspan="3" rowspan="2">feature',
222 my $name = $caniuse->{agents}->{$_}->{browser};
223 sprintf('<th colspan="%d" class="%s" title="%s">%s',
224 scalar @{ $versions{$_} },
225 join(' ', map {"b-a-$_"} grep {$_}
226 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
229 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
233 length $name <= (3 * @{ $versions{$_} }) ? $name
234 : $caniuse->{agents}->{$_}->{abbr};
240 print '<thead>', $header;
241 # preceding row without any colspan to work around gecko bug
243 for my $browser (@browsers) {
244 for my $span (@{ $versions{$browser} }) {
245 my $lastver = first {
246 !defined $caniuse->{agents}->{$browser}->{verrelease}->{$_} # stable
248 printf('<td title="%s"%s>%s',
250 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{ @{$span} }) * $usagepct),
251 'version ' . showversions(@{$span}, undef),
252 $span->[-1] eq $lastver ? () : '(development)',
254 !defined $lastver && ' class="ex"',
255 showversions($lastver // $span->[0]),
260 say '<tfoot>', $header;
262 # prefix indicates browser family; count adjacent families
263 my (@families, %familycount);
264 for my $browser (@browsers) {
265 my $family = $caniuse->{agents}->{$browser}->{prefix};
266 push @families, $family unless $familycount{$family};
267 $familycount{$family} += @{ $versions{$browser} };
270 print "\n", '<tr class="cat">';
271 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
276 # relative amount of support for given feature
278 if (my $row = shift) {
280 while (my ($browser, $versions) = each %$row) {
281 ref $versions eq 'HASH' or next;
283 for my $version (@{ $caniuse->{agents}->{$browser}->{versions} }) {
284 my $status = $versions->{$version} // $prev;
285 $status =~ s/\h\#\d+//g;
286 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
293 while (my ($browser, $vercols) = each %versions) {
294 my $div = 0; # multiplier exponent (decreased to lower value)
295 my @vers = map { $row->{$browser}->{$_} } @$vercols;
296 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
297 my @future; # find upcoming releases (after current)
298 for (reverse @$vercols) {
299 last if $_ eq $current;
300 push @future, pop @vers;
301 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
303 splice @vers, -1, 0, @future; # move ahead to decrease precedence
305 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
314 s/\r\n?/\n/g; # windows returns
315 s/\h* $//gmx; # trailing whitespace
316 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
318 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
319 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
328 s{ \[ ([^]]*) \] \( [^)]* \) }{$1}gx; # strip link urls
335 my $row = $caniuse->{data}->{$id};
337 for ($row->{categories}) {
338 my $cell = $_ ? lc $_->[0] : '-';
339 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
340 printf '<th title="%s">%s', join(' + ', @$_), $cell;
344 sprintf('<a href="%s" onclick="%s">%s</a>',
346 sprintf("try { %s; return false } catch(err) { return true }",
347 "document.getElementById('$id').classList.toggle('target')",
352 print '<div class=aside>';
354 for formatnotes($row->{description}, $row->{notes} || ());
355 if (my %notes = %{ $row->{notes_by_num} }) {
356 say '<p>Browser-specific notes:';
357 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
360 printf 'Resources: %s.', join(', ', map {
361 showlink($_->{title}, $_->{url})
362 } @$_) for grep { @$_ } $row->{links} // ();
363 printf '<br>Parent feature: %s.', join(', ', map {
364 showlink($caniuse->{data}->{$_}->{title}, "#$_")
365 } $_) for $row->{parent} || ();
371 my $row = $caniuse->{data}->{$id};
373 for ($row->{status}) {
374 my $cell = $_ // '-';
375 $cell = showlink($cell, $_) for $row->{spec} // ();
376 printf '<td title="%s" class="l %s">%s',
377 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
382 my ($id, $browser) = @_;
383 my $feature = $caniuse->{data}->{$id};
384 my $data = $feature->{stats}->{$browser};
385 if (ref $data eq 'ARRAY') {
386 # special case for unsupported
387 my $release = $caniuse->{agents}->{$browser}->{verrelease};
389 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
394 for my $ver (@{ $versions{$browser} }, undef) {
396 !defined $ver ? undef : # last column if nameless
397 ref $data ne 'HASH' ? '' : # unclassified if no support hash
398 (first { defined } @{$data}{ reverse @{$ver} }) # last known version
399 // $prev # inherit from predecessor
402 if (defined $prev and not $prev ~~ $compare) {
404 my @vercover = (map { @{$_} } @span); # accumulated conforming versions
405 for ($ver ? @{$ver} : ()) {
406 last if defined $data->{$_}; # until different
407 push @vercover, $_; # matches from next span start
409 my $usage = sum(@{ $canihas->{$browser} }{@vercover});
411 # strip #\d note references from support class
413 push @notes, $feature->{notes_by_num}->{$1}
414 while $prev =~ s/\h \# (\d+) \b//x;
416 # prepare version hover details
417 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(' ',
418 (map { ref $_ eq 'CODE' ? $_->($browser, $vercover[0]) : $_ }
419 map { $DSTATS{$_} // () }
420 map { split / /, $_ }
423 'in', $caniuse->{agents}->{$browser}->{abbr},
424 showversions(@vercover, undef),
426 $title .= "\n$_" for notestotitle(@notes);
428 $prev .= ' #' if @notes and $prev =~ /^y/;
429 printf('<td class="%s" colspan="%d" title="%s">%s',
432 !$usage ? ('p0') : ('p',
433 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
434 sprintf('p%02d', $usage * ($usagepct - .0001)),
439 showversions($span[0]->[0], @span > 1 && defined $ver ? $span[-1]->[-1] : ()),
445 my $startversion = first { defined $data->{ $ver->[$_] } }
446 reverse 0 .. $#{$ver}; # compare index
447 push @span, [ @{$ver}[ $startversion .. $#{$ver} ] ];
455 print '<td>', int $caniuse->{data}->{$id}->{usage};
460 $caniuse->{data}->{$b}->{usage} <=> $caniuse->{data}->{$a}->{usage}
461 } keys %{ $caniuse->{data} }) {
462 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
463 printf '<tr id="%s">', $id;
466 saybrowsercols($id, $_) for @browsers;
474 # normalised version number comparable as string (cmp)
475 $_[0] =~ m/(?:.*-|^)(\d*)(.*)/;
476 # matched (major)(.minor) of last value in range (a-B)
477 return sprintf('%02d', length $1 ? $1 : 99) . $2;
481 # title to describe minumum version and optional maximum for multiple cells
482 my @span = (map { split /-/ } grep { defined } @_);
483 return $span[0] =~ s/\.0\z//r if @_ <= 1;
485 return join('‒', @span);
492 <table class="glyphs"><tr>
493 <td class="X l5">supported
494 <td class="X l4">annotated
495 <td class="X l3">partial
496 <td class="X l2">optional
497 <td class="X l1">missing
498 <td class="X l0">unknown
499 <td class="X ex">prefixed
502 <p><: if ($usage) { :>
504 <span class=" p0">0</span> -
505 <span class="p p0 p00">.01</span> -
506 <span class="p p0 p05">1-9</span> -
507 <span class="p p1">10</span> -
508 <span class="p p2">20</span> -
509 <span class="p p5">majority</span>
511 <table class="glyphs"><tr>
512 <td class="p p1">previous version</td>
513 <td class="p p3">current</td>
514 <td class="p p0 p00">upcoming (within months)</td>
515 <td class=" p0">future (within a year)</td>
520 <ul class="legend legend-set">
521 <li>default <strong>style</strong> is
522 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
523 <li><strong>usage</strong> source is
524 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
525 <li>usage <strong>threshold</strong> is
526 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
531 <script type="text/javascript" src="/searchlocal.js"></script>
532 <script type="text/javascript"><!--
533 prependsearch(document.getElementById('intro'));