charset: improve arabic inheritance
[sheet.git] / charset.plp
1 <(common.inc.plp)><:
2
3 Html({
4         title => 'charset cheat sheet',
5         version => '1.0',
6         description => [
7                 "Reference sheet with all glyphs in common character encoding tables,",
8                 "and an overview of Unicode ranges and UTF-8 bytes.",
9         ],
10         keywords => [qw'
11                 charset codepage unicode ascii utf8 latin glyph character encoding
12                 reference common overview table
13         '],
14         stylesheet => [qw'light'],
15         data => [qw'charset-unicode.inc.pl charset-utf8.inc.pl'],
16 });
17
18 my @tablist = split m{/+}, $Request || 'default';
19
20 :>
21 <h1>Character encoding</h1>
22
23 <:
24 use POSIX qw( ceil );
25 use Shiar_Sheet::FormatChar;
26 my $glyphs = Shiar_Sheet::FormatChar->new;
27 my @request;
28
29 sub tabinput {
30         # generate character table(s)
31         my $input = shift or return;
32
33         state $ALIAS = {
34                 default    => [qw( unicode+0-639 utf-8 iso-8859-1 iso-8859-15 cp1252 cp437 cp850 )],
35                 us         => [qw( cp437 cp863 AdobeStandardEncoding gsm0338 )],
36                 ebcdic     => [qw( cp37 cp500 cp875 cp1026 cp1047 posix-bc )],
37                 iso        => [map {"iso-8859-$_"} 1 .. 11, 13 .. 16],
38                 dos        => [qw( cp437 cp865 cp861 cp860 cp863 cp850 cp857 cp852 cp775
39                                    cp737 cp869 cp866 cp855 cp862 cp864 )],
40                 aix        => [qw( cp1006 )],
41                 win        => [qw( cp1252 cp1250 cp1254 cp1257 cp1258 cp1253 cp1251 cp1255 cp1256 cp874 )],
42                 mac        => [qw( MacRoman MacRomanian MacRumanian MacCroatian MacCentralEurRoman MacTurkish MacIcelandic MacSami
43                                    MacGreek MacCyrillic MacHebrew MacArabic MacFarsi MacThai )],
44
45                 # languages
46                 westeur    => [qw( iso-8859-1 iso-8859-15 cp1252 iso-8859-14 cp850 hp-roman8 nextstep MacRoman )],
47                 centeur    => [qw( iso-8859-2 iso-8859-16 cp1250 cp852 MacRomanian MacCroatian MacCentralEurRoman )], # MacRumanian only for DB
48                 turkish    => [qw( iso-8859-9 iso-8859-3 cp1254 cp857 MacTurkish )],
49                 baltic     => [qw( iso-8859-4 iso-8859-13 cp1257 cp775 )],
50                 nordic     => [qw( iso-8859-10 cp865 cp861 MacIcelandic MacSami )],
51                 cyrillic   => [qw( koi8-r koi8-u koi8-f iso-8859-5 cp1251 MacCyrillic cp866 cp855 )], # MacUkrainian is broken
52                 arabic     => [qw( iso-8859-6 cp1256 MacArabic cp864 cp1006 MacFarsi )],
53                 greek      => [qw( iso-8859-7 cp1253 MacGreek cp737 cp869 )],
54                 hebrew     => [qw( iso-8859-8 cp1255 MacHebrew cp862 )],
55                 thai       => [qw( iso-8859-11 cp874 MacThai )],
56                 vietnamese => [qw( viscii cp1258 MacVietnamese )],
57                 symbol     => [qw( symbol dingbats MacDingbats AdobeZdingbat AdobeSymbol )],
58
59                 # iso-code shorthand
60                 1 => 'westeur',
61                 2 => 'centeur',
62                 3 => 'turkish',
63                 4 => 'baltic',
64                 5 => 'cyrillic',
65                 6 => 'arabic',
66                 7 => 'greek',
67                 8 => 'hebrew',
68                 9 => 'turkish',
69                 10 => 'nordic',
70                 11 => 'thai',
71         };
72         if (my $follow = $ALIAS->{$input}) {
73                 tabinput($_) for ref $follow ? @{$follow} : $follow;
74                 return;
75         }
76
77         state $visible = {};  # all present tables
78         my %row = (offset => 0, cols => 16);
79         my $endpoint = 255;
80         my $params = $input =~ s/[+](.*)\z// ? $1 : undef;
81
82         if (not defined $params) {
83                 use List::Util qw( first pairfirst pairs );
84
85                 state $INHERIT = {
86                         'cp437'       => ['cp850' => 0], # ascii range overridden later
87                         'gsm0338'     => ['ascii' => '0-127'],
88
89                         'iso-8859-2'  => ['iso-8859-1' => '160'],
90                         'iso-8859-3'  => ['iso-8859-1' => '160'], #TODO: also apply to iso-8859-9
91                         'iso-8859-4'  => ['iso-8859-2' => '160'],
92                         'iso-8859-5'  => ['iso-8859-1' => '160'],
93                         'iso-8859-6'  => ['cp1256' => '128', 'iso-8859-1' => '160'],
94                         'iso-8859-7'  => ['iso-8859-1' => '160'],
95                         'iso-8859-8'  => ['iso-8859-1' => '160'],
96                         'iso-8859-9'  => ['iso-8859-1' => '208-223+240'],
97                         'iso-8859-10' => ['iso-8859-4' => '160'],
98                         'iso-8859-11' => ['iso-8859-1' => '160'],
99                         'iso-8859-13' => ['iso-8859-4' => '160'],
100                         'iso-8859-14' => ['iso-8859-1' => '160'],
101                         'iso-8859-15' => ['iso-8859-1' => '160-191'],
102                         'iso-8859-16' => ['iso-8859-2' => '160'],
103                         'hp-roman8'   => ['iso-8859-1' => '160'],
104
105                         'cp1252'      => ['iso-8859-1' => '128-159'],
106                         'cp1250'      => ['iso-8859-2' => '128-191', 'cp1252' => '128'],
107                         'cp1254'      => ['iso-8859-9' => '128-159', 'cp1252' => '128-159+208'],
108                         'cp874'       => ['iso-8859-11' => '128-159', 'cp1252' => '128'], # windows-874 actually cp1162
109                         'cp1257'      => ['iso-8859-13' => '128-159+255', 'cp1252' => '128'],
110                         'cp1251'      => ['cp1252' => '128'],
111                         'cp1253'      => ['cp1252' => '128'],
112                         'cp1255'      => ['iso-8859-8' => '128-223', 'cp1252' => '128'],
113                         'cp1256'      => ['cp1252' => '128'],
114                         'cp1258'      => ['cp1252' => '128-159+192'],
115
116                         'cp850'       => ['cp437' => '144'],
117                         'cp860'       => ['cp437' => '128-175'],
118                         'cp861'       => ['cp865' => '128-175'],
119                         'cp863'       => ['cp437' => '128-175'],
120                         'cp865'       => ['cp437' => '144-175'],
121                         'cp852'       => ['cp850' => '128', 'cp437' => '128'],
122                         'cp857'       => ['cp850' => '128-175+208-239', 'cp437' => '128'],
123                         'cp775'       => ['cp850' => '128'],  # partial cp437
124                         'cp866'       => ['cp437' => '128-175+224'],
125                         'cp855'       => ['cp437' => '128'],
126                         'cp1006'      => ['iso-8859-6' => '160', 'cp437' => '128'],
127                         'cp737'       => ['cp437' => '128-175+224'],
128                         'cp869'       => ['cp437' => '128'],
129                         'cp862'       => ['cp437' => '128-159'],
130                         'cp864'       => ['MacArabic' => '128', 'iso-8859-6' => '128', 'cp437' => '128'], #TODO: compare form variants
131
132                         'koi8-u'      => ['koi8-r' => 128],
133                         'koi8-f'      => ['koi8-u' => 128],
134
135                         'MacRomanian' => ['MacRoman' => '160-191+208-223'],
136                         'MacRumanian' => ['MacRomanian' => '160-191+208-223', 'MacRoman' => '160-191+208-223'],
137                         'MacCroatian' => ['MacRoman' => '160'],
138                         'MacCentralEurRoman' => ['MacRoman' => '128'],
139                         'MacIcelandic'=> ['MacRoman' => '160-175+208-239'], #TODO: gaps at C/E
140                         'MacTurkish'  => ['MacRoman' => '208-223'], # F5 is unassigned
141                         'MacSami'     => ['MacIcelandic' => '144', 'MacRoman' => '144'],
142                         'MacGreek'    => ['MacRoman' => '128'],
143                         'MacCyrillic' => ['MacRoman' => '128'],
144                         'MacHebrew'   => ['iso-8859-8' => '128', 'MacRoman' => '128-143+160'], # partial ascii
145                         'MacArabic'   => ['iso-8859-6' => '128', 'cp864' => '128', 'MacRoman' => '128'], #TODO: multiple parents
146                         'MacFarsi'    => ['MacArabic' => '176-191', 'MacRoman' => '128'],
147                 };
148
149                 my @parents = @{ $INHERIT->{$input} || [] };
150
151                 if (my ($parent, $part) = pairfirst { defined $visible->{$a} } @parents) {
152                         $row{parent} = $parent;
153                         $params = $part;
154                         $params = 128 unless $visible->{$parent}
155                                 or ($input eq 'MacCroatian' and defined $visible->{MacRomanian});
156                 }
157                 elsif (defined $visible->{ascii}) {
158                         $row{parent} = $parents[0];
159                         $params = $parents[1] || 128;
160                         $params = 128 if $params >= 128;  # ascii offset at most
161                 }
162                 elsif (@parents) {
163                         $row{parent} = $parents[0];
164                 }
165                 $visible->{$_} //= 0 for $row{parent};
166         }
167
168         if (defined $params) {
169                 $params =~ m{
170                         \A (?<offset> \d+)
171                         (?: (?: [-] (?<stop> \d+) )? (?: [+] (?<restart> \d+) ) )?
172                         (?: [-] (?<endpoint> \d+) )? \z
173                 }x or Alert([
174                         "Unknown range parameters for $input",
175                         "<q>$params</q> is not in format start(-stop)(+restart(-end))",
176                 ]);
177
178                 $row{offset} = $+{offset};
179                 $endpoint = $+{endpoint} if $+{endpoint};
180                 if (my $restart = $+{restart}) {
181                         my $skip = int(($+{stop} || $row{offset}) / $row{cols});
182                         for ($skip + 1 .. ($restart / $row{cols}) - 1) {
183                                 $row{skip}->{ $_ * $row{cols} - $row{offset} }++;
184                         }
185                 }
186         }
187
188         if ($input =~ /^U([0-9a-f]+)(?:-([0-9a-f]+))?/) {
189                 my $start = hex($1) << ($2 ? 4 : 8);
190                 my $end = $2 ? hex($2) << 4 : $start + 240;
191                 $row{table} = join '', map { chr } $start .. $end+15;
192                 utf8::upgrade($row{table});  # prevent latin1 output
193                 $row{endpoint} = $end + 14 - $start;
194                 $row{set} = sprintf 'Unicode block U+%02Xxx', $start >> 8;
195         }
196         elsif ($input eq 'U') {
197                 $row{set} = 'Unicode planes';
198                 $row{cell} = do 'charset-ucplanes.inc.pl'
199                         or Alert('Table data could not be read', $@ || $!);
200                 $row{cols} *= 2;
201                 $row{endpoint} = 1023 * $row{cell}->{colsize};
202         }
203         elsif ($row{set} = Encode::resolve_alias($input)) {
204                 if ($row{set} eq 'Internal') {
205                         $row{set} = 'Unicode BMP';
206                         $row{cell} = do 'charset-unicode.inc.pl'
207                                 or Alert('Table data could not be read', $@ || $!);
208                         $row{endpoint} = ($endpoint || 8191) * $row{cell}->{colsize};
209                 }
210                 elsif ($row{set} eq 'utf-8-strict') {
211                         $row{set} = 'UTF-8';
212                         $row{cell} = do 'charset-utf8.inc.pl'
213                                 or Alert('Table data could not be read', $@ || $!);
214                         $row{endpoint} = 255;
215                 }
216                 else {
217                         if ($row{set} eq 'MacHebrew' or $row{set} eq 'MacThai') {
218                                 # array of possibly multiple characters per code point
219                                 $row{table} = [
220                                         map { Encode::decode($row{set}, pack 'C*', $_) } $row{offset} .. $endpoint
221                                 ];
222                         }
223                         else {
224                                 # ~16x faster than decoding in loop;
225                                 # substr strings is twice as fast as splitting to an array
226                                 $row{table} = Encode::decode($row{set}, pack 'C*', $row{offset} .. $endpoint);
227                         }
228                         $row{endpoint} = $endpoint - $row{offset};
229
230                         if ($row{set} eq 'cp437' and !$row{offset}) {
231                                 substr($row{table}, 237, 1) = pack 'U*', 0x3D5; # phi sign
232                                 substr($row{table}, 0, 32) = pack 'U*', map {hex} qw(
233                                         2007 263A 263B 2665 2666 2663 2660 2022
234                                         25D8 25CB 25D9 2642 2640 266A 266B 263C
235                                         25BA 25C4 2195 203C 00B6 00A7 25AC 21A8
236                                         2191 2193 2192 2190 221F 2194 25B2 25BC
237                                 );
238                         }
239
240                         $visible->{ascii} =  # assume common base
241                         $visible->{ $row{set} } = 1;
242                 }
243         }
244         else {
245                 Alert("Encoding <q>$input</q> unknown");
246                 return;
247         }
248         push @request, \%row;
249 }
250 tabinput($_) for @tablist;
251
252 my $NOCHAR = chr 0xFFFD;
253
254 sub range_cell {
255         my ($info, $offset) = @_;
256         my $table = $info->{cell} or return;
257         my $def = $table->{$offset} or return;
258         my ($len, $class, $name, $title) = @{$def};
259
260         my $cols = $info->{cols};
261         my $colsize = $table->{colsize} || 1;
262         my $attr = '';
263         $len /= $colsize;
264         $name //= $len <= 2 ? 'res' : 'reserved';
265
266         if (my $part = $offset/$colsize % $cols) {
267                 # continued row
268                 my $rest = $cols - $part;  # remaining
269                 $rest = $len if $len < $rest; #TODO: optimise
270                 if ($len -= $rest) {
271                         # continued on new row
272                         my @next = ($len * $colsize, "$class joinu");
273                         if ($len > $rest) {
274                                 # minority remains
275                                 push @next, $name, $title;
276                                 $title ||= $name;
277                                 $name = '';
278                         }
279                         else {
280                                 # minority on next row
281                                 push @next, '"', $title || $name;
282                         }
283                         $table->{$offset + $colsize*$rest} //= \@next;
284                         $class .= ' joind';
285                 }
286                 $len = $rest;
287         }
288         elsif (my $rows = int($len / $cols)) {
289                 # multiple full rows
290                 if ($len -= $rows * $cols) {
291                         # partial row remains
292                         $table->{$offset + $colsize*$rows * $cols} //= [$len*$colsize, "$class joinu", '', $title];
293                         $class .= ' joind';
294                 }
295                 $attr .= sprintf ' rowspan=%d', $rows;
296                 $len = $cols;
297         }
298
299         $attr .= sprintf ' colspan=%d', $len unless $len == 1;
300         $attr .= $1 if $class and $class =~ s/( \w+="[^"]*")//;
301         $attr .= sprintf ' class="%s"', $class if $class;
302         $attr .= sprintf ' title="%s"', EscapeHTML($title) if $title;
303         return "<td$attr>$name";
304 }
305
306 for my $row (@request) {
307         my $cols = $row->{cols};
308         my $colsize = $row->{cell} && $row->{cell}->{colsize} || 1;
309         my $coldigits = ceil(log($colsize * $cols) / log(16));  # uniform length of hexadecimal header
310         my $rowdiv = 16 ** $coldigits;  # row divide for column digits
311         $rowdiv = 1 if $rowdiv != $cols * $colsize;  # divide only if all columns are matched
312         my $offset = 0;
313
314         printf '<div class="section"><table class="glyphs%s">', !$row->{cell} && ' charmap';
315         my $title = $row->{set};
316         $title .= " <aside>(over $_)</aside>"
317                 for grep { $_ ne 'iso-8859-1' } $row->{parent} // ();
318         printf '<caption>%s</caption>', $title;
319         print '<col>' x ($cols + 1);
320         for my $section (qw{thead}) {
321                 print "<$section><tr><th>", $rowdiv == 1 ? '+' : '↱';
322                 printf '<th>%0*X', $coldigits, $_ * $colsize for 0 .. $cols - 1;
323                 print "\n";
324         }
325         print '<tbody>';
326         while ($offset < $row->{endpoint}) {
327                 if ($row->{skip}->{$offset}) {
328                         $offset += $cols * $colsize;
329                         next;
330                 }
331
332                 print '<tr><th>';
333                 {
334                         if (my $rowmod = $offset % $rowdiv) {
335                                 # offset in column units
336                                 printf '<small>+%X</small>', $rowmod;
337                         }
338                         else {
339                                 # divided row offset
340                                 printf '%X', ($offset + $row->{offset}) / $rowdiv;
341                         }
342                 }
343                 for (1 .. $cols) {
344                         if ($row->{cell}) {
345                                 print range_cell($row, $offset);
346                                 next;
347                         }
348
349                         my $glyph = ref $row->{table} eq 'ARRAY' ? $row->{table}->[$offset] :
350                                 substr $row->{table}, $offset, 1;
351                         if ($glyph eq $NOCHAR) {
352                                 print '<td>';
353                                 next;
354                         }
355
356                         if (exists $get{compare}) {
357                                 state $visible = {};
358                                 my $cp = $offset + $row->{offset};
359                                 printf '<td class="%s" title="%3$s">%2$s',
360                                         $cp == ord $glyph ? 'l4' :
361                                         $row->{parent} && $glyph eq
362                                                 Encode::decode($row->{parent}, pack 'C', $cp) ? 'l3' :
363                                         $visible->{$glyph} ? 'l2' :
364                                         'l1',
365                                         $glyphs->glyph_html($glyph);
366                                 $visible->{$glyph}++;
367                                 next;
368                         }
369
370                         print "\n".$glyphs->glyph_cell($glyph);
371                 }
372                 continue {
373                         $offset += $colsize;
374                 }
375                 print "\n";
376         }
377         say '</table></div>';
378 }
379
380 :>
381 <hr>
382
383 <div class="legend">
384         <table class="glyphs"><tr><: if (exists $get{compare}) { :>
385         <td class="X l4">unicode
386         <td class="X l3">inherited
387         <td class="X l2">existing
388         <td class="X l1">original
389         <td class="">unassigned
390 <: } else { :>
391         <td class="X Cc">control
392         <td class="X Zs"><span>whitespace</span>
393         <td class="X Mn">diacritic<table class="glyphs"><tr>
394                 <td class="X Sk">letter
395                 </table>
396         <td class="X Po">punctuation<table class="glyphs"><tr>
397                 <td class="X Pf">quote
398                 </table>
399         <td class="X So">symbol<table class="glyphs"><tr>
400                 <td class="X Sm">math
401                 <td class="X Sc">currency
402                 </table>
403         <td class="X No">numeric
404         <td class="X Greek">greek<table class="glyphs"><tr>
405                 <td class="X Latin">latin
406                 <td class="X Cyrillic">cyrillic
407                 </table>
408         <td class="X Aramaic">aramaic<table class="glyphs"><tr>
409                 <td class="X Brahmic">brahmic
410                 <td class="X Arabic">arabic
411                 </table>
412         <td class="X Syllabic">syllabic<table class="glyphs"><tr>
413                 <td class="X African">african
414                 <td class="X Hiragana">japanese
415                 <td class="X Han">cjk
416                 <td class="X Bopomofo">chinese
417                 </table>
418         <td class="X Alpha">alphabetic
419         </table>
420
421         <table class="glyphs"><tr>
422         <td class="X">unicode 7.0
423         <td class="X Xr">proposed
424         <td class="X Xd">deprecated
425         <td class="">unassigned
426         <td class="X Xi">invalid
427 <: } :> </table>
428 </div>
429