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 %{$_} }
205 map { $canihas->{$_} }
210 $_->{usage} = featurescore($_->{stats}) * $usagepct
211 for values %{ $caniuse->{data} };
213 print '<table class="mapped">';
214 print '<col span="3">'; # should match first thead row
215 printf '<colgroup span="%d">', scalar @{ $versions{$_} } for @browsers;
216 say '</colgroup><col>';
218 my $header = join('',
220 '<th colspan="3" rowspan="2">feature',
222 my $name = $caniuse->{agents}->{$_}->{browser};
223 sprintf('<th colspan="%d" class="%s" title="%s">%s',
224 scalar @{ $versions{$_} },
225 join(' ', map {"b-a-$_"} grep {$_}
226 $_, @{ $caniuse->{agents}->{$_} }{'prefix', 'type'},
229 sprintf('%.1f%%', $canihas->{$_}->{-total} * $usagepct),
233 length $name <= (3 * @{ $versions{$_} }) ? $name
234 : $caniuse->{agents}->{$_}->{abbr};
240 print '<thead>', $header;
241 # preceding row without any colspan to work around gecko bug
243 for my $browser (@browsers) {
244 for my $span (@{ $versions{$browser} }) {
245 my $lastver = first {
246 !defined $caniuse->{agents}->{$browser}->{verrelease}->{$_} # stable
248 printf('<td title="%s"%s>%s',
250 sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{ @{$span} }) * $usagepct),
251 'version ' . showversions(@{$span}, undef),
252 $span->[-1] eq $lastver ? () : '(development)',
254 !defined $lastver && ' class="ex"',
255 showversions($lastver // $span->[0]),
260 say '<tfoot>', $header;
262 # prefix indicates browser family; count adjacent families
263 my (@families, %familycount);
264 for my $browser (@browsers) {
265 my $family = $caniuse->{agents}->{$browser}->{prefix};
266 push @families, $family unless $familycount{$family};
267 $familycount{$family} += @{ $versions{$browser} };
270 print "\n", '<tr class="cat">';
271 printf '<th colspan="%d">%s', $familycount{$_}, $_ for @families;
276 # relative amount of support for given feature
278 if (my $row = shift) {
280 while (my ($browser, $versions) = each %$row) {
281 ref $versions eq 'HASH' or next;
282 while (my ($version, $status) = each %$versions) {
283 $status =~ s/\h\#\d+//g;
284 $rank += ($canihas->{$browser}->{$version} || .001) * $PSTATS{$status};
290 while (my ($browser, $vercols) = each %versions) {
291 my $div = 0; # multiplier exponent (decreased to lower value)
292 my @vers = map { $row->{$browser}->{$_} } @$vercols;
293 if (my $current = $caniuse->{agents}->{$browser}->{versions}->[-3]) {
294 my @future; # find upcoming releases (after current)
295 for (reverse @$vercols) {
296 last if $_ eq $current;
297 push @future, pop @vers;
298 $_ eq 'u' and $_ = $vers[-1] for $future[-1]; # inherit latest value if unknown
300 splice @vers, -1, 0, @future; # move ahead to decrease precedence
302 $rank += $PSTATS{$_} * 2**($div--) for reverse @vers;
311 s/\r\n?/\n/g; # windows returns
312 s/\h* $//gmx; # trailing whitespace
313 s/(?<= [^.\n]) $/./gmx; # consistently end each line by a period
315 s{ ` ([^`]*) ` }{<code>$1</code>}gx;
316 s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
325 s{ \[ ([^]]*) \] \( [^)]* \) }{$1}gx; # strip link urls
332 my $row = $caniuse->{data}->{$id};
334 for ($row->{categories}) {
335 my $cell = $_ ? lc $_->[0] : '-';
336 $cell =~ s/ api$//; # trim unessential fluff in 'js api'
337 printf '<th title="%s">%s', join(' + ', @$_), $cell;
341 sprintf('<a href="%s" onclick="%s">%s</a>',
343 sprintf("try { %s; return false } catch(err) { return true }",
344 "document.getElementById('$id').classList.toggle('target')",
349 print '<div class=aside>';
351 for formatnotes($row->{description}, $row->{notes} || ());
352 if (my %notes = %{ $row->{notes_by_num} }) {
353 say '<p>Browser-specific notes:';
354 say "<br>#$_: ", formatnotes($notes{$_}) for sort keys %notes;
357 printf 'Resources: %s.', join(', ', map {
358 showlink($_->{title}, $_->{url})
359 } @$_) for grep { @$_ } $row->{links} // ();
360 printf '<br>Parent feature: %s.', join(', ', map {
361 showlink($caniuse->{data}->{$_}->{title}, "#$_")
362 } $_) for $row->{parent} || ();
368 my $row = $caniuse->{data}->{$id};
370 for ($row->{status}) {
371 my $cell = $_ // '-';
372 $cell = showlink($cell, $_) for $row->{spec} // ();
373 printf '<td title="%s" class="l %s">%s',
374 $caniuse->{statuses}->{$_}, $CSTATUS{$_} // '', $cell;
379 my ($id, $browser) = @_;
380 my $feature = $caniuse->{data}->{$id};
381 my $data = $feature->{stats}->{$browser};
382 if (ref $data eq 'ARRAY') {
383 # special case for unsupported
384 my $release = $caniuse->{agents}->{$browser}->{verrelease};
386 map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
391 for my $ver (@{ $versions{$browser} }, undef) {
393 !defined $ver ? undef : # last column if nameless
394 ref $data ne 'HASH' ? '' : # unclassified if no support hash
395 $data->{ $ver->[-1] } // $prev # known or inherit from predecessor
396 // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
397 ~~ 'n' && 'n' # first known version is unsupported
400 unless (!defined $prev or $prev ~~ $compare) {
401 my @vercover = (map { @{$_} } @span);
402 for ($ver ? @{$ver} : ()) {
403 $data->{$_} eq $data->{$vercover[-1]} or last;
404 push @vercover, $_; # matches from next span start
406 my $usage = sum(@{ $canihas->{$browser} }{@vercover});
408 # strip #\d note references from support class
410 push @notes, $feature->{notes_by_num}->{$1}
411 while $prev =~ s/\h \# (\d+) \b//x;
413 # prepare version hover details
414 my $title = sprintf('%.1f%% %s', $usage * $usagepct, join(' ',
415 (map { ref $_ eq 'CODE' ? $_->($browser, $vercover[0]) : $_ }
416 map { $DSTATS{$_} // () }
417 map { split / /, $_ }
420 'in', $caniuse->{agents}->{$browser}->{abbr},
421 showversions(@vercover, undef),
423 $title .= "\n$_" for notestotitle(@notes);
425 $prev .= ' #' if @notes and $prev =~ /^y/;
426 printf('<td class="%s" colspan="%d" title="%s">%s',
429 !$usage ? ('p0') : ('p',
430 sprintf('p%01d', $usage * ($usagepct - .0001) / 10),
431 sprintf('p%02d', $usage * ($usagepct - .0001)),
436 showversions($span[0]->[0], @span > 1 && defined $ver ? $span[-1]->[-1] : ()),
441 push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$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('%02d', length $1 ? $1 : 99) . $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'));