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 %{$_} } values %{$canihas}
207 $_->{usage} = featurescore($_->{stats}) * $usagepct
208 for values %{ $caniuse->{data} };
210 print '<table class="mapped">';
211 print '<col span="3">'; # should match first thead row
212 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
213 say '</colgroup><col>';
215 my $header = join('',
217 '<th colspan="3" rowspan="2">feature',
219 my $name = $caniuse->{agents}->{$_}->{browser};
220 sprintf('<th colspan="%d" class="%s" title="%s">%s',
221 scalar @{ $versions{$_} },
222 join(' ', map {"b-a-$_"} grep {$_}
223 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
226 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
230 length $name <= (3 * @{ $versions{$_} }) ? $name
231 : $caniuse->{agents}->{$_}->{abbr};
237 print '<thead>', $header;
238 # preceding row without any colspan to work around gecko bug
240 for my $browser (@browsers) {
241 for my $span (@{ $versions{$browser} }) {
242 my $lastver = first {
243 !defined $caniuse->{agents}->{$browser}->{verrelease}->{$_} # stable
245 printf('<td title="%s"%s>%s',
247 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{ @{$span} }) * $usagepct),
248 'version ' . showversions(@{$span}, undef),
249 $span->[-1] eq $lastver ? () : '(development)',
251 !defined $lastver && ' class="ex"',
252 showversions($lastver // $span->[0]),
257 say '<tfoot>', $header;
259 # prefix indicates browser family; count adjacent families
260 my (@families, %familycount);
261 for my $browser (@browsers) {
262 my $family = $caniuse->{agents}->{$browser}->{prefix};
263 push @families, $family unless $familycount{$family};
264 $familycount{$family} += @{ $versions{$browser} };
267 print "\n", '<tr class="cat">';
268 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
273 # relative amount of support for given feature
275 if (my $row = shift) {
277 while (my ($browser, $versions) = each %$row) {
278 ref $versions eq 'HASH' or next;
279 while (my ($version, $status) = each %$versions) {
280 $status =~ s/\h\#\d+//g;
281 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
287 while (my ($browser, $vercols) = each %versions) {
288 my $div = 0; # multiplier exponent (decreased to lower value)
289 my @vers = map { $row->{$browser}->{$_} } @$vercols;
290 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
291 my @future; # find upcoming releases (after current)
292 for (reverse @$vercols) {
293 last if $_ eq $current;
294 push @future, pop @vers;
295 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
297 splice @vers, -1, 0, @future; # move ahead to decrease precedence
299 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
308 s/\r\n?/\n/g; # windows returns
309 s/\h* $//gmx; # trailing whitespace
310 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
312 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
313 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
322 s{ \[ ([^]]*) \] \( [^)]* \) }{$1}gx; # strip link urls
329 my $row = $caniuse->{data}->{$id};
331 for ($row->{categories}) {
332 my $cell = $_ ? lc $_->[0] : '-';
333 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
334 printf '<th title="%s">%s', join(' + ', @$_), $cell;
338 sprintf('<a href="%s" onclick="%s">%s</a>',
340 sprintf("try { %s; return false } catch(err) { return true }",
341 "document.getElementById('$id').classList.toggle('target')",
346 print '<div class=aside>';
348 for formatnotes($row->{description}, $row->{notes} || ());
349 if (my %notes = %{ $row->{notes_by_num} }) {
350 say '<p>Browser-specific notes:';
351 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
354 printf 'Resources: %s.', join(', ', map {
355 showlink($_->{title}, $_->{url})
356 } @$_) for grep { @$_ } $row->{links} // ();
357 printf '<br>Parent feature: %s.', join(', ', map {
358 showlink($caniuse->{data}->{$_}->{title}, "#$_")
359 } $_) for $row->{parent} || ();
365 my $row = $caniuse->{data}->{$id};
367 for ($row->{status}) {
368 my $cell = $_ // '-';
369 $cell = showlink($cell, $_) for $row->{spec} // ();
370 printf '<td title="%s" class="l %s">%s',
371 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
376 my ($id, $browser) = @_;
377 my $feature = $caniuse->{data}->{$id};
378 my $data = $feature->{stats}->{$browser};
379 if (ref $data eq 'ARRAY') {
380 # special case for unsupported
381 my $release = $caniuse->{agents}->{$browser}->{verrelease};
383 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
388 for my $ver (@{ $versions{$browser} }, undef) {
390 !defined $ver ? undef : # last column if nameless
391 ref $data ne 'HASH' ? '' : # unclassified if no support hash
392 $data->{ $ver->[-1] } // $prev # known or inherit from predecessor
393 // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
394 ~~ 'n' && 'n' # first known version is unsupported
397 unless (!defined $prev or $prev ~~ $compare) {
398 my @vercover = (map { @{$_} } @span);
399 for ($ver ? @{$ver} : ()) {
400 $data->{$_} eq $data->{$vercover[-1]} or last;
401 push @vercover, $_; # matches from next span start
403 my $usage = sum(@{ $canihas->{$browser} }{@vercover});
405 # strip #\d note references from support class
407 push @notes, $feature->{notes_by_num}->{$1}
408 while $prev =~ s/\h \# (\d+) \b//x;
410 # prepare version hover details
411 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(' ',
412 (map { ref $_ eq 'CODE' ? $_->($browser, $vercover[0]) : $_ }
413 map { $DSTATS{$_} // () }
414 map { split / /, $_ }
417 'in', $caniuse->{agents}->{$browser}->{abbr},
418 showversions(@vercover, undef),
420 $title .= "\n$_" for notestotitle(@notes);
422 $prev .= ' #' if @notes and $prev =~ /^y/;
423 printf('<td class="%s" colspan="%d" title="%s">%s',
426 !$usage ? ('p0') : ('p',
427 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
428 sprintf('p%02d', $usage * ($usagepct - .0001)),
433 showversions($span[0]->[0], @span > 1 && defined $ver ? $span[-1]->[-1] : ()),
438 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
445 print '<td>', int $caniuse->{data}->{$id}->{usage};
450 $caniuse->{data}->{$b}->{usage} <=> $caniuse->{data}->{$a}->{usage}
451 } keys %{ $caniuse->{data} }) {
452 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
453 printf '<tr id="%s">', $id;
456 saybrowsercols($id, $_) for @browsers;
464 # normalised version number comparable as string (cmp)
465 $_[0] =~ m/(?:.*-|^)(\d*)(.*)/;
466 # matched (major)(.minor) of last value in range (a-B)
467 return sprintf('%02d', length $1 ? $1 : 99) . $2;
471 # title to describe minumum version and optional maximum for multiple cells
472 my @span = (map { split /-/ } grep { defined } @_);
473 return $span[0] =~ s/\.0\z//r if @_ <= 1;
475 return join('‒', @span);
482 <table class="glyphs"><tr>
483 <td class="X l5">supported
484 <td class="X l4">annotated
485 <td class="X l3">partial
486 <td class="X l2">optional
487 <td class="X l1">missing
488 <td class="X l0">unknown
489 <td class="X ex">prefixed
492 <p><: if ($usage) { :>
494 <span class=" p0">0</span> -
495 <span class="p p0 p00">.01</span> -
496 <span class="p p0 p05">1-9</span> -
497 <span class="p p1">10</span> -
498 <span class="p p2">20</span> -
499 <span class="p p5">majority</span>
501 <table class="glyphs"><tr>
502 <td class="p p1">previous version</td>
503 <td class="p p3">current</td>
504 <td class="p p0 p00">upcoming (within months)</td>
505 <td class=" p0">future (within a year)</td>
510 <ul class="legend legend-set">
511 <li>default <strong>style</strong> is
512 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
513 <li><strong>usage</strong> source is
514 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
515 <li>usage <strong>threshold</strong> is
516 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
521 <script type="text/javascript" src="/searchlocal.js"></script>
522 <script type="text/javascript"><!--
523 prependsearch(document.getElementById('intro'));