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 $@ || $!;
44 p => 'plugin required',
47 d => '(disabled by default)',
52 ($caniuse->{agents}->{$_[0]}->{prefix_exceptions} // {})->{$_[1]}
53 // $caniuse->{agents}->{$_[0]}->{prefix} // (),
57 my %PSTATS = ( # score percentage
59 a => .5, 'a x' => .5, 'a d' => .2,
61 n => 0, 'n d' => .2, 'n x d' => .2,
65 unoff => 'l1', # unofficial
67 cr => 'l3', # candidate
68 pr => 'l3', # proposed
69 rec => 'l5', # recommendation
71 ietf => 'l0', # standard
72 other => 'l0', # non-w3
75 while (my ($browser, $row) = each %{ $caniuse->{agents} }) {
76 $versions{$browser} = [
77 sort { paddedver($a) cmp paddedver($b) } grep { defined }
82 my $ref = showlink('Can I use', 'https://caniuse.com/');
83 $ref =~ s/(?=>)/ title="updated $_"/
84 for map { s/[\sT].*//r } $caniuse->{-date} || ();
85 $ref = "Fyrd's $ref page";
86 say '<p id="intro">Alternate rendition of '.$ref;
88 my ($canihas, $usage);
89 my $minusage = $get{threshold} // 1;
90 given ($get{usage} // 'wm') {
94 when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
96 'Invalid browser usage data request',
97 'Identifier must be alphanumeric name or <q>0</q>.',
100 $canihas = do "data/browser/usage-$_.inc.pl" or do {
101 Alert('Browser usage data not found', $@ || $!);
105 my $ref = $canihas->{-title} || 'unknown';
106 $ref = showlink($ref, $_)
107 for $canihas->{-site} || $canihas->{-source} || ();
108 $ref =~ s/(?=>)/ title="updated $_"/ for $canihas->{-date} || ();
109 print "\nwith $ref browser usage statistics";
113 if ($usage) { # first() does not work inside given >:(
114 # adapt version usage to actual support data
115 my %engineuse; # prefix => usage sum
116 for my $browser (keys %versions) {
117 my $row = $canihas->{$browser} // {};
118 my $verlist = $versions{$browser} or next;
119 if ($minusage and sum(values %$row) < $minusage) {
120 delete $versions{$browser};
123 my %supported = map { $_ => 1 } @$verlist;
125 # cascade unknown versions
126 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
127 while (my ($version, $usage) = each %$row) {
128 next if defined $supported{$version};
129 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
130 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
131 $row->{$next} += $usage;
132 $row->{$version} = 0; # balance browser total
135 # build row list for each version
137 my @vershown; # $verlist replacement
138 my ($rowusage, @verrow) = (0); # replacement row tracking
140 push @verrow, $_; # queue each version
141 if (($rowusage += $row->{$_}) >= $minusage) {
142 push @vershown, [@verrow]; # add row
143 ($rowusage, @verrow) = (0); # reset row tracking
146 push @vershown, \@verrow if @verrow; # always add latest
147 @$verlist = @vershown;
150 @$verlist = map { [$_] } @$verlist;
153 # reusable aggregates (grouped by prefix (engine) and browser)
154 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
155 $row->{-total} = sum(values %$row);
158 # order browser columns by usage grouped by engine
160 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
161 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
163 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
167 # order browser columns by name grouped by engine
168 @{$_} = map { [$_] } @{$_} for values %versions;
170 $caniuse->{agents}->{$b}->{prefix} cmp
171 $caniuse->{agents}->{$a}->{prefix}
184 my $zero = $#$_ - 2; # baseline index
185 ($_->[$zero - 2] => .5), # past
186 ($_->[$zero - 1] => 10 ), # previous
187 ($_->[$zero + 2] => 0 ), # future
188 ($_->[$zero + 1] => .5), # next
189 ($_->[$zero ] => 30 ), # current
190 } $caniuse->{agents}->{$_}->{versions}
193 }; # fallback hash based on release semantics
195 # score multiplier for percentage of all browser versions
196 my $usagepct = 99.99 / sum(
197 map { $_->{-total} // values %{$_} }
198 map { $canihas->{$_} }
203 $_->{usage} = featurescore($_->{stats}) * $usagepct
204 for values %{ $caniuse->{data} };
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 my $span (@{ $versions{$browser} }) {
238 my $lastver = first {
239 $caniuse->{agents}->{$browser}->{version_list}->{$_}->{release_date} # stable
241 printf('<td title="%s"%s>%s',
243 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{ @{$span} }) * $usagepct),
244 'version ' . showversions(@{$span}, undef),
246 $_ ? () : '(development)'
247 } $caniuse->{agents}->{$browser}->{version_list}->{$lastver}->{release_date}),
249 !defined $lastver && ' class="ex"',
250 showversions($lastver // $span->[0]),
255 say '<tfoot>', $header;
257 # prefix indicates browser family; count adjacent families
258 my (@families, %familycount);
259 for my $browser (@browsers) {
260 my $family = $caniuse->{agents}->{$browser}->{prefix};
261 push @families, $family unless $familycount{$family};
262 $familycount{$family} += @{ $versions{$browser} };
265 print "\n", '<tr class="cat">';
266 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
271 # relative amount of support for given feature
273 if (my $row = shift) {
275 while (my ($browser, $versions) = each %$row) {
276 ref $versions eq 'HASH' or next;
278 for my $version (@{ $caniuse->{agents}->{$browser}->{versions} }) {
279 my $status = $versions->{$version} // $prev;
280 $status =~ s/\h\#\d+//g;
281 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
288 while (my ($browser, $vercols) = each %versions) {
289 my $div = 0; # multiplier exponent (decreased to lower value)
290 my @vers = map { $row->{$browser}->{$_} } @$vercols;
291 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
292 my @future; # find upcoming releases (after current)
293 for (reverse @$vercols) {
294 last if $_ eq $current;
295 push @future, pop @vers;
296 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
298 splice @vers, -1, 0, @future; # move ahead to decrease precedence
300 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
309 s/\r\n?/\n/g; # windows returns
310 s/\h* $//gmx; # trailing whitespace
311 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
313 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
314 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
323 s{ \[ ([^]]*) \] \( [^)]* \) }{$1}gx; # strip link urls
330 my $row = $caniuse->{data}->{$id};
332 for ($row->{categories}) {
333 my $cell = $_ ? lc $_->[0] : '-';
334 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
335 printf '<th title="%s">%s', join(' + ', @$_), $cell;
339 sprintf('<a href="%s" onclick="%s">%s</a>',
341 sprintf("try { %s; return false } catch(err) { return true }",
342 "document.getElementById('$id').classList.toggle('target')",
347 print '<div class=aside>';
349 for formatnotes($row->{description}, $row->{notes} || ());
350 if (my %notes = %{ $row->{notes_by_num} }) {
351 say '<p>Browser-specific notes:';
352 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
355 printf 'Resources: %s.', join(', ', map {
356 showlink($_->{title}, $_->{url})
357 } @$_) for grep { @$_ } $row->{links} // ();
358 printf '<br>Parent feature: %s.', join(', ', map {
359 showlink($caniuse->{data}->{$_}->{title}, "#$_")
360 } $_) for $row->{parent} || ();
366 my $row = $caniuse->{data}->{$id};
368 for ($row->{status}) {
369 my $cell = $_ // '-';
370 $cell = showlink($cell, $_) for $row->{spec} // ();
371 printf '<td title="%s" class="l %s">%s',
372 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
377 my ($id, $browser) = @_;
378 my $feature = $caniuse->{data}->{$id};
379 my $data = $feature->{stats}->{$browser};
380 if (ref $data eq 'ARRAY') {
381 # special case for unsupported
384 keys %{ $caniuse->{agents}->{$browser}->{version_list} }
389 for my $ver (@{ $versions{$browser} }, undef) {
391 !defined $ver ? undef : # last column if nameless
392 ref $data ne 'HASH' ? '' : # unclassified if no support hash
393 (first { defined } @{$data}{ reverse @{$ver} }) # last known version
394 // $prev # inherit from predecessor
397 if (defined $prev and not $prev ~~ $compare) {
399 my @vercover = (map { @{$_} } @span); # accumulated conforming versions
400 for ($ver ? @{$ver} : ()) {
401 last if defined $data->{$_}; # until different
402 push @vercover, $_; # matches from next span start
404 my $usage = sum(@{ $canihas->{$browser} }{@vercover});
406 # strip #\d note references from support class
408 push @notes, $feature->{notes_by_num}->{$1}
409 while $prev =~ s/\h \# (\d+) \b//x;
411 # prepare version hover details
412 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(' ',
413 (map { ref $_ eq 'CODE' ? $_->($browser, $vercover[0]) : $_ }
414 map { $DSTATS{$_} // () }
415 map { split / /, $_ }
418 'in', $caniuse->{agents}->{$browser}->{abbr},
419 showversions(@vercover, undef),
421 $title .= "\n$_" for notestotitle(@notes);
423 $prev .= ' #' if @notes and $prev =~ /^y/;
424 printf('<td class="%s" colspan="%d" title="%s">%s',
427 !$usage ? ('p0') : ('p',
428 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
429 sprintf('p%02d', $usage * ($usagepct - .0001)),
434 showversions($span[0]->[0], @span > 1 && defined $ver ? $span[-1]->[-1] : ()),
440 my $startversion = first { defined $data->{ $ver->[$_] } }
441 reverse 0 .. $#{$ver}; # compare index
442 push @span, [ @{$ver}[ $startversion .. $#{$ver} ] ];
450 print '<td>', int $caniuse->{data}->{$id}->{usage};
455 $caniuse->{data}->{$b}->{usage} <=> $caniuse->{data}->{$a}->{usage}
456 } keys %{ $caniuse->{data} }) {
457 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
458 printf '<tr id="%s">', $id;
461 saybrowsercols($id, $_) for @browsers;
469 # normalised version number comparable as string (cmp)
470 $_[0] =~ m/(?:.*-|^)(\d*)(.*)/;
471 # matched (major)(.minor) of last value in range (a-B)
472 return sprintf('%02d', length $1 ? $1 : 99) . $2;
476 # title to describe minumum version and optional maximum for multiple cells
477 my @span = (map { split /-/ } grep { defined } @_);
478 return $span[0] =~ s/\.0\z//r if @_ <= 1;
480 return join('‒', @span);
487 <table class="glyphs"><tr>
488 <td class="X l5">supported
489 <td class="X l4">annotated
490 <td class="X l3">partial
491 <td class="X l2">optional
492 <td class="X l1">missing
493 <td class="X l0">unknown
494 <td class="X ex">prefixed
497 <p><: if ($usage) { :>
499 <span class=" p0">0</span> -
500 <span class="p p0 p00">.01</span> -
501 <span class="p p0 p05">1-9</span> -
502 <span class="p p1">10</span> -
503 <span class="p p2">20</span> -
504 <span class="p p5">majority</span>
506 <table class="glyphs"><tr>
507 <td class="p p1">previous version</td>
508 <td class="p p3">current</td>
509 <td class="p p0 p00">upcoming (within months)</td>
510 <td class=" p0">future (within a year)</td>
515 <ul class="legend legend-set">
516 <li>default <strong>style</strong> is
517 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
518 <li><strong>usage</strong> source is
519 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
520 <li>usage <strong>threshold</strong> is
521 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
526 <script type="text/javascript" src="/searchlocal.js"></script>
527 <script type="text/javascript"><!--
528 prependsearch(document.getElementById('intro'));