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 data => ['data/browser/support.inc.pl'],
20 say "<h1>Browser compatibility</h1>\n";
22 my $caniuse = Data('data/browser/support');
43 p => 'plugin required',
46 d => '(disabled by default)',
51 $caniuse->{agents}->{$_[0]}->{version_list}->{$_[1]}->{prefix}
52 // $caniuse->{agents}->{$_[0]}->{prefix} // (),
56 my %PSTATS = ( # score percentage
58 a => .5, 'a x' => .5, 'a d' => .2,
60 n => 0, 'n d' => .2, 'n x d' => .2,
64 unoff => 'l1', # unofficial
66 cr => 'l3', # candidate
67 pr => 'l3', # proposed
68 rec => 'l5', # recommendation
70 ietf => 'l0', # standard
71 other => 'l0', # non-w3
74 while (my ($browser, $row) = each %{ $caniuse->{agents} }) {
75 $versions{$browser} = [@{ $row->{versions} }];
78 my $ref = showlink('Can I use', 'https://caniuse.com/');
79 $ref =~ s/(?=>)/ title="updated $_"/
80 for map { s/[\sT].*//r } $caniuse->{-date} || ();
81 $ref = "Fyrd's $ref page";
82 say '<p id="intro">Alternate rendition of '.$ref;
84 my ($canihas, $usage);
85 my $minusage = $get{threshold} // 1;
86 given ($get{usage} // 'wm') {
90 when (!m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
92 'Invalid browser usage data request',
93 'Identifier must be alphanumeric name or <q>0</q>.',
96 $canihas = eval { Data("data/browser/usage-$_") } or do {
97 Alert('Browser usage data not found', $@);
101 my $ref = $canihas->{-title} || 'unknown';
102 $ref = showlink($ref, $_)
103 for $canihas->{-site} || $canihas->{-source} || ();
104 $ref =~ s/(?=>)/ title="updated $_"/ for $canihas->{-date} || ();
105 print "\nwith $ref browser usage statistics";
109 if ($usage) { # first() does not work inside given >:(
110 # adapt version usage to actual support data
111 my %engineuse; # prefix => usage sum
112 for my $browser (keys %versions) {
113 my $row = $canihas->{$browser} // {};
114 my $verlist = $versions{$browser} or next;
115 if ($minusage and sum(values %$row) < $minusage) {
116 delete $versions{$browser};
119 my %supported = map { $_ => 1 } @$verlist;
121 # cascade unknown versions
122 $row->{$_} //= undef for @$verlist; # ensure stable keys during iteration
123 while (my ($version, $usage) = each %$row) {
124 next if defined $supported{$version};
125 my $next = first { paddedver($_) ge paddedver($version) } @$verlist
126 or warn("No fallback found for $browser v$version; $usage% ignored"), next;
127 $row->{$next} += $usage;
128 $row->{$version} = 0; # balance browser total
131 # build row list for each version
133 my @vershown; # $verlist replacement
134 my ($rowusage, @verrow) = (0); # replacement row tracking
136 push @verrow, $_; # queue each version
137 if (($rowusage += $row->{$_}) >= $minusage) {
138 push @vershown, [@verrow]; # add row
139 ($rowusage, @verrow) = (0); # reset row tracking
142 push @vershown, \@verrow if @verrow; # always add latest
143 @$verlist = @vershown;
146 @$verlist = map { [$_] } @$verlist;
149 # reusable aggregates (grouped by prefix (engine) and browser)
150 $engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
151 $row->{-total} = sum(values %$row);
154 # order browser columns by usage grouped by engine
156 $engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
157 $engineuse{ $caniuse->{agents}->{$a}->{prefix} }
159 $canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
163 # order browser columns by name grouped by engine
164 @{$_} = map { [$_] } @{$_} for values %versions;
166 $caniuse->{agents}->{$b}->{prefix} cmp
167 $caniuse->{agents}->{$a}->{prefix}
180 my $zero = $#$_ - 2; # baseline index
181 ($_->[$zero - 2] => .5), # past
182 ($_->[$zero - 1] => 10 ), # previous
183 ($_->[$zero + 2] => 0 ), # future
184 ($_->[$zero + 1] => .5), # next
185 ($_->[$zero ] => 30 ), # current
186 } $caniuse->{agents}->{$_}->{versions}
189 }; # fallback hash based on release semantics
191 # score multiplier for percentage of all browser versions
192 my $usagepct = 99.99 / sum(
193 map { $_->{-total} // values %{$_} }
194 map { $canihas->{$_} }
199 $_->{usage} = featurescore($_->{stats}) * $usagepct
200 for values %{ $caniuse->{data} };
202 print '<table class="mapped">';
203 print '<col span="3">'; # should match first thead row
204 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
205 say '</colgroup><col>';
207 my $header = join('',
209 '<th colspan="3" rowspan="2">feature',
211 my $name = $caniuse->{agents}->{$_}->{browser};
212 sprintf('<th colspan="%d" class="%s" title="%s">%s',
213 scalar @{ $versions{$_} },
214 join(' ', map {"b-a-$_"} grep {$_}
215 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
218 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
222 length $name <= (3 * @{ $versions{$_} }) ? $name
223 : $caniuse->{agents}->{$_}->{abbr};
229 print '<thead>', $header;
230 # preceding row without any colspan to work around gecko bug
232 for my $browser (@browsers) {
233 for my $span (@{ $versions{$browser} }) {
234 my $lastver = first {
235 $caniuse->{agents}->{$browser}->{version_list}->{$_}->{release_date} # stable
237 printf('<td title="%s"%s>%s',
239 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{ @{$span} }) * $usagepct),
240 'version ' . showversions(@{$span}, undef),
242 $_ ? sprintf('(released %d)', $_/3600/24/365.25 + 1970) : '(development)'
243 } $caniuse->{agents}->{$browser}->{version_list}->{$lastver}->{release_date}),
245 !defined $lastver && ' class="ex"',
246 showversions($lastver // $span->[0]),
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;
274 for my $version (@{ $caniuse->{agents}->{$browser}->{versions} }) {
275 my $status = $versions->{$version} // $prev;
276 $status =~ s/\h\#\d+//g;
277 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
284 while (my ($browser, $vercols) = each %versions) {
285 my $div = 0; # multiplier exponent (decreased to lower value)
286 my @vers = map { $row->{$browser}->{$_} } @$vercols;
287 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
288 my @future; # find upcoming releases (after current)
289 for (reverse @$vercols) {
290 last if $_ eq $current;
291 push @future, pop @vers;
292 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
294 splice @vers, -1, 0, @future; # move ahead to decrease precedence
296 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
305 s/\r\n?/\n/g; # windows returns
306 s/\h* $//gmx; # trailing whitespace
307 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
309 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
310 s{ \(\K (?: \Qhttps://caniuse.com\E )? (?: /? \#feat= | / ) }{#}gx;
311 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
320 s{ \[ ([^]]*) \] \( [^)]* \) }{$1}gx; # strip link urls
327 my $row = $caniuse->{data}->{$id};
329 for ($row->{categories}) {
330 my $cell = $_ ? lc $_->[0] : '-';
331 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
332 printf '<th title="%s">%s', join(' + ', @$_), $cell;
336 sprintf('<a href="%s" onclick="%s">%s</a>',
338 sprintf("try { %s; return false } catch(err) { return true }",
339 "document.getElementById('$id').classList.toggle('target')",
344 print '<div class=aside>';
346 for formatnotes($row->{description}, $row->{notes} || ());
347 if (my %notes = %{ $row->{notes_by_num} }) {
348 say '<p>Browser-specific notes:';
349 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
352 printf 'Resources: %s.', join(', ', map {
353 showlink($_->{title}, $_->{url})
354 } @$_) for grep { @$_ } $row->{links} // ();
355 printf '<br>Parent feature: %s.', join(', ', map {
356 showlink($caniuse->{data}->{$_}->{title}, "#$_")
357 } $_) for $row->{parent} || ();
363 my $row = $caniuse->{data}->{$id};
365 for ($row->{status}) {
366 my $cell = $_ // '-';
367 $cell = showlink($cell, $_) for $row->{spec} // ();
368 printf '<td title="%s" class="l %s">%s',
369 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
374 my ($id, $browser) = @_;
375 my $feature = $caniuse->{data}->{$id};
376 my $data = $feature->{stats}->{$browser};
377 if (ref $data eq 'ARRAY') {
378 # special case for unsupported
381 keys %{ $caniuse->{agents}->{$browser}->{version_list} }
386 for my $ver (@{ $versions{$browser} }, undef) {
388 !defined $ver ? undef : # last column if nameless
389 ref $data ne 'HASH' ? '' : # unclassified if no support hash
390 (first { defined } @{$data}{ reverse @{$ver} }) # last known version
391 // $prev # inherit from predecessor
394 if (defined $prev and not $prev ~~ $compare) {
396 my @vercover = (map { @{$_} } @span); # accumulated conforming versions
397 for ($ver ? @{$ver} : ()) {
398 last if defined $data->{$_}; # until different
399 push @vercover, $_; # matches from next span start
401 my $usage = sum(@{ $canihas->{$browser} }{@vercover});
403 # strip #\d note references from support class
405 push @notes, $feature->{notes_by_num}->{$1}
406 while $prev =~ s/\h \# (\d+) \b//x;
408 # prepare version hover details
409 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(' ',
410 (map { ref $_ eq 'CODE' ? $_->($browser, $vercover[0]) : $_ }
411 map { $DSTATS{$_} // () }
412 map { split / /, $_ }
415 'in', $caniuse->{agents}->{$browser}->{abbr},
416 showversions(@vercover, undef),
418 $title .= "\n$_" for notestotitle(@notes);
420 $prev .= ' #' if @notes and $prev =~ /^y/;
421 printf('<td class="%s" colspan="%d" title="%s">%s',
424 !$usage ? ('p0') : ('p',
425 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
426 sprintf('p%02d', $usage * ($usagepct - .0001)),
431 showversions($span[0]->[0], @span > 1 && defined $ver ? $span[-1]->[-1] : ()),
437 my $startversion = first { defined $data->{ $ver->[$_] } }
438 reverse 0 .. $#{$ver}; # compare index
439 push @span, [ @{$ver}[ $startversion .. $#{$ver} ] ];
447 print '<td>', int $caniuse->{data}->{$id}->{usage};
452 $caniuse->{data}->{$b}->{usage} <=> $caniuse->{data}->{$a}->{usage}
453 } keys %{ $caniuse->{data} }) {
454 $caniuse->{data}->{$id}->{stats} or next; # skip metadata [summary]
455 printf '<tr id="%s">', $id;
458 saybrowsercols($id, $_) for @browsers;
466 # normalised version number comparable as string (cmp)
467 $_[0] =~ m/(?:.*-|^)(\d*)(.*)/;
468 # matched (major)(.minor) of last value in range (a-B)
469 return sprintf('%03d', length $1 ? $1 : 999) . $2;
473 # title to describe minumum version and optional maximum for multiple cells
474 my @span = (map { split /-/ } grep { defined } @_);
475 return $span[0] =~ s/\.0\z//r if @_ <= 1;
477 return join('‒', @span);
484 <table class="glyphs"><tr>
485 <td class="X l5">supported
486 <td class="X l4">annotated
487 <td class="X l3">partial
488 <td class="X l2">optional
489 <td class="X l1">missing
490 <td class="X l0">unknown
491 <td class="X ex">prefixed
494 <p><: if ($usage) { :>
496 <span class=" p0">0</span> -
497 <span class="p p0 p00">.01</span> -
498 <span class="p p0 p05">1-9</span> -
499 <span class="p p1">10</span> -
500 <span class="p p2">20</span> -
501 <span class="p p5">majority</span>
503 <table class="glyphs"><tr>
504 <td class="p p1">previous version</td>
505 <td class="p p3">current</td>
506 <td class="p p0 p00">upcoming (within months)</td>
507 <td class=" p0">future (within a year)</td>
512 <ul class="legend legend-set">
513 <li>default <strong>style</strong> is
514 <:= defined $get{style} && 'set to ' :><em><:= $style :></em>
515 <li><strong>usage</strong> source is
516 <:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
517 <li>usage <strong>threshold</strong> is
518 <:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
523 <script type="text/javascript" src="/searchlocal.js"></script>
524 <script type="text/javascript"><!--
525 prependsearch(document.getElementById('intro'));