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 = Data('data/browser/support');
44 p => 'plugin required',
47 d => '(disabled by default)',
52 $caniuse->{agents}->{$_[0]}->{version_list}->{$_[1]}->{prefix}
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} = [@{ $row->{versions} }];
79 my $ref = showlink('Can I use', 'https://caniuse.com/');
80 $ref =~ s/(?=>)/ title="updated $_"/
81 for map { s/[\sT].*//r } $caniuse->{-date} || ();
82 $ref = "Fyrd's $ref page";
83 say '<p id="intro">Alternate rendition of '.$ref;
85 my ($canihas, $usage);
86 my $minusage = $get{threshold} // 1;
87 given ($get{usage} // 'wm') {
91 when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
93 'Invalid browser usage data request',
94 'Identifier must be alphanumeric name or <q>0</q>.',
97 $canihas = eval { Data("data/browser/usage-$_") } or do {
98 Alert('Browser usage data not found', $@);
102 my $ref = $canihas->{-title} || 'unknown';
103 $ref = showlink($ref, $_)
104 for $canihas->{-site} || $canihas->{-source} || ();
105 $ref =~ s/(?=>)/ title="updated $_"/ for $canihas->{-date} || ();
106 print "\nwith $ref browser usage statistics";
110 if ($usage) { # first() does not work inside given >:(
111 # adapt version usage to actual support data
112 my %engineuse; # prefix => usage sum
113 for my $browser (keys %versions) {
114 my $row = $canihas->{$browser} // {};
115 my $verlist = $versions{$browser} or next;
116 if ($minusage and sum(values %$row) < $minusage) {
117 delete $versions{$browser};
120 my %supported = map { $_ => 1 } @$verlist;
122 # cascade unknown versions
123 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
124 while (my ($version, $usage) = each %$row) {
125 next if defined $supported{$version};
126 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
127 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
128 $row->{$next} += $usage;
129 $row->{$version} = 0; # balance browser total
132 # build row list for each version
134 my @vershown; # $verlist replacement
135 my ($rowusage, @verrow) = (0); # replacement row tracking
137 push @verrow, $_; # queue each version
138 if (($rowusage += $row->{$_}) >= $minusage) {
139 push @vershown, [@verrow]; # add row
140 ($rowusage, @verrow) = (0); # reset row tracking
143 push @vershown, \@verrow if @verrow; # always add latest
144 @$verlist = @vershown;
147 @$verlist = map { [$_] } @$verlist;
150 # reusable aggregates (grouped by prefix (engine) and browser)
151 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
152 $row->{-total} = sum(values %$row);
155 # order browser columns by usage grouped by engine
157 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
158 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
160 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
164 # order browser columns by name grouped by engine
165 @{$_} = map { [$_] } @{$_} for values %versions;
167 $caniuse->{agents}->{$b}->{prefix} cmp
168 $caniuse->{agents}->{$a}->{prefix}
181 my $zero = $#$_ - 2; # baseline index
182 ($_->[$zero - 2] => .5), # past
183 ($_->[$zero - 1] => 10 ), # previous
184 ($_->[$zero + 2] => 0 ), # future
185 ($_->[$zero + 1] => .5), # next
186 ($_->[$zero ] => 30 ), # current
187 } $caniuse->{agents}->{$_}->{versions}
190 }; # fallback hash based on release semantics
192 # score multiplier for percentage of all browser versions
193 my $usagepct = 99.99 / sum(
194 map { $_->{-total} // values %{$_} }
195 map { $canihas->{$_} }
200 $_->{usage} = featurescore($_->{stats}) * $usagepct
201 for values %{ $caniuse->{data} };
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 my $span (@{ $versions{$browser} }) {
235 my $lastver = first {
236 $caniuse->{agents}->{$browser}->{version_list}->{$_}->{release_date} # stable
238 printf('<td title="%s"%s>%s',
240 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{ @{$span} }) * $usagepct),
241 'version ' . showversions(@{$span}, undef),
243 $_ ? sprintf('(released %d)', $_/3600/24/365.25 + 1970) : '(development)'
244 } $caniuse->{agents}->{$browser}->{version_list}->{$lastver}->{release_date}),
246 !defined $lastver && ' class="ex"',
247 showversions($lastver // $span->[0]),
252 say '<tfoot>', $header;
254 # prefix indicates browser family; count adjacent families
255 my (@families, %familycount);
256 for my $browser (@browsers) {
257 my $family = $caniuse->{agents}->{$browser}->{prefix};
258 push @families, $family unless $familycount{$family};
259 $familycount{$family} += @{ $versions{$browser} };
262 print "\n", '<tr class="cat">';
263 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
268 # relative amount of support for given feature
270 if (my $row = shift) {
272 while (my ($browser, $versions) = each %$row) {
273 ref $versions eq 'HASH' or next;
275 for my $version (@{ $caniuse->{agents}->{$browser}->{versions} }) {
276 my $status = $versions->{$version} // $prev;
277 $status =~ s/\h\#\d+//g;
278 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
285 while (my ($browser, $vercols) = each %versions) {
286 my $div = 0; # multiplier exponent (decreased to lower value)
287 my @vers = map { $row->{$browser}->{$_} } @$vercols;
288 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
289 my @future; # find upcoming releases (after current)
290 for (reverse @$vercols) {
291 last if $_ eq $current;
292 push @future, pop @vers;
293 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
295 splice @vers, -1, 0, @future; # move ahead to decrease precedence
297 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
306 s/\r\n?/\n/g; # windows returns
307 s/\h* $//gmx; # trailing whitespace
308 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
310 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
311 s{ \(\K (?: \Qhttps://caniuse.com\E )? (?: /? \#feat= | / ) }{#}gx;
312 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
321 s{ \[ ([^]]*) \] \( [^)]* \) }{$1}gx; # strip link urls
328 my $row = $caniuse->{data}->{$id};
330 for ($row->{categories}) {
331 my $cell = $_ ? lc $_->[0] : '-';
332 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
333 printf '<th title="%s">%s', join(' + ', @$_), $cell;
337 sprintf('<a href="%s" onclick="%s">%s</a>',
339 sprintf("try { %s; return false } catch(err) { return true }",
340 "document.getElementById('$id').classList.toggle('target')",
345 print '<div class=aside>';
347 for formatnotes($row->{description}, $row->{notes} || ());
348 if (my %notes = %{ $row->{notes_by_num} }) {
349 say '<p>Browser-specific notes:';
350 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
353 printf 'Resources: %s.', join(', ', map {
354 showlink($_->{title}, $_->{url})
355 } @$_) for grep { @$_ } $row->{links} // ();
356 printf '<br>Parent feature: %s.', join(', ', map {
357 showlink($caniuse->{data}->{$_}->{title}, "#$_")
358 } $_) for $row->{parent} || ();
364 my $row = $caniuse->{data}->{$id};
366 for ($row->{status}) {
367 my $cell = $_ // '-';
368 $cell = showlink($cell, $_) for $row->{spec} // ();
369 printf '<td title="%s" class="l %s">%s',
370 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
375 my ($id, $browser) = @_;
376 my $feature = $caniuse->{data}->{$id};
377 my $data = $feature->{stats}->{$browser};
378 if (ref $data eq 'ARRAY') {
379 # special case for unsupported
382 keys %{ $caniuse->{agents}->{$browser}->{version_list} }
387 for my $ver (@{ $versions{$browser} }, undef) {
389 !defined $ver ? undef : # last column if nameless
390 ref $data ne 'HASH' ? '' : # unclassified if no support hash
391 (first { defined } @{$data}{ reverse @{$ver} }) # last known version
392 // $prev # inherit from predecessor
395 if (defined $prev and not $prev ~~ $compare) {
397 my @vercover = (map { @{$_} } @span); # accumulated conforming versions
398 for ($ver ? @{$ver} : ()) {
399 last if defined $data->{$_}; # until different
400 push @vercover, $_; # matches from next span start
402 my $usage = sum(@{ $canihas->{$browser} }{@vercover});
404 # strip #\d note references from support class
406 push @notes, $feature->{notes_by_num}->{$1}
407 while $prev =~ s/\h \# (\d+) \b//x;
409 # prepare version hover details
410 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(' ',
411 (map { ref $_ eq 'CODE' ? $_->($browser, $vercover[0]) : $_ }
412 map { $DSTATS{$_} // () }
413 map { split / /, $_ }
416 'in', $caniuse->{agents}->{$browser}->{abbr},
417 showversions(@vercover, undef),
419 $title .= "\n$_" for notestotitle(@notes);
421 $prev .= ' #' if @notes and $prev =~ /^y/;
422 printf('<td class="%s" colspan="%d" title="%s">%s',
425 !$usage ? ('p0') : ('p',
426 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
427 sprintf('p%02d', $usage * ($usagepct - .0001)),
432 showversions($span[0]->[0], @span > 1 && defined $ver ? $span[-1]->[-1] : ()),
438 my $startversion = first { defined $data->{ $ver->[$_] } }
439 reverse 0 .. $#{$ver}; # compare index
440 push @span, [ @{$ver}[ $startversion .. $#{$ver} ] ];
448 print '<td>', int $caniuse->{data}->{$id}->{usage};
453 $caniuse->{data}->{$b}->{usage} <=> $caniuse->{data}->{$a}->{usage}
454 } keys %{ $caniuse->{data} }) {
455 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
456 printf '<tr id="%s">', $id;
459 saybrowsercols($id, $_) for @browsers;
467 # normalised version number comparable as string (cmp)
468 $_[0] =~ m/(?:.*-|^)(\d*)(.*)/;
469 # matched (major)(.minor) of last value in range (a-B)
470 return sprintf('%03d', length $1 ? $1 : 999) . $2;
474 # title to describe minumum version and optional maximum for multiple cells
475 my @span = (map { split /-/ } grep { defined } @_);
476 return $span[0] =~ s/\.0\z//r if @_ <= 1;
478 return join('‒', @span);
485 <table class="glyphs"><tr>
486 <td class="X l5">supported
487 <td class="X l4">annotated
488 <td class="X l3">partial
489 <td class="X l2">optional
490 <td class="X l1">missing
491 <td class="X l0">unknown
492 <td class="X ex">prefixed
495 <p><: if ($usage) { :>
497 <span class=" p0">0</span> -
498 <span class="p p0 p00">.01</span> -
499 <span class="p p0 p05">1-9</span> -
500 <span class="p p1">10</span> -
501 <span class="p p2">20</span> -
502 <span class="p p5">majority</span>
504 <table class="glyphs"><tr>
505 <td class="p p1">previous version</td>
506 <td class="p p3">current</td>
507 <td class="p p0 p00">upcoming (within months)</td>
508 <td class=" p0">future (within a year)</td>
513 <ul class="legend legend-set">
514 <li>default <strong>style</strong> is
515 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
516 <li><strong>usage</strong> source is
517 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
518 <li>usage <strong>threshold</strong> is
519 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
524 <script type="text/javascript" src="/searchlocal.js"></script>
525 <script type="text/javascript"><!--
526 prependsearch(document.getElementById('intro'));