<(common.inc.plp)><:
use List::Util qw(sum max first);
Html({
title => 'browser compatibility cheat sheet',
version => '1.6',
description => [
"Compatibility table of new web features (HTML5, CSS3, SVG, Javascript)",
"comparing support and usage share for all popular browser versions.",
],
keywords => [qw'
web browser support compatibility usage matrix available feature
html html5 css css3 svg javascript js dom mobile
ie internet explorer firefox chrome safari webkit opera
'],
data => ['data/browser/support.inc.pl'],
});
say "<h1>Browser compatibility</h1>\n";
my $caniuse = Data('data/browser/support');
my %CSTATS = (
'n' => 'l1',
'n d' => 'l2',
'n x d' => 'l2 ex',
'p d' => 'l2',
'a d' => 'l2',
'y' => 'l5',
'y #' => 'l4',
'y x' => 'l5 ex',
'y x #' => 'l4 ex',
'a' => 'l3',
'a x' => 'l3 ex',
'p' => 'l2',
'u' => 'l0',
'u d' => 'l2',
);
my %DSTATS = (
u => 'unknown',
n => 'unsupported',
p => 'plugin required',
a => 'partial',
y => 'supported',
d => '(disabled by default)',
x => sub {
join(' ',
'with prefix',
map {"-$_-"}
$caniuse->{agents}->{$_[0]}->{version_list}->{$_[1]}->{prefix}
// $caniuse->{agents}->{$_[0]}->{prefix} // (),
);
},
);
my %PSTATS = (
y => 1, 'y x' => .9,
a => .5, 'a x' => .5, 'a d' => .2,
p => .2, 'p d' => .1,
n => 0, 'n d' => .2, 'n x d' => .2,
u => 0,
);
my %CSTATUS = (
unoff => 'l1',
wd => 'l2',
cr => 'l3',
pr => 'l3',
rec => 'l5',
ls => 'l4',
ietf => 'l0',
other => 'l0',
);
my %versions;
while (my ($browser, $row) = each %{ $caniuse->{agents} }) {
$versions{$browser} = [@{ $row->{versions} }];
}
my $ref = showlink('Can I use', 'https://caniuse.com/');
$ref =~ s/(?=>)/ title="updated $_"/
for map { s/[\sT].*//r } $caniuse->{-date} || ();
$ref = "Fyrd's $ref page";
say '<p id="intro">Alternate rendition of '.$ref;
my ($canihas, $usage);
my $minusage = $get{threshold} // 1;
for ($get{usage} // 'wm') {
$_ or next;
unless (m{ \A [a-z]\w+ (?:/\d[\d-]*\d)? \z }x) {
Alert([
'Invalid browser usage data request',
'Identifier must be alphanumeric name or <q>0</q>.',
]);
next;
}
$canihas = eval { Data("data/browser/usage-$_") } or do {
Alert('Browser usage data not found', ref $@ ? @{$@} : $@);
next;
};
$usage = $_;
my $ref = $canihas->{-title} || 'unknown';
$ref = showlink($ref, $_)
for $canihas->{-site} || $canihas->{-source} || ();
$ref =~ s/(?=>)/ title="updated $_"/ for $canihas->{-date} || ();
print "\nwith $ref browser usage statistics";
}
my @browsers;
if ($usage) {
my %engineuse;
for my $browser (keys %versions) {
my $row = $canihas->{$browser} // {};
my $verlist = $versions{$browser} or next;
if ($minusage and sum(values %$row) < $minusage) {
delete $versions{$browser};
next;
}
my %supported = map { $_ => 1 } @$verlist;
$row->{$_} //= undef for @$verlist;
while (my ($version, $usage) = each %$row) {
next if defined $supported{$version};
my $next = first { paddedver($_) ge paddedver($version) } @$verlist
or warn("No fallback found for $browser v$version; $usage% ignored"), next;
$row->{$next} += $usage;
$row->{$version} = 0;
}
if ($minusage) {
my @vershown;
my ($rowusage, @verrow) = (0);
for (@$verlist) {
push @verrow, $_;
if (($rowusage += $row->{$_}) >= $minusage) {
push @vershown, [@verrow];
($rowusage, @verrow) = (0);
}
}
push @vershown, \@verrow if @verrow;
@$verlist = @vershown;
}
else {
@$verlist = map { [$_] } @$verlist;
}
$engineuse{ $caniuse->{agents}->{$browser}->{prefix} } +=
$row->{-total} = sum(values %$row);
}
@browsers = sort {
$engineuse{ $caniuse->{agents}->{$b}->{prefix} } <=>
$engineuse{ $caniuse->{agents}->{$a}->{prefix} }
||
$canihas->{$b}->{-total} <=> $canihas->{$a}->{-total}
} keys %versions;
}
else {
@{$_} = map { [$_] } @{$_} for values %versions;
@browsers = sort {
$caniuse->{agents}->{$b}->{prefix} cmp
$caniuse->{agents}->{$a}->{prefix}
||
$a cmp $b
} keys %versions;
}
:>.
</p>
<:
$canihas ||= {
map {
$_ => +{
map {
my $zero = $#$_ - 2;
($_->[$zero - 2] => .5),
($_->[$zero - 1] => 10 ),
($_->[$zero + 2] => 0 ),
($_->[$zero + 1] => .5),
($_->[$zero ] => 30 ),
} $caniuse->{agents}->{$_}->{versions}
}
} @browsers
};
my $usagepct = 99.99 / sum(
map { $_->{-total} // values %{$_} }
map { $canihas->{$_} }
grep { !/^-/ }
keys %{$canihas}
);
$_->{usage} = featurescore($_->{stats}) * $usagepct
for values %{ $caniuse->{data} };
print '<table class="mapped">';
print '<col span="3">';
printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
say '</colgroup><col>';
my $header = join('',
'<tr>',
'<th colspan="3" rowspan="2">feature',
(map {
my $name = $caniuse->{agents}->{$_}->{browser};
sprintf('<th colspan="%d" class="%s" title="%s">%s',
scalar @{ $versions{$_} },
join(' ', map {"b-a-$_"} grep {$_}
$_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
),
join(' ',
sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
$name,
),
do {
length $name <= (3 * @{ $versions{$_} }) ? $name
: $caniuse->{agents}->{$_}->{abbr};
},
)
} @browsers),
'<th rowspan="2">%',
);
print '<thead>', $header;
print "\n<tr>";
for my $browser (@browsers) {
for my $span (@{ $versions{$browser} }) {
my $lastver = first {
$caniuse->{agents}->{$browser}->{version_list}->{$_}->{release_date}
} reverse @{$span};
printf('<td title="%s"%s>%s',
join(' ',
sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{ @{$span} }) * $usagepct),
'version ' . showversions(@{$span}, undef),
(map {
$_ ? sprintf('(released %d)', $_/3600/24/365.25 + 1970) : '(development)'
} $caniuse->{agents}->{$browser}->{version_list}->{$lastver}->{release_date}),
),
!defined $lastver && ' class="ex"',
showversions($lastver // $span->[0]),
);
}
}
say '</thead>';
say '<tfoot>', $header;
{
my (@families, %familycount);
for my $browser (@browsers) {
my $family = $caniuse->{agents}->{$browser}->{prefix};
push @families, $family unless $familycount{$family};
$familycount{$family} += @{ $versions{$browser} };
}
print "\n", '<tr class="cat">';
printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
}
say '</tfoot>';
sub featurescore {
my $rank = 0;
if (my $row = shift) {
if ($canihas) {
while (my ($browser, $versions) = each %$row) {
ref $versions eq 'HASH' or next;
my $prev;
for my $version (@{ $caniuse->{agents}->{$browser}->{versions} }) {
my $status = $versions->{$version} // $prev;
$status =~ s/\h\#\d+//g;
$rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
$prev = $status;
}
}
return $rank;
}
while (my ($browser, $vercols) = each %versions) {
my $div = 0;
my @vers = map { $row->{$browser}->{$_} } @$vercols;
if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
my @future;
for (reverse @$vercols) {
last if $_ eq $current;
push @future, pop @vers;
$_ eq 'u' and $_ = $vers[-1] for $future[-1];
}
splice @vers, -1, 0, @future;
}
$rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
}
}
return $rank;
}
sub formatnotes {
my @html = @_;
for (@html) {
s/\r\n?/\n/g;
s/\h* $//gmx;
s/(?<= [^.\n]) $/./gmx;
Entity($_);
s{ ` ([^`]*) ` }{<code>$1</code>}gx;
s{ \(\K (?: \Qhttps://caniuse.com\E )? (?: /? \#feat= | / ) }{#}gx;
s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
}
return @html;
}
sub notestotitle {
my @notes = @_;
for (@notes) {
EscapeHTML($_);
s{ \[ ([^]]*) \] \( [^)]* \) }{$1}gx;
}
return @notes;
}
sub saytitlecol {
my ($id) = @_;
my $row = $caniuse->{data}->{$id};
for ($row->{categories}) {
my $cell = $_ ? lc $_->[0] : '-';
$cell =~ s/ api$//;
printf '<th title="%s">%s', join(' + ', @$_), $cell;
}
print '<td>', map {
sprintf('<a href="%s" onclick="%s">%s</a>',
"#$id",
sprintf("try { %s; return false } catch(err) { return true }",
"document.getElementById('$id').classList.toggle('target')",
),
Entity($_),
);
} $row->{title};
print '<div class=aside>';
print "<p>$_</p>"
for formatnotes($row->{description}, $row->{notes} || ());
if (my %notes = %{ $row->{notes_by_num} }) {
say '<p>Browser-specific notes:';
say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
say '</p>';
}
printf 'Resources: %s.', join(', ', map {
showlink($_->{title}, $_->{url})
} @$_) for grep { @$_ } $row->{links} // ();
printf '<br>Parent feature: %s.', join(', ', map {
showlink($caniuse->{data}->{$_}->{title}, "#$_")
} $_) for $row->{parent} || ();
print '</div>';
}
sub saystatuscol {
my ($id) = @_;
my $row = $caniuse->{data}->{$id};
for ($row->{status}) {
my $cell = $_ // '-';
$cell = showlink($cell, $_) for $row->{spec} // ();
printf '<td title="%s" class="l %s">%s',
$caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
}
}
sub saybrowsercols {
my ($id, $browser) = @_;
my $feature = $caniuse->{data}->{$id};
my $data = $feature->{stats}->{$browser};
if (ref $data eq 'ARRAY') {
$data = {
map { $_ => 'n' }
keys %{ $caniuse->{agents}->{$browser}->{version_list} }
};
}
my ($prev, @span);
for my $ver (@{ $versions{$browser} }, undef) {
my $compare = (
!defined $ver ? undef :
ref $data ne 'HASH' ? '' :
(first { defined } @{$data}{ reverse @{$ver} })
// $prev
|| 'u'
);
if (defined $prev and not (defined $compare and $prev eq $compare)) {
my @vercover = (map { @{$_} } @span);
for ($ver ? @{$ver} : ()) {
last if defined $data->{$_};
push @vercover, $_;
}
my $usage = sum(@{ $canihas->{$browser} }{@vercover});
note
my @notes;
push @notes, $feature->{notes_by_num}->{$1}
while $prev =~ s/\h \# (\d+) \b//x;
my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(' ',
(map { ref $_ eq 'CODE' ? $_->($browser, $vercover[0]) : $_ }
map { $DSTATS{$_} // () }
map { split / /, $_ }
$prev
),
'in', $caniuse->{agents}->{$browser}->{abbr},
showversions(@vercover, undef),
));
$title .= "\n$_" for notestotitle(@notes);
$prev .= ' #' if @notes and $prev =~ /^y/;
printf('<td class="%s" colspan="%d" title="%s">%s',
join(' ',
X => $CSTATS{$prev},
!$usage ? ('p0') : ('p',
sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
sprintf('p%02d', $usage * ($usagepct - .0001)),
),
),
scalar @span,
$title,
showversions($span[0]->[0], @span > 1 && defined $ver ? $span[-1]->[-1] : ()),
);
undef $prev;
@span = ();
}
if ($ver) {
my $startversion = first { defined $data->{ $ver->[$_] } }
reverse 0 .. $#{$ver};
push @span, [ @{$ver}[ $startversion .. $#{$ver} ] ];
}
$prev = $compare;
}
}
sub sayusagecol {
my ($id) = @_;
print '<td>', int $caniuse->{data}->{$id}->{usage};
}
say '<tbody>';
for my $id (sort {
$caniuse->{data}->{$b}->{usage} <=> $caniuse->{data}->{$a}->{usage}
} keys %{ $caniuse->{data} }) {
$caniuse->{data}->{$id}->{stats} or next;
printf '<tr id="%s">', $id;
saytitlecol($id);
saystatuscol($id);
saybrowsercols($id, $_) for @browsers;
sayusagecol($id);
say '</tr>';
}
say '</tbody>';
say '</table>';
sub paddedver {
$_[0] =~ m/(?:.*-|^)(\d*)(.*)/;
return sprintf('%03d', length $1 ? $1 : 999) . $2;
}
sub showversions {
my @span = (map { split /-/ } grep { defined } @_);
return $span[0] =~ s/\.0\z//r if @_ <= 1;
splice @span, 1, -1;
return join('‒', @span);
}
:>
<hr>
<div class="legend">
<table class="glyphs"><tr>
<td class="X l5">supported
<td class="X l4">annotated
<td class="X l3">partial
<td class="X l2">optional
<td class="X l1">missing
<td class="X l0">unknown
<td class="X ex">prefixed
</table>
<p><: if ($usage) { :>
Usage percentage:
<span class=" p0">0</span> -
<span class="p p0 p00">.01</span> -
<span class="p p0 p05">1-9</span> -
<span class="p p1">10</span> -
<span class="p p2">20</span> -
<span class="p p5">majority</span>
<: } else { :>
<table class="glyphs"><tr>
<td class="p p1">previous version</td>
<td class="p p3">current</td>
<td class="p p0 p00">upcoming (within months)</td>
<td class=" p0">future (within a year)</td>
</table>
<: } :> </p>
<div class="right">
<ul class="legend legend-set">
<li>default <strong>style</strong> is
<:= defined $get{style} && 'set to ' :><em><:= $style :></em>
<li><strong>usage</strong> source is
<:= !defined $get{usage} && 'default ' :><:= defined $usage ? "<em>$usage</em>" : 'not included (<em>0</em>)' :>
<li>usage <strong>threshold</strong> is
<:= defined $get{threshold} && 'changed to ' :><em><:= $minusage :>%</em>
</ul>
</div>
</div>
<script type="text/javascript" src="/searchlocal.js"></script>
<script type="text/javascript">
prependsearch(document.getElementById('intro'));
</script>