index: release v1.18 with only altgr index linked master
authorMischa POSLAWSKY <perl@shiar.org>
Tue, 5 Mar 2024 23:00:08 +0000 (00:00 +0100)
committerMischa POSLAWSKY <perl@shiar.org>
Wed, 6 Mar 2024 00:54:29 +0000 (01:54 +0100)
149 files changed:
.gitignore
.htaccess
Makefile
Shiar_Sheet/DB.pm [new file with mode: 0644]
Shiar_Sheet/FormRow.pm [new file with mode: 0644]
Shiar_Sheet/FormatChar.pm
Shiar_Sheet/ImagePrep.pm [new file with mode: 0644]
Shiar_Sheet/Keyboard.pm
Shiar_Sheet/KeyboardChars.pm [new file with mode: 0644]
TODO
apl.inc.pl
apl.plp
base.css
base.plp
browser.plp
chars.plp
charset-encoding.inc.pl
charset.inc.pl [new file with mode: 0644]
charset.plp
circus.css
cli.plp [new file with mode: 0644]
codec-audio.inc.pl [new file with mode: 0644]
codec-image.inc.pl [new file with mode: 0644]
codec.plp [new file with mode: 0644]
common.inc.plp
countries.plp
dark.css
darklite.css
dieren.inc.pl [new file with mode: 0644]
dieren.jpg [new file with mode: 0644]
dieren.plp [new file with mode: 0644]
digits.plp
digraphs.plp
digraphs.vim.plp
emoji-gmail.inc.pl
emoji-msn.inc.pl
emoji-yahoo.inc.pl
emoji.plp
font.plp
graffiti.ttf [new file with mode: 0644]
index.plp
keyboard.plp
keyboard/altgr/apl.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/boyeg.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/drix.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/emojiworks.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/eurkey.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/index.inc.plp [new file with mode: 0644]
keyboard/altgr/ipa.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/macos-abc.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/macos.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/msx-graph.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/msx.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/olpc.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/spacecadet.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/symbolics.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/ukext.eng.inc.pl [new file with mode: 0644]
keyboard/altgr/windows.eng.inc.pl [new file with mode: 0644]
keyboard/less.eng.inc.pl [moved from less.eng.inc.pl with 96% similarity]
keyboard/mplayer.eng.inc.pl [moved from mplayer.eng.inc.pl with 79% similarity]
keyboard/mpv.eng.inc.pl [new file with mode: 0644]
keyboard/mutt.eng.inc.pl [moved from mutt.eng.inc.pl with 87% similarity]
keyboard/nethack.eng.inc.pl [moved from nethack.eng.inc.pl with 85% similarity]
keyboard/readline.eng.inc.pl [moved from readline.eng.inc.pl with 78% similarity]
keyboard/screen.eng.inc.pl [moved from screen.eng.inc.pl with 91% similarity]
keyboard/vi.eng.inc.pl [moved from vi.eng.inc.pl with 68% similarity]
keyboard/vimperator.eng.inc.pl [moved from vimperator.eng.inc.pl with 96% similarity]
latin.plp
latinsample.js
less.plp
lite.css
map-numbers.nld.tsv [new file with mode: 0644]
map.plp [new file with mode: 0644]
mono.css
mplayer.plp
mutt.plp
nethack.plp
osicon.ttf [new file with mode: 0644]
perl.inc.pl
perl.plp
readline.plp
red.css
robots.txt [deleted file]
robots.txt.plp [new file with mode: 0644]
sample.plp
sample.png [new file with mode: 0644]
sample.txt [new symlink]
sc-units-bw.inc.pl
sc-units-hots.inc.pl
sc-units-lotv.inc.pl [new file with mode: 0644]
sc.css
sc.plp
screen.plp
searchlocal.js
shell.inc.pl [new file with mode: 0644]
shell.plp [new file with mode: 0644]
source.plp
termcol.inc.pl
termcol.plp
tools/icomoon-selection.json [new file with mode: 0644]
tools/lastword [new file with mode: 0755]
tools/mkcaniuse
tools/mkcharinfo
tools/mkclioptions [new file with mode: 0755]
tools/mkcountries-geonames
tools/mkdigraphlist
tools/mkdigraphs-plan9 [new file with mode: 0755]
tools/mkdigraphs-rfc
tools/mkdigraphs-shiar
tools/mkdigraphs-xorg
tools/mkfontinfo
tools/mkimg-google [new file with mode: 0755]
tools/mkimgthumb [new file with mode: 0755]
tools/mkjson [new file with mode: 0755]
tools/mkkeyboard-xkb-symbols [new file with mode: 0755]
tools/mksitemap
tools/mkttfinfo
tools/mkusage-squid
tools/mkusage-statcounter
tools/mkusage-wikimedia
tools/mkwordlist [new file with mode: 0755]
tools/mkwordthumb [new file with mode: 0755]
tools/mkxkeysymdef [new file with mode: 0755]
tools/perlinc-static [deleted file]
tools/word.pg.sql [new file with mode: 0644]
tools/wordpairs [new file with mode: 0755]
unicode-table.inc.pl
unicode.plp
vi.plp
vimperator.plp
word.plp [new file with mode: 0644]
word/debug.css [new file with mode: 0644]
word/edit.plp [new file with mode: 0644]
word/editor.css [new file with mode: 0644]
word/editor.js [new file with mode: 0644]
word/finder.js [new file with mode: 0644]
word/memory.css [new file with mode: 0644]
word/memory.js [new file with mode: 0644]
word/multichoice.css [new file with mode: 0644]
word/multichoice.js [new file with mode: 0644]
word/quiz.js [new file with mode: 0644]
word/report.plp [new file with mode: 0644]
word/wijzer.css [new file with mode: 0644]
word/wijzer.js [new file with mode: 0644]
writing-brah.inc.pl
writing-latn.inc.pl
writing-phnx.inc.pl
writing-script.inc.pl
writing.plp

index 5c53b312bee52468fc3ec73056124071c8dfbcbd..88155205ca944855a90622a68770ee22cdeee44b 100644 (file)
@@ -1,9 +1,16 @@
 # downloaded data and generated includes
 /data
+/word/put.js
 
-# derived contents
+# derived or compiled contents
 /sitemap.xml
 /light.css
+/UPDATE
+/plan.plp
+/word/*.min.js
+
+# optional cache files
+/source/*.html
 
 # site owner tag for google webmaster tools
 /google????????????????.html
index 41f82b4e38e68cc370d93bcf2d11fa298e4b3d35..90f32b4dea33dea798eae4db36313137dd1e1ecd 100644 (file)
--- a/.htaccess
+++ b/.htaccess
@@ -1,28 +1,47 @@
-Options        -MultiViews
+Options        -MultiViews -Indexes
 DirectoryIndex index.plp
+DirectorySlash Off
+AddCharset     utf-8 .txt
 
 RewriteEngine  on
 RewriteBase    /
 
-# redirect from old vim-only subdomain
-RewriteCond %{HTTP_HOST}             ^vim?\.shiar\.\w+$
+# redirect from deprecated domain names
+RewriteCond %{HTTP_HOST}             ^vim?\.shiar\.\w+$       [OR]
+RewriteCond %{HTTP_HOST}             =sheet.shiar.net
 RewriteRule ^(vi(?=m$)|.*)           http://sheet.shiar.nl/$1 [R=301]
 
 # redirect old locations
-RewriteRule ^vim$                    /vi [R=301]
-RewriteRule ^cc$                     /countries [R=301]
+RewriteRule ^vim$                    /vi                      [R=301]
+RewriteRule ^cc$                     /countries               [R=301]
+
+# forward to https protocol if requested
+RewriteCond %{HTTPS}                          =off
+RewriteCond %{HTTP:Upgrade-Insecure-Requests} =1
+RewriteCond %{HTTP_HOST}                      =sheet.shiar.nl
+RewriteRule (.*)                     https://%{HTTP_HOST}/$1  [L]
 
 # serve vim commands when requesting /digraphs.ex as well
-RewriteRule    ^(digraphs)\.ex(/.*)?$ $1.vim$2
+RewriteRule ^(digraphs)\.ex(/.*)?$   $1.vim$2
 
 # add .plp if a file exists with .plp appended (topdir only)
-RewriteCond    %{REQUEST_FILENAME}.plp  -f
-RewriteRule    ^/*([^/]+)(.*)           $1.plp$2
+RewriteCond %{REQUEST_FILENAME}     !-f
+RewriteCond %{DOCUMENT_ROOT}/$1.plp  -f
+RewriteRule ^/*([^/]+)(.*)           $1.plp$2
+
+# replace jpeg images by webp alternatives if supported
+RewriteCond %{HTTP_ACCEPT}           \bimage/webp
+RewriteCond %{DOCUMENT_ROOT}/$1.webp -f
+RewriteRule (.*)\.jpg$               $1.webp
 
-# allow browsers to cache for upto a month
+# allow browsers to cache static assets for upto a month
 <IfModule headers_module>
-<FilesMatch "\.(?:css|js)$">
+<FilesMatch "\.(?:css|gif|png|jpg|webp|jxl|svg)$">
+Header set Cache-Control "max-age=2592000"
+</FilesMatch>
+<FilesMatch "\.(?:js|json)$">
 Header set Cache-Control "max-age=2592000"
+Header set Access-Control-Allow-Origin "*"
 </FilesMatch>
 </IfModule>
 
index a2b28467394619122e574f54e64616c5f90ef43b..df6cbe5bab35d15e5837a61ca3d67c2f711a6e59 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,7 @@
-all: sitemap.xml light.css data/digraphs.inc.pl data/unicode-cover.inc.pl data/countries.inc.pl data/browser data/termcol-xcolor.inc.pl data/digraphs-xorg.inc.pl
-more: all
+all: sitemap.xml light.css plan.plp UPDATE data cache
 
-download: data/DerivedAge.txt data/rfc1345.txt data/xorg-compose data/countryInfo.txt data/browser/caniuse data/browser/usage-wm.tsv data/xcolors data/unicode-sampler
-.PHONY: download
+.PHONY: force # applied to download after 2 hours
+download := $(shell [ -z $$(find data/download -mmin -120) ] && (touch data/download && echo force))
 
 # atomically create file by command
 cmdsave = @echo '$1' $2 \>$@; mispipe '$1 $2' 'ifne sponge $@'
@@ -17,13 +16,29 @@ sitemap.xml: tools/mksitemap
 light.css: tools/stripcss base.css
        $(call cmdsave,$^)
 
-data/DerivedAge.txt:
+plan.plp: TODO
+       kramdown $< >$@
+
+UPDATE: $(download)
+       $(call cmdsave,git log -1 --date=short --pretty="%ad    %s")
+
+cache: $(patsubst %.inc.pl,data/%.json,$(wildcard charset-*.inc.pl) $(wildcard keyboard/altgr/*.inc.pl) writing-latn.inc.pl)
+word: word/put.min.js data/wordlist.en.json data/wordlist.nl.json data/wordlist.ru.json data/wordpairs.json
+
+word/put.js: $(download)
+       tools/wget-ifmodified https://github.com/kriszyp/put-selector/raw/master/put.js $@
+word/%.min.js: word/%.js
+       uglifyjs -m '' $< -o $@
+
+data: data/digraphs.json data/unicode-cover.inc.pl data/countries.inc.pl data/browser data/termcol-xcolor.inc.pl data/digraphs-xorg.json data/unicode-sampler word
+
+data/DerivedAge.txt: $(download)
        tools/wget-ifmodified http://www.unicode.org/Public/UNIDATA/$(@F) $@
 
 data/unicode-age.inc.pl: tools/mkcharver data/DerivedAge.txt
        $(call cmdsave,$^)
 
-data/rfc1345.txt:
+data/rfc1345.txt: $(download)
        tools/wget-ifmodified http://www.ietf.org/rfc/$(@F) $@
 
 data/digraphs-rfc.inc.pl: tools/mkdigraphs-rfc data/rfc1345.txt
@@ -32,34 +47,56 @@ data/digraphs-rfc.inc.pl: tools/mkdigraphs-rfc data/rfc1345.txt
 data/digraphs-shiar.inc.pl: tools/mkdigraphs-shiar shiar.inc.txt
        $(call cmdsave,$^)
 
-data/xorg-compose:
-       tools/wget-ifmodified http://cgit.freedesktop.org/xorg/lib/libX11/plain/nls/en_US.UTF-8/Compose.pre $@
-
-data/digraphs-xorg.inc.pl: tools/mkdigraphs-xorg data/xorg-compose
+data/xorg-compose: $(download)
+       tools/wget-ifmodified http://gitlab.freedesktop.org/xorg/lib/libx11/-/raw/master/nls/en_US.UTF-8/Compose.pre $@
+data/keysymdef.h: $(download)
+       tools/wget-ifmodified http://gitlab.freedesktop.org/xorg/proto/xorgproto/-/raw/master/include/X11/$(@F) $@
+data/keysymdef.json: tools/mkxkeysymdef data/keysymdef.h
+       $(call cmdsave,$^)
+data/digraphs-xorg.json: tools/mkdigraphs-xorg data/xorg-compose data/keysymdef.json
        $(call cmdsave,$^)
 
 data/digraphs-vim.inc.pl: tools/mkdigraphs-vim
        $(call cmdsave,$<)
 
-data/digraphs.inc.pl: tools/mkdigraphlist data/digraphs-rfc.inc.pl data/digraphs-vim.inc.pl data/digraphs-shiar.inc.pl data/unicode-char.inc.pl
+data/digraphs-plan9.txt:
+       tools/wget-ifmodified https://9fans.github.io/usr/local/plan9/lib/keyboard $@
+data/digraphs-plan9.inc.pl: tools/mkdigraphs-plan9 data/digraphs-plan9.txt
+       $(call cmdsave,$^)
+
+data/digraphs.json: tools/mkdigraphlist data/digraphs-rfc.inc.pl data/digraphs-vim.inc.pl data/digraphs-shiar.inc.pl data/unicode-char.inc.pl
        $(call cmdsave,$<)
 
 data/unicode-char.inc.pl: tools/mkcharinfo data/digraphs-rfc.inc.pl data/digraphs-shiar.inc.pl data/unicode-age.inc.pl unicode-table.inc.pl
        $(call cmdsave,$<)
 
-data/font/%.inc.pl: tools/mkttfinfo data/font/%.ttf
+data/font/%.inc.pl: tools/mkttfinfo data/font/%.ttf #TODO ttc
        $(call cmdsave,$^) || true
 
-data/unicode-sampler:
+data/unicode-sampler: $(download)
        $(call gitsave,git://git.shiar.nl/unicode-sampler)
 
 data/xcolors/themes: data/xcolors
-data/xcolors:
+data/xcolors: $(download)
        $(call gitsave,https://github.com/tlatsas/xcolors)
 
 data/termcol-xcolor.inc.pl: tools/mktermcol-xcolor data/xcolors/themes
        $(call cmdsave,$^/*)
 
+data/wordlist.version.txt: force
+       @[ -e $@ ] || date -Is >$@
+       tools/lastword $@ || true
+
+data/wordlist.%.inc.pl: tools/mkwordlist data/wordlist.version.txt
+       $(call cmdsave,$< $*)
+data/word%.json: data/word%.inc.pl
+       $(call cmdsave,tools/mkjson $<)
+data/%.json: %.inc.pl
+       $(call cmdsave,tools/mkjson $<)
+
+data/wordpairs.inc.pl: data/wordlist.version.txt
+       tools/wordpairs >$@
+
 .SECONDARY: data/font/%.ttf
 data/font/%.ttf:
        find /usr/share/fonts/truetype/ ~/.fonts/ -iname "$(@F)" | head -1 | xargs -i ln -sf {} $@
@@ -74,7 +111,7 @@ data/font/droidserif.ttf:
 data/font/free%.ttf:
        find /usr/share/fonts/truetype/freefont/ -iname "$(@F)" | head -1 | xargs -i ln -sf {} $@
 data/font/roboto.ttf:
-       ln -sf /usr/share/fonts/truetype/roboto/Roboto-Regular.ttf $@
+       ln -sf /usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Regular.ttf $@
 data/font/noto%.ttf:
        find /usr/share/fonts/truetype/noto/ -iname "Noto$(*F)-Regular.ttf" | head -1 | xargs -i ln -sf {} $@
 
@@ -93,10 +130,10 @@ data/font/all-other: data/font/unifont.inc.pl data/font/code2000.inc.pl data/fon
 #      $< unifont.ttf $@
 #      $< --headless unifont_upper.ttf >>$@
 
-data/unicode-cover.inc.pl: tools/mkfontinfo data/font $(patsubst data/font/%.ttf,data/font/%.inc.pl,$(wildcard data/font/*.ttf))
+data/unicode-cover.inc.pl: tools/mkfontinfo data/font # $(wildcard data/font/*.inc.pl)
        $(call cmdsave,$<)
 
-data/countryInfo.txt:
+data/countryInfo.txt: $(download)
        tools/wget-ifmodified http://download.geonames.org/export/dump/$(@F) $@
 
 data/countries.inc.pl: tools/mkcountries-geonames data/countryInfo.txt
@@ -104,21 +141,22 @@ data/countries.inc.pl: tools/mkcountries-geonames data/countryInfo.txt
 
 data/browser: data/browser/support.inc.pl data/browser/usage-wm.inc.pl
 
+data/browser/caniuse/fulldata-json/data-2.0.json: data/browser/caniuse
 data/browser/caniuse/data.json: data/browser/caniuse
-data/browser/caniuse:
+data/browser/caniuse: $(download)
        $(call gitsave,https://github.com/Fyrd/caniuse.git)
 
-data/browser/support.inc.pl: tools/mkcaniuse data/browser/caniuse/data.json
+data/browser/support.inc.pl: tools/mkcaniuse data/browser/caniuse/fulldata-json/data-2.0.json
        $(call cmdsave,$^)
 
-data/browser/usage-wm.tsv:
+data/browser/usage-wm.tsv: $(download)
        tools/wget-ifmodified https://analytics.wikimedia.org/datasets/periodic/reports/metrics/browser/all_sites_by_browser_family_and_major_percent.tsv $@
 
 data/browser/usage-wm.inc.pl: tools/mkusage-wikimedia data/browser/usage-wm.tsv
        $(call cmdsave,$^)
 
 clean:
-       -rm data/digraphs.inc.pl
+       -rm data/digraphs.json
        -rm data/unicode-char.inc.pl
        -rm data/unicode-age.inc.pl
        -rm -rf data/font/
@@ -126,8 +164,3 @@ clean:
        -rm data/browser/support.inc.pl
        -rm data/browser/usage-wm.inc.pl
 
-.SECONDEXPANSION:
-
-data/writing-latn.inc.pl: tools/perlinc-static $$(@F)
-       $(call cmdsave,$^)
-
diff --git a/Shiar_Sheet/DB.pm b/Shiar_Sheet/DB.pm
new file mode 100644 (file)
index 0000000..e978d80
--- /dev/null
@@ -0,0 +1,24 @@
+package Shiar_Sheet::DB;
+
+use 5.014;
+use warnings;
+use DBIx::Simple;
+
+our $VERSION = '1.00';
+
+my @dbinfo = (
+       'DBI:Pg:dbname=sheet;host=localhost', 'sheetadmin', 'fairuse',
+) or die "database not configured\n";
+our $db;
+
+sub connect {
+       return $db if $db and $db->dbh->ping;
+       $db = DBIx::Simple->new(@dbinfo[0..2], {
+               RaiseError => 1,
+               pg_enable_utf8 => 1,
+       });
+       $db->abstract->{array_datatypes}++;
+       return $db;
+}
+
+1;
diff --git a/Shiar_Sheet/FormRow.pm b/Shiar_Sheet/FormRow.pm
new file mode 100644 (file)
index 0000000..03f1c22
--- /dev/null
@@ -0,0 +1,65 @@
+package Shiar_Sheet::FormRow;
+
+use 5.014;
+use warnings;
+use PLP::Functions 'EscapeHTML';
+
+our $VERSION = '1.00';
+
+sub input {
+       my ($row, $col, $attr) = @_;
+       my $val = $row->{$col} // '';
+       my $html = '';
+       $html .= qq( $_="$attr->{$_}") for sort grep {!/^-/} keys %{$attr // {}};
+
+       if (my $options = $attr->{-select}) {
+               $options = $options->(@_) if ref $options eq 'CODE';
+               $options->{$val} //= "unknown ($val)";  # preserve current
+               return (
+                       sprintf('<select id="%s" name="%1$s">', $col),
+                       (map { sprintf('<option value="%s"%s>%s</option>',
+                               $_, $val eq $_ && ' selected', $options->{$_}
+                       ) } sort keys %{$options}),
+                       '</select>',
+               );
+       }
+       elsif ($attr->{type} eq 'textarea') {
+               return (
+                       (map {
+                               sprintf('<label for="%s">%s</label>', $col, $_)
+                       } $attr->{-label} // ()),
+                       sprintf('<textarea id="%s" name="%1$s"%s>%s</textarea>',
+                               $col, $html, EscapeHTML($val)
+                       ),
+               );
+       }
+       elsif ($attr->{type} eq 'checkbox') {
+               $html .= ' checked' if $val;
+               return sprintf(
+                       join('',
+                               '<label>',
+                               '<input name="%1$s" value="0" type="hidden" />',
+                               '<input id="%s" name="%1$s" value="1"%s>',
+                               ' %s</label>',
+                       ), $col, $html, $attr->{-label}
+               );
+       }
+       else {
+               my $multiple = ref $val eq 'ARRAY' || $attr->{-multiple};
+               return (
+                       (map {
+                               sprintf('<label for="%s">%s</label>', $col, $_)
+                       } $attr->{-label} // ()),
+                       $multiple ? '<span class="inline multiinput">' : (),
+                       (map {
+                               sprintf('<input name="%s" value="%s" />', $col, EscapeHTML($_))
+                       } ref $val eq 'ARRAY' ? @{$val} : ()),
+                       sprintf('<input id="%s" name="%1$s" value="%s"%s />',
+                               $col, $multiple ? '' : EscapeHTML($val), $html
+                       ),
+                       $multiple ? '</span>' : (),
+               );
+       }
+}
+
+1;
index c93a48a708fbff4eccddabfc0bd2a3102759d207..f471497b1f4bbb8687920a461ae0f6635d2b3661 100644 (file)
@@ -8,7 +8,7 @@ use utf8;
 use Data::Dump 'pp';
 use PLP::Functions 'EscapeHTML';
 
-our $VERSION = '1.08';
+our $VERSION = '1.10';
 
 our $uc = do 'data/unicode-char.inc.pl';
 
@@ -17,14 +17,25 @@ sub new {
        bless { anno => ['di', 0], style => 'di' }, $class;
 }
 
-sub glyph_info {
+sub glyph_mkinfo {
        my ($self, $codepoint) = @_;
-       return $uc->{chr $codepoint} || eval {
+       # attempt to get unicode character information
+       my $info = eval {
                require Unicode::UCD;
-               if (my $fullinfo = Unicode::UCD::charinfo($codepoint)) {
-                       return [@$fullinfo{qw/category name - string/}];
-               }
-       } || [];
+               Unicode::UCD::charinfo($codepoint)
+                       || { category => 'Xn', name => '' };
+       } or return;
+       my $string;
+       if ($info->{combining}) {
+               # overlay combining diacritics
+               $string = chr(9676) . chr($codepoint);
+       }
+       return [@$info{qw( category name )}, undef, $string];
+}
+
+sub glyph_info {
+       my ($self, $codepoint) = @_;
+       return $uc->{chr $codepoint} || $self->glyph_mkinfo($codepoint) || [];
 }
 
 sub glyph_html {
@@ -34,7 +45,7 @@ sub glyph_html {
        my ($class, $name, $mnem, $entity, $string) = @$info;
 
        my $cell = EscapeHTML($string || $char);
-       my $title = sprintf 'U+%04X%s', $codepoint, !!$name && " ($name)";
+       my $title = sprintf 'U+%04X%s', $codepoint, !!$name && " $name";
 
        $cell = "<span>$cell</span>" if $class and $class =~ /\bZs\b/;
        $cell = '&nbsp;' if $cell eq '';
@@ -61,6 +72,31 @@ sub glyph_cell {
        return sprintf('<td class="%3$s" title="%2$s">%s', $self->glyph_html($char));
 }
 
+sub glyph_level_univer {
+       my ($self, $input) = @_;
+       if ($input =~ /\p{age=unassigned}/) {
+               # check include for assignments after unicode 6.0 (perl v5.14)
+               state $agemap = do 'data/unicode-age.inc.pl';
+               my $version = $agemap->{ord $input};
+               return $version ? 'l2' : 'l1';
+       }
+       elsif ($input =~ /^\p{in=1.1}*$/) {
+               return 'l5';  # first release 1993
+       }
+       elsif ($input =~ /^\p{in=3.0}*$/) {
+               return 'l4';  # 20th century
+       }
+       elsif ($input =~ /^\p{in=4.1}*$/) {
+               return 'l4';  # over 10 years ago
+       }
+       elsif ($input =~ /^\p{in=6.0}*$/) {
+               return 'l3';  # before 2012
+       }
+       else {
+               return 'l2';  # more recent
+       }
+}
+
 sub cell {
        my ($self, $input, $html) = @_;
        my (@class, $title, $cell, $mnem, $entity);
@@ -81,33 +117,13 @@ sub cell {
 
                $input =~ s/^\\//;  # escaped char
                ($cell, $title, my $class, $mnem, $entity) = $self->glyphs_html($input);
-               my $codepoint = ord $input;
 
                if ($self->{style} eq 'univer') {
-                       if ($input =~ /\p{age=unassigned}/) {
-                               # check include for assignments after unicode 6.0 (perl v5.14)
-                               state $agemap = do 'data/unicode-age.inc.pl';
-                               my $version = $agemap->{$codepoint};
-                               push @class, $version ? 'l2' : 'l1';
-                       }
-                       elsif ($input =~ /^\p{in=1.1}*$/) {
-                               push @class, 'l5';  # first release 1993
-                       }
-                       elsif ($input =~ /^\p{in=3.0}*$/) {
-                               push @class, 'l4';  # 20th century
-                       }
-                       elsif ($input =~ /^\p{in=4.1}*$/) {
-                               push @class, 'l4';  # over 10 years ago
-                       }
-                       elsif ($input =~ /^\p{in=6.0}*$/) {
-                               push @class, 'l3';  # before 2012
-                       }
-                       else {
-                               push @class, 'l2';  # more recent
-                       }
+                       push @class, $self->glyph_level_univer($input);
                        next;
                }
 
+               my $codepoint = ord $input;
                if ($self->{style} eq 'di') {
                        if ($mnem and $mnem =~ /…/) {
                                # incomplete representation, usually partial
diff --git a/Shiar_Sheet/ImagePrep.pm b/Shiar_Sheet/ImagePrep.pm
new file mode 100644 (file)
index 0000000..328cb4f
--- /dev/null
@@ -0,0 +1,92 @@
+package Shiar_Sheet::ImagePrep;
+
+use 5.020;
+use warnings;
+use experimental 'signatures';
+
+our $VERSION = '1.03';
+
+sub new ($class, $target) {
+       bless \$target, $class;
+}
+
+sub download ($target, $download) {
+       # copy changed remote url to local file
+       unlink $$target if -e $$target;
+       defined $download or return 1;
+       require LWP::UserAgent;
+       my $ua = LWP::UserAgent->new;
+       $ua->agent('/');
+       my $status = $ua->mirror($download, $$target);
+       $status->is_success
+               or die "Download from <q>$download</q> failed: ".$status->status_line."\n";
+}
+
+sub dimensions ($imgpath) {
+       require IPC::Run;
+       IPC::Run::run(
+               [identify => -format => '%w %h', $$imgpath],
+               '<' => \undef, '>&' => \my $xy
+       ) or die ["Image dimensions could not be determined.", $$imgpath];
+       return split /\s/, $xy, 3;
+}
+
+sub generate ($imgpath, $thumbpath, $opt) {
+       if (not -e $$imgpath) {
+               return !-e $thumbpath || unlink $thumbpath;
+       }
+       my @cmds = @{$opt->{convert} // []};
+       unshift @cmds, -area => $_ for $opt->{crop32} || ();
+       $imgpath->convert($thumbpath, \@cmds, '300x200') and # low-res cover
+       $imgpath->convert($thumbpath =~ s/\.jpg$/.webp/r,
+               [@cmds, -quality => 40], '600x400' # higher dpi tradeoff
+       );
+}
+
+sub convert ($imgpath, $thumbpath, $cmds, $xyres = 0) {
+       #my ($w, $h) = $imgpath->dimensions;
+       #my $aspect = 3/2; # $xyres
+       my @cmds = @{$cmds};
+       if (my ($cmdarg) = grep { $cmds[$_] eq '-area' } 0 .. $#cmds) {
+               # replace option by permillage crop
+               my @dim = map { $_ / 1000 } split /\D/, $cmds[$cmdarg + 1];
+               $dim[$_] ||= 1 for 2, 3; # optional end
+               push @dim, $dim[2 + $_] - $dim[$_] for 0, 1; # add width, height
+               splice @cmds, $cmdarg, 2, (
+                       #crop="%[fx:floor(w*$ratio)]x%[fx:floor(h*$ratio)]"
+                       #crop="$crop+%[fx:ceil((w-w*$ratio)/2)]+%[fx:ceil((h-h*$ratio)/2)]"
+                       -set => 'option:distort:viewport' => sprintf(
+                               '%%[fx:%s]x%%[fx:%s]+%%[fx:%s]+%%[fx:%s]',
+                               "w*$dim[4]", "h*$dim[5]", # width x height
+                               #"max(w*$dim[4], h*$dim[5]*$aspect)", # width
+                               #"max(h*$dim[5], w*$dim[4]/$aspect)", # height
+                               "w*$dim[0]", "h*$dim[1]", # x+y offset
+                       ),
+                       -distort => SRT => 0, # noop transform to apply viewport
+               );
+       }
+       @cmds = (
+               'convert',
+               $$imgpath,
+               -delete => '1--1', -background => 'white',
+               '-strip', -quality => '60%', -interlace => 'plane',
+               -gravity => defined $cmds ? 'northwest' : 'center',
+               @cmds,
+               $xyres ? (-resize => "$xyres^", -extent => $xyres) : (),
+               $thumbpath
+       );
+
+       $imgpath->runcommand(@cmds);
+}
+
+sub runcommand ($, @cmds) {
+       require IPC::Run;
+       my $output;
+       IPC::Run::run(\@cmds, '<' => \undef, '>&' => \$output) or die [
+               "Failed to convert source image.",
+               "@cmds\n" .
+               ($output || ($? & 127 ? "signal $?" : "error code ".($? >> 8))),
+       ];
+}
+
+1;
index 98c5a40a330a774e564464f4ceb005db21227ce0..c7e455b353616b10d7780c963c02c618cfba8e20 100644 (file)
@@ -6,7 +6,7 @@ use warnings;
 no  warnings 'uninitialized';  # save some useless checks for more legible code
 use Carp;
 
-our $VERSION = '2.07';
+our $VERSION = '2.10';
 
 my @casedesc = (undef, qw/shift ctrl meta/, 'shift meta');
 my @rowdesc = qw(numeric top home bottom);
@@ -56,6 +56,7 @@ sub escapeclass {
        s/\+/_m/g;
        s/\[/_sbo/g;
        s/\]/_sbc/g;
+       s/\\/_b/g;
        s/^$/_/;
        return $_;
 }
@@ -110,18 +111,18 @@ sub print_key {
        if (not defined $flags) {
                $flags = $key eq '^0' ? 'ni' : 'no';
        }
-       elsif ($flags =~ s/^=//) { # alias
-               $desc = $self->{sign}->{alias};
-               $desc .= $flags eq "\e" ? 'esc' : $flags;
-               $flags = $self->keyunalias($flags) . ' alias';
+       elsif ($flags =~ s/^=(\S+)\s?//) { # alias
+               my $ref = $1;
+               $desc = $self->{sign}->{alias} . ($ref eq "\e" ? 'esc' : $ref);
+               $flags = join ' ', $self->keyunalias($ref), 'alias', $flags;
        }
        if (my $txt = $self->{key}->{$mode.$key}) {
                ($desc, $mnem) = split /\n/, $self->escapedesc($txt);
        }
 
        my $keytxt = $self->print_letter($key, $mode);
-          $keytxt .= $self->{sign}->{$1} while $flags =~ s/(?:^| )(arg[a-ln-z]?)\b//;  # arguments
           $keytxt .= "<small>$self->{sign}->{motion}</small>" if $flags =~ s/ ?\bargm\b//;  # motion argument
+          $keytxt .= $self->{sign}->{$1} while $flags =~ s/(?:^| )(arg[a-ln-z]?)\b//;  # arguments
        my $keyhint = defined($mnem) && qq{ title="$mnem"};
           $keytxt  = "<b$keyhint>$keytxt</b>";
           $keytxt .= ' '.$desc if defined $desc;
@@ -130,6 +131,8 @@ sub print_key {
                ' onclick="setmode(%s)"',
                $1 eq '' ? '' : sprintf(q{'mode%s'}, escapeclass($1))
        );
+       $flags =~ s/\bx\w+/ext/;
+       $flags =~ s/\bv\d+/new/;
        $flags .= ' chr'.ord(substr $key, -1) if $key ne '^0';
 
        print qq{\t\t<td class="$flags"$onclick>$keytxt};
@@ -147,11 +150,16 @@ sub print_rows {
        );
        my @modes = sort keys %{ $self->{def} };
 
-       print '<table id="rows" class="keys">'."\n\n";
+       printf '<table id="rows" class="%s">'."\n\n", $self->{tableclass} // 'keys';
 
+print_row:
        for (my $row = -1; $row <= $#{ $keyrows{$self->{map}} }; $row++) {
                my $keyrow = $row < 0 ? [["\e"]] : $keyrows{$self->{map}}->[$row];
 
+#              grep {
+#                      defined $self->{def}->{''}->{$_} or defined $self->{def}->{g}->{$_}
+#              } map { @{$_} } @{$keyrow} or next;
+
                printf qq{<tbody class="row row%d">\n}, $row+1;
                for my $basemode (@modes) {
                        my @moderows = split /\s+/,
@@ -160,8 +168,8 @@ sub print_rows {
 
                for my $submode (@moderows ? @moderows : '') {
                        my $mode = $basemode . $submode;
-                       my @caserows = $mode =~ s/(\d+)(?:-(\d+))?$//
-                               ? (map {$_ - 1} split //, $row == 0 && $2 || $1)  # user override
+                       my @caserows = $mode =~ s/(\d+)(?:-(\d*))?$//
+                               ? (map {$_ - 1} split //, $row == 0 ? $2 // $1 : $1)  # user override
                                : @$defrows;  # default
                        my $modekeys = $self->{def}{$mode};
 
@@ -199,44 +207,12 @@ sub print_legend {
        my ($class, $flags) = @_;
 
        say qq{\t\t<dl class="legend $class">};
-       printf("\t\t".'<dt class="%s">%s'."\n\t\t\t".'<dd>%s'."\n",
+       printf("\t\t".'<dt class="%s">%s'."\n\t\t\t".'<dd>%s</dd>'."\n",
                $_, map { $self->escapedesc($_) } @{ $self->{flag}->{$_} || ["($_)", '...'] }
        ) for @$flags;
        say "\t\t</dl>";
 }
 
-sub print_legends {
-       my $self = shift;
-       my ($input) = @_;
-
-       say "<hr/>\n";
-       say '<div class="help">';
-
-       say "\t", '<div class="left">';
-       my @groups = sort grep {/^g\d/} keys %{ $self->{flag} };
-       $self->print_legend('legend-types', \@groups);
-       say "\t</div>\n";
-
-       say "\t", '<div class="right">';
-       my @attr = sort grep {!/^g\d/} keys %{ $self->{flag} };
-       $self->print_legend('legend-options', \@attr);
-       say '';
-
-       say "\t\t", '<ul class="legend legend-set">';
-
-       say "\t\t<li>keyboard <strong>map</strong> is ",
-               ($input->{map} ? 'set to ' : ''), "<em>$self->{map}</em>";
-       say "\t\t<li><strong>keys</strong> are ",
-               "<em>", ($self->{showkeys} ? 'always shown' : 'hidden if unassigned'), "</em>",
-               (!defined $self->{showkeys} && ' by default');
-       say "\t\t<li>default <strong>style</strong> is ",
-               (defined $input->{style} && 'set to '), "<em>$self->{style}</em>";
-
-       say "\t\t</ul>";
-       say "\t</div>\n";
-       say "</div>\n";
-}
-
 1;
 
 =head1 NAME
@@ -267,7 +243,7 @@ Shiar_Sheet::Keyboard - Output HTML for key sheets
 
 =head1 DESCRIPTION
 
-Used by http://sheet.shiar.nl to display keyboard sheets.
+Used by https://sheet.shiar.nl to display keyboard sheets.
 Assumes specific stylesheets and javascript from this site,
 so probably not of much use elsewhere.
 
diff --git a/Shiar_Sheet/KeyboardChars.pm b/Shiar_Sheet/KeyboardChars.pm
new file mode 100644 (file)
index 0000000..d0e84c8
--- /dev/null
@@ -0,0 +1,82 @@
+package Shiar_Sheet::KeyboardChars;
+
+use 5.020;
+use warnings;
+use utf8;
+use experimental 'signatures';
+use parent 'Exporter';
+use Unicode::Normalize qw( NFKD );
+use Text::Unidecode ();
+use Shiar_Sheet::FormatChar;
+
+our $VERSION = '1.04';
+our @EXPORT = qw( kbchars kbmodes );
+
+my $uc = Shiar_Sheet::FormatChar->new;
+
+our %unaccent = qw(
+       ⍺ a  ⍵ w  ∊ E  ⍷ E  ⍴ r  ⍳ i  ⍸ i  ○ O  ⍥ O  ⌿ /  ⍟ (*) ⊕ (+)
+       Ʊ U  ǝ e  Ǝ E  ʌ vA Ʌ VA ɥ h  ʘ O  ɰ mw ɯ mw Ɯ MW ə @ae Ə @AE
+       ɸ PF ʎ yl ɔ co Ɔ CO ɛ 3E ƣ q  Ƣ Q  ∀ A  ∃ E  ∪ u  ∩ n   ≠ !=
+       ≈ =~ ∅ /0 ∘ o  ⋅ .  ∫ s  ≝ =d ″ "  ≤ <  ≥ >  √ rV ∛ 3V  ∜ 4V
+       Α A  Β B  Γ G  Δ D  Ε E  Ζ Z  Η H  Θ CQ Ι I  Κ K  Λ L  Μ M
+       Ν N  Ξ X  Ο O  Π P  Ρ R  Σ S  Τ T  Υ YU Φ F  Χ CX Ψ Y  Ω W
+       α a  β b  γ g  δ d  ε e  ζ z  η h  θ cq ι i  κ k  λ l  μ m
+       ν n  ξ x  ο o  π p  ρ r  σ s  τ t  υ yu φ f  χ cx ψ y  ω w
+                                ς sc      ϑ cq                µ mu
+);
+
+sub unidecode {
+       return $unaccent{$_[0]} // Text::Unidecode::unidecode($_[0]);
+}
+
+sub kbchars ($rows) {
+       return kbmodes({'' => $rows});
+}
+
+sub kbmodes ($modes) {
+       my %g; # present group classes
+       my %info = (
+               tableclass => 'keys big',
+               rows => [1, 0],
+       );
+       for my $lead (keys %{$modes}) {
+               if ($lead ne '') {
+                       $info{def}->{''}->{$lead} = "g1 mode$lead";
+                       $g{g1} = 1;
+                       $info{mode}->{$lead} //= "mode $lead";
+                       $info{def}->{$lead}{$lead} = 'g1 mode'; # back
+               }
+               while (my ($k, $v) = each %{ $modes->{$lead} }) {
+                       my ($glyph, $title) = $uc->glyph_html($v);
+                       $info{key}{$lead.$k} = join "\n", $glyph, $title;
+                       my $c = $k =~ s/\A[+^](?=.)//r;  # trim modifier indicator
+
+                       my $class = (
+                                 !defined $v || $c eq $v ? 'no' # identical
+                               : $v =~ /\A\p{Mn}+\z/ ? 'g9' # combining accent
+                               : NFKD($v) =~ /\A\Q$c\E\p{Mn}*\z/ ? 'g2' # decomposed equivalent
+                               : unidecode($v) =~ /\Q$c\E+/i ? 'g4' # transliterated
+                               : $v =~ /\A[\p{Sk}\p{Lm}]+\z/ ? 'g8' # modifier symbol
+                               : $v =~ /\A[\pM\pP]+\z/ ? 'g7' # mark
+                               : $v =~ /^\p{Latin}/ ? 'g5' # latin script
+                               : 'g6'
+                       );
+                       $g{$class} = 1 unless $class eq 'no';
+                       $info{def}{$lead}{$k} //= $class;
+               }
+       }
+       $info{flag} = {%{{
+               g1 => ['mode' => "switch to an alternate set of keys"],
+               g2 => ['accented', "decomposes to the original letter with a combining accent"],
+               g4 => ['similar', "transliterates (mostly) into the unmodified letter"],
+               g5 => ['latin', "a different (accented) latin letter"],
+               g6 => ['symbol', "other character not directly deducible from key"],
+               g7 => ['punctuation', "(punctuation) mark"],
+               g8 => ['mark', "modifier letter or mark (spacing diacritic)"],
+               g9 => ['combining', "diacritical mark to be combined with a following character"],
+       }}{keys %g}};
+       return \%info;
+}
+
+1;
diff --git a/TODO b/TODO
index 4f444286c5015b3b568245d0b92347764923cf71..43eacb3ce29cbdd973318d084366fcb84324d2c4 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,6 +1,8 @@
+# ToDo
+
 - style for .undo (maybe just some css3-only shadow)
-- fix mode switching in konq (used to work)
-  only when <wbr>s present?! use browser sniffing to force ?ascii=0 :(
+- fix mode switching in konq (used to work)  
+  only when \<wbr>s present?! use browser sniffing to force ?ascii=0 :(
 - safari loading bug?
 - multiple stylesheets selectable
 - ghosting option documented and default
 - footer options clickable to change (javascript)
 - change options dynamically if possible (at least ?style)
 - footer style option to top position/button?
-- top-left header (logo to root)?
+- top-left header (logo to root)?  
   conflicts with Esc key positioning
 - /browser history
+- word
+       - interactive quiz
+               - image multiple choice from translation
+               - given image (options or free)
+               - memory (find groups)
+                       - match pairs
+                               - distinctive male/female plumage
+                               - young age (actors, animals)
+                               - marriages
+                               - plant tree/leaf/fruit/flower
+                       - duplicates
+                       - mirrored duplets
+               - remember order (progressive amount)
+               - 20 questions
+               - captcha quiz (confirm several options for question)
+       - alternate aspect ratios
+       - subcategories
+               - food
+                       - sumac
+               - animal species
+                       - photoshop cover image <https://www.telegraph.co.uk/news/0/piggyback-riders-funny-photos-of-lazy-animals-hitchhiking/>
+                       - dog breeds
+                       - cat kinds
+                       - ducks
+                       - birbs
+                       - mushrooms
+                       - spiders
+                       - distinct eyes (quiz linked animals)
+                       - match collective nouns
+               - mythological
+                       - hybrid creatures <https://en.wikipedia.org/wiki/List_of_hybrid_creatures_in_folklore>
+                       - greek deities <https://en.wikipedia.org/wiki/List_of_Greek_mythological_figures#Immortals>
+                               - roman
+               - media
+                       - tv series
+                               - known characters
+                                       - sesame street muppets
+                                               - international variants linked to countries
+                       - video games
+                               - screenshots
+                               - characters
+                                       - silhouette
+                               - maps
+               - famous people
+                       - historic hotornot
+                       - movie actors +characters
+                       - country leaders (↓)
+                       - nobel price winners
+               - clothing
+                       - traditional (+regions)
+                               - dress code
+                                       - fashion
+                       - costumes
+                               - cosplay +characters
+                       - jewelry
+                       - construction materials
+               - technical
+                       - connectors (plug+port)
+                       - removable media
+                       - computer processors (blur identifiers for quiz)
+                       - font families
+               - cocktails
+                       - moscow mule
+               - car brands
+               - countries
+                       - flags
+                       - capital buildings
+                       - traditional dress
+                       - representative cuisine
+                       - national animal/mascot
+       - api
index 355937232e021c3ab33c7da90b831302b759eb29..3683d543a30e813f15d58216b2ae509b1bb2b2b1 100644 (file)
@@ -1,5 +1,5 @@
 use utf8;
-( # dyadic, monadic
+[ # dyadic, monadic
 
 # arithmetic
 ["+\n-", "add\nSum of A and B", "conjugate\nNo change to B"],
@@ -122,4 +122,4 @@ undef,
 ["⍤\nJ", "rank\nApply function successively to the sub-arrays in B specified by k"],
 ["⍥\nO", "coax"],
 
-);
+];
diff --git a/apl.plp b/apl.plp
index 9ca99701ff2ff92f9f32176dbb1b12cdfeb24aef..302a55d5d0af890c5eb8328fb9bb1d5aeafdd50e 100644 (file)
--- a/apl.plp
+++ b/apl.plp
@@ -27,9 +27,7 @@ EOT
 
 use Shiar_Sheet::FormatChar;
 my $glyphs = Shiar_Sheet::FormatChar->new;
-
-my @ops = do 'apl.inc.pl';
-@ops > 1 or die "cannot open operator include: $@\n";
+my $ops = Data('apl');
 
 :>
 <h1>APL Symbols</h1>
@@ -49,7 +47,7 @@ my @ops = do 'apl.inc.pl';
 <tbody>
 
 <:
-for my $op (@ops) {
+for my $op (@{$ops}) {
        $op or do {
                say '<tbody>';
                next;
@@ -70,9 +68,9 @@ for my $op (@ops) {
                [defined $entity ? 'l4' : $ascii ? 'l5' : 'l1', $entity // "#$codepoint"],
        );
        printf(
-               defined $_ ? '<td%s>%s' : '<td class=Xi>',
-               map { !!$_->[1] && qq( title="$_->[1]"), $_->[0] }
-               [map { EscapeHTML($_) } split /\n/, $_, 2]
+               '<td%s>%s',
+               map { defined ? (!!$_->[1] && qq( title="$_->[1]"), $_->[0]) : (' class=Xi', '') }
+               $_ && [map { EscapeHTML($_) } split /\n/, $_, 2]
        ) for $monad, $dyad;
        say '</td>';
 }
index f20f167b3a9f184b95a5dc674d2118cd2e4c72ae..e4ed58d7fb2da99e8fbd74814177f6124730add6 100644 (file)
--- a/base.css
+++ b/base.css
@@ -33,6 +33,8 @@ caption aside {
        margin-left: 1ex;
        font-weight: normal;
        display: inline;
+       font-size: 91%; /* 100% */
+       margin-top: .3ex; /* align with 110% baseline */
 }
 
 hr {
@@ -99,8 +101,10 @@ ul {
 }
 
 pre {
-       display: inline-block;
        text-align: left;
+       margin: 2ex auto;
+       white-space: pre-wrap;
+       overflow-wrap: break-word;
 }
 body > pre {
        width: 78ch;
@@ -109,27 +113,56 @@ body > pre {
        padding: 0 1em;
        border-width: 0 1px;
        border-style: solid;
+       white-space: pre;
+       font-size: 1.9vmin; /* cover full width at most */
+}
+code {
+       white-space: nowrap;
 }
 
+h1 small,
 h2 small {
        position: absolute; /* side note; do not influence alignment */
        margin-left: 1em;
+       font-weight: normal;
+       font-size: 50%; /* 1rem */
+       padding-top: 1.75ex; /* align baseline with container */
+}
+h2 small {
+       font-size: 90.9%;
+       padding-top: .17ex;
 }
 
-dl > dt {
-       float: left;
-       width: 50%;
+.section dl {
+       display: grid;
+       grid: auto-flow / 1fr 1fr;
+       clear: both;
+}
+.section dl > dt {
+       grid-column: 1;
        text-align: right;
 }
+dt code {
+       white-space: normal;
+}
 dl > dd {
+       grid-column: 2;
        text-align: left;
        padding-left: 1em;
-       overflow: hidden;
+       margin: 0 0 .5ex;
+}
+@media (max-width: 42em) {
+       .section dl {
+               grid: auto-flow / minmax(8em, 1fr) minmax(20em, 1fr);
+       }
+       dl > dd {
+               margin-bottom: 1ex; /* distinguish rows more as dts can wrap */
+       }
 }
 
 /* "keyboard" (list of keys) */
 
-#rows {margin-top: -5ex} /* top (esc) row fits besides header */
+.row0 {margin-top: -5ex} /* top (esc) row fits besides header */
 .row2 {margin-left: 7em} /* row offsets relative to ~6em key width */
 .row3 {margin-left: 8em}
 .row4 {margin-left: 10em} /* should actually align to next key on row1 */
@@ -152,20 +185,18 @@ h3      {display: none} /* semantic details (non-css/js) */
 
 table.keys {
        display: block;
-       width: 82.5em; /* 12 * td(2px + 1px + 6.2em + 1px + 2px) + 8em */
-//     padding-right: 72px;
        border-spacing: 0;
        border-collapse: collapse;
-}
-table.keys > * {
-       margin-right: -72px;
+       white-space: nowrap;
+       text-align: left;
 }
 
 /* individual keys */
 
 dl.legend dt,
 .keys td {
-       float: left;
+       display: inline-block;
+       white-space: normal;
        width: 6.2em;
        line-height: 2.25ex; /* a little terser (seems to be gecko's default anyway) */
        height: 4.5ex; /* 2 lines */
@@ -215,6 +246,49 @@ dl.legend dt,
        font-weight: normal; /* nice and subtle */
 }
 
+/* enlarged keys */
+
+.keys.big td {
+       width: 1em;
+       min-width: auto;
+       height: 2.25ex;
+       font-size: 200%;
+       font-size: calc(7vmin - 4px); /* fit 12 keys to page */
+       overflow: visible;
+       position: relative;
+       padding: 0;
+       margin: 0 .2vw -1px;
+}
+
+/* override row alignments */
+.keys.big tbody {
+       font-size: calc(1.4vmin); /* enlarged td conversion */
+}
+.keys.big .row2 {margin-left: 7em}
+.keys.big .row3 {margin-left: 8em}
+.keys.big .row4 {margin-left: 10em}
+.keys.big .row0 {
+       display: none; /* headerless */
+}
+
+.keys.big td b {
+       position: absolute; /* overlay */
+       z-index: 1;
+       top: -1.2ex; /* halfway over shift */
+       left: 0;
+       right: 0;
+       font-size: 50%;
+       opacity: .5;
+       color: #FFF;
+       line-height: 2.25ex;
+}
+.keys.big.cmp td b,
+.keys.big .ctrl td b,
+.keys.big .meta td b,
+.keys.big .shift td b {
+       display: none;
+}
+
 /* tables */
 
 table {
@@ -243,6 +317,12 @@ td.joinl {
        border-left: none;
 }
 
+thead {
+       position: sticky;
+       top: 0;
+       background: #DDD8;
+}
+
 /* character table */
 
 .glyphs thead th, .glyphs td {
@@ -276,6 +356,10 @@ thead td {
 .glyphs thead td {
        width: auto; /* no glyph cells in header */
 }
+.glyphs caption {
+       margin-left: 2.2em; /* 1ex + offset head column (1.6em + 0.4em) / 110% */
+               /* adjusted insignificant -.2em to fit wide contents on /charset/mac */
+}
 th {
        padding: 0 0.2em;
 }
@@ -436,10 +520,10 @@ table.dimap {
 .u-invalid {background: #BBB} /* invalid, impossible */
 
 /* foreground representation */
-#digraphs .u-l3 {color: #080} /* partial */
-#digraphs .u-l3.ex {color: #4C0} /* experimental */
-#digraphs .u-l2 {color: #A44; color: rgba(128, 0, 0, .6)} /* unofficial proposal */
-#digraphs .u-l1 {color: #D00; color: rgba(255, 0, 0, .8)} /* minimal or invalid */
+#digraphs .u-l4 {color: #080} /* partial */
+#digraphs .u-l5 {color: #4C0} /* experimental */
+#digraphs .u-l2 {color: #A44; color: rgba(128, 0, 0, .6)} /* unofficial */
+#digraphs .u-l1 {color: #D00; color: rgba(255, 0, 0, .8)} /* missing */
 
 /* support percentage (browser cells) */
 .p0         {opacity: .6}
@@ -464,10 +548,10 @@ table.dimap {
 /* code syntax */
 .sy-comment    { color: #888 }
 .sy-constant   { color: #008 }
-.sy-type,
 .sy-identifier { color: #804 }
 .sy-statement  { }
 .sy-preProc    { }
+.sy-type,
 .sy-special    { color: #408 }
 .sy-error      { font-weight: bold; background-color: #F00; color: #FFF }
 .sy-todo       { background-color: #FF0 }
@@ -511,8 +595,8 @@ table.dimap {
 .l3:hover                                  {background: #FF8}
 .l4:hover                                  {background: #CF8}
 .l5:hover                                  {background: #8F8}
-.u-l3:hover    {outline: 1px solid #080}
-.u-l3.ex:hover {outline: 1px solid #8F0}
+.u-l4:hover    {outline: 1px solid #080}
+.u-l5:hover    {outline: 1px solid #8F0}
 .u-l2:hover    {outline: 1px solid #800}
 .u-l1:hover    {outline: 1px solid #F00}
 
@@ -569,12 +653,13 @@ dl.legend dt.more:hover,
 .keys td.more:hover b {
        text-shadow: #F20 0 0 0.5em, #FC0 0 0 0.2em;
 }
-dl.legend dt.ext,
-.keys td.ext {
-       border-style: dashed;
-}
 dl.legend dt.new,
 .keys td.new {
+       border-style: dashed;
+}
+.ext,
+dl.legend dt.ext,
+.keys td.ext {
        opacity: .6;
 }
 
@@ -650,6 +735,128 @@ ul.legend-set li {
        padding: 0 0.2em;
 }
 
+/* images */
+
+figure {
+       margin: 0;
+       position: relative;
+}
+figure img {
+       vertical-align: bottom;
+       width: 100%;
+}
+
+@media (min-width: 60em) {
+       figcaption {
+               padding: 0 1em;
+               color: #000;
+               background: rgba(255, 255, 255, .66);
+               position: absolute;
+               right: 0;
+               bottom: 0;
+               max-width: 100%;
+               box-sizing: border-box;
+       }
+       .gallery li.parent:hover > figure > figcaption,
+       .gallery figure:hover > figcaption {
+               /* highlight title of current and parents */
+               font-size: 175%;
+               right: 50%;
+               bottom: 50%;
+               transform: translate(50%, 50%);
+               margin-left: -60%; /* keep width */
+       }
+}
+
+/* image gallery */
+
+.gallery {
+       display: grid;
+       grid: auto-flow dense / repeat(auto-fit, minmax(200px, 1fr));
+       grid-gap: 1px;
+}
+.gallery li, .gallery ul {
+       display: contents;
+}
+.gallery figure {
+       overflow: hidden;
+       box-sizing: border-box;
+       hyphens: auto;
+       max-width: 900px;
+}
+.gallery figcaption > small {
+       display: inline-block;
+}
+
+@media (min-width: 403px) and (min-height: 266px) {
+       /* able to fit 2 cells of 200x133 */
+       .gallery li.large > figure {
+               grid-row: span 2;
+               grid-column: span 2;
+       }
+}
+@media (min-width: 603px) and (min-height: 400px) {
+       /* fit 3 cells of 200x133 */
+       .gallery > li:first-child > figure,
+       .gallery li.huge > figure {
+               grid-row: span 3;
+               grid-column: span 3;
+       }
+}
+
+.gallery figure, .gallery figcaption {
+       transition: all .5s ease-in;
+}
+.gallery figure:hover ~ ul figcaption {
+       /* mark all children */
+       color: #FFF;
+       background: rgba(0, 0, 0, .5);
+}
+
+.gallery figure[data-sup]:after {
+       position: absolute;
+       right: 0;
+       content: attr(data-sup);
+       color: #FFF;
+       background: #0006;
+       border-radius: 1em;
+       padding: .1ex .4em;
+       margin: .4em;
+}
+.gallery .expand > figure[data-sup]:after {
+       content: '+' attr(data-sup);
+       background: #0008;
+       font-size: 150%;
+       border: 2px solid #FFF8;
+}
+
+/* specialised galleries */
+
+body#word {
+       margin: 8px 1px;
+}
+
+table.gallery {
+       grid-auto-flow: row;
+       grid-template-columns: repeat(auto-fit, minmax(2em, max-content)); /* 1fr */
+}
+table.gallery tbody,
+table.gallery tr {
+       display: contents;
+}
+table.gallery tr > :first-child {
+       grid-column: 1;
+       -grid-row: span 6;
+       margin: auto; /* vertical-align: middle */
+}
+table.gallery tr > :nth-child(2) {
+       grid-column: 2; /* in case 1st is missing */
+}
+table.gallery td {
+       border: 0; /* does not collapse */
+       outline: 1px solid #888; /* over grid-gap */
+}
+
 /* page-specific */
 
 #browser td > a {
@@ -713,6 +920,69 @@ nav > .section {
        margin-top: 1em;
 }
 
+.units tbody tr:hover:not(.race) {
+       background: #EEE;
+}
+.unit-gas {
+       color: #040;
+}
+.unit-min, .unit-min a:not(:hover) {
+       color: #004;
+}
+.unit-supply {
+       color: #080;
+}
+.unit-o {color: #C08} /* organic */
+.unit-u {color: #44C} /* mechanic */
+.unit-p {color: #0A8} /* psionic */
+.unit-composed {
+       color: #C88;
+}
+.unit-air {
+       color: #08C;
+}
+.unit-x {color: #888}
+.unit-s {color: #890}
+.unit-m {color: #C70}
+.unit-l {color: #D22}
+.unit-h {color: #804}
+.magic-opt:before,
+.magic-opt:after {
+       color: #000;
+}
+.hurtrel, .units .hurtrel {
+       color: #778;
+}
+tbody .unit-shield {
+       color: #64A;
+}
+.unit-pdd {
+       color: #A8C;
+}
+.unit-splash {
+       color: #4A0;
+}
+.hurt-a {
+       color: #036;
+}
+.hurt-g {
+       color: #063;
+}
+.unit-massive {
+       color: #D88;
+}
+.unit-detect::before {
+       color: #0A8;
+}
+.unit-jump {
+       color: #8A4;
+}
+body .magic-perma {
+               text-decoration-color: #8C0;
+          -moz-text-decoration-color: #8C0;
+       -webkit-text-decoration-color: #8C0;
+}
+
 /* printing hints */
 
 @page {
@@ -726,10 +996,19 @@ nav > .section {
 
 /* terse optimisation */
 
+@media (min-height: 112ex) and (min-width: 90em) {
+       .keys td {
+               padding: 1ex 0 1ex .1em;
+               width: 7em;
+       }
+}
+
 @media (max-width: 79em) {
        .keys td {
                position: relative; /* hides overflow */
                width: 4.5em;
+               min-width: 6.5vw;
+               min-width: calc(7.7vw - 8px);
        }
        .keys td b,
        .keys .meta td b,
@@ -743,13 +1022,26 @@ nav > .section {
                color: #FFF;
        }
 
-       table.keys {
-               width: 62.1em; /* 82.5em - 12 * Δtd(6.2em - 4.5em) */
-       }
        .row2 {margin-left: 5.3em} /* 7em / Δtd(6em : 4.5em) */
        .row3 {margin-left: 6em}   /* 8em / Δtd */
        .row4 {margin-left: 7.5em} /* 10em / Δtd */
 
+       /* letter scripts columns to rows */
+       .legend .glyphs:first-child td {
+               display: table-row;
+               vertical-align: baseline;
+       }
+       .legend .glyphs td > table {
+               width: auto;
+               display: inline;
+               margin-left: 1ex;
+       }
+       .legend .glyphs:first-child td td {
+               margin: 2px;
+               display: inline-block;
+               width: auto;
+       }
+
        @media (max-width: 61em) {
                .keys td {
                        width: 3em;
@@ -763,13 +1055,34 @@ nav > .section {
                        line-height: 4ex;
                }
 
-               table.keys {
-                       width: 37em; /* (12 * td(3em + 6px) + 8em) * 80% */
-               }
                .row2 {margin-left: 3.5em} /* 7em / Δtd(6em : 3em) */
                .row3 {margin-left: 4em}   /* 8em / Δtd */
                .row4 {margin-left: 5em}   /* 10em / Δtd */
        }
+
+       @media (max-width: 42em) {
+               /* flatten right legend column on mobile */
+               .help > * {
+                       display: table-row;
+                       width: auto;
+               }
+               ul.legend-set {
+                       clear: left;
+               }
+               .right dl.legend {
+                       margin-right: 0;
+                       margin-left: 6.4em;
+               }
+               .right dl.legend dt {
+                       margin-right: 0;
+                       margin-left: -6.4em;
+                       float: left;
+                       clear: left;
+               }
+               .right dl.legend dd {
+                       float: left;
+               }
+       }
 }
 
 /*
@@ -779,8 +1092,6 @@ nav > .section {
                margin-top: 1ex;
                transform: rotate(90deg);
                transform-origin: top left;
-               width: 68em;
-               height: 37em;
                margin-left: 40em;
                margin-bottom: 30em;
                font-size: 80%;
index f1ed06cfa42265c32a2719ae78819896a7e4a55e..a68a71ff6e1c5c30c0ce1bedb956fc4a75ba5f8d 100644 (file)
--- a/base.plp
+++ b/base.plp
@@ -2,7 +2,7 @@
 
 Html({
        title => 'number bases',
-       version => '1.1',
+       version => '1.2',
        description => [
                "Cheat sheets summarising various software programs and standards.",
        ],
@@ -15,20 +15,53 @@ Html({
 my @cols = (2, 6, 8, 9, 10, 12, 16, 18, 20);
 my @morecols = (2 .. 6, 8, 9, 10, 12, 16, 18, 20, 24, 32, 36, 64);
 my @char = (0..9, 'A'..'Z', 'a'..'z');
+my %RADIXNAME = (
+        2 => 'binary',
+        3 => 'ternary',
+       #4 => 'quaternary',
+       #5 => 'quinary',
+        6 => 'senary',
+        8 => 'octal',
+       #9 => 'nonary',
+       10 => 'decimal',
+       12 => 'dozenal', # duodecimal
+       16 => 'hexadecimal',
+       20 => 'vigesimal',
+       #36 => 'double-senary',
+       60 => 'sexagesimal',
+       #64 => 'double-octal',
+);
 :>
 <h1>Number bases</h1>
 
 <h2>Radix economy</h2>
-<table>
+<table class=mapped>
 <:
+sub showcolhead {
+       print '<col>';
+       my @spans;
+       $spans[ $_ > 10 ]++ for @_;
+       print "<colgroup span=$_>" for @spans;
+       print '<thead><tr><th>';
+       for (@_) {
+               print '<th>', $_ < 36 ? $char[$_] : $char[35].'+'.$char[$_ - 35];
+               print " <small>($_)</small>" for join(', ',
+                       $RADIXNAME{$_} // (),
+                       $_ >= 10 ? "base$_" : (),
+               ) || ();
+       }
+       say '</thead>';
+}
+
 sub radix_economy {
        my ($val, $radix) = @_;
        return $radix * int(log($val) / log($radix) + 1);
 }
 
 use List::Util 'sum';
-print '<tr><th>';
-print '<th>', $_ for @morecols;
+
+showcolhead(@morecols);
+
 for my $max (100, 255, 1024) {
        print '<tr><th>⍳', $max;
        for my $radix (@morecols) {
@@ -39,11 +72,8 @@ for my $max (100, 255, 1024) {
 :></table>
 
 <h2>Reciprocal fractions (n⁻¹)</h2>
-<table>
+<table class=mapped>
 <:
-print '<tr><th>';
-print '<th>', $_ for @cols;
-
 use Math::BigFloat;
 
 my $count = 40;
@@ -80,7 +110,10 @@ ADD_DIGITS:
        printf '<td%s style="text-align:left">%s', $class && qq( class="$class"), $out;
 }
 
+showcolhead(@cols);
+
 for my $n (2 .. $count) {
+       print '<tbody>' if $n % 8 == 1;
        print '<tr>';
        print '<th>', $n;
        for my $radix (@cols) {
@@ -88,6 +121,7 @@ for my $n (2 .. $count) {
                Math::BigFloat->accuracy($accuracy);
                showfrac(scalar Math::BigFloat->new(1)->bdiv($n, $accuracy+1), $radix);
        }
+       say '';
 }
 
 :></table>
@@ -95,7 +129,7 @@ for my $n (2 .. $count) {
 <hr>
 
 <h2>Duplication (2ⁿ)</h2>
-<table>
+<table class=mapped>
 <:
 sub showint {
        my ($int, $radix) = @_;
@@ -109,16 +143,19 @@ sub showint {
 }
 
 @cols = grep { not $_ ~~ [2,8,16] } @cols, 36;
-print '<tr><th>';
-print '<th>', $_ for @cols;
+showcolhead(@cols);
 
-for my $n (3 .. 16, 18, 20, 24, 30, 32, 36, 40, 48, 50, 60, 64) {
+for my $n (0, 3 .. 16, 0, 18, 20, 24, 30, 32, 36, 40, 48, 50, 60, 64) {
+       if (!$n) {
+               print '<tbody>';
+               next;
+       }
        print '<tr>';
        print '<th>', $n;
        for my $radix (@cols) {
                print '<td style="text-align:right">', showint(2 ** $n, $radix);
        }
-       print '<th>', {
+       say '<th>', {
                 4 => 'nibble',
                 8 => 'octet',
                16 => '2o',
index 1c5bb8535cb9dfab8a1d9d95421c0a6a86f08e2e..6d75d4a35dc2023297707a09827bef847fff17aa 100644 (file)
@@ -4,7 +4,7 @@ no if $] >= 5.018, warnings => 'experimental::smartmatch';
 
 Html({
        title => 'browser compatibility cheat sheet',
-       version => '1.5',
+       version => '1.6',
        description => [
                "Compatibility table of new web features (HTML5, CSS3, SVG, Javascript)",
                "comparing support and usage share for all popular browser versions.",
@@ -20,14 +20,7 @@ Html({
 
 say "<h1>Browser compatibility</h1>\n";
 
-my $caniuse = do 'data/browser/support.inc.pl' or die $@ || $!;
-$_->{verrelease} = {
-       # mark last three (future) versions as unreleased, ensure current isn't
-       map {
-               $_->[-1] => 0, $_->[-2] => 0, $_->[-3] => 0,
-               $_->[-4] => undef,
-       } $_->{versions}
-} for values %{ $caniuse->{agents} };
+my $caniuse = Data('data/browser/support');
 
 my %CSTATS = (
        'n'   => 'l1',
@@ -36,11 +29,14 @@ my %CSTATS = (
        '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',
@@ -53,7 +49,7 @@ my %DSTATS = (
                join(' ',
                        'with prefix',
                        map {"-$_-"}
-                       ($caniuse->{agents}->{$_[0]}->{prefix_exceptions} // {})->{$_[1]}
+                       $caniuse->{agents}->{$_[0]}->{version_list}->{$_[1]}->{prefix}
                        // $caniuse->{agents}->{$_[0]}->{prefix} // (),
                );
        },
@@ -77,14 +73,14 @@ my %CSTATUS = (
 );
 my %versions;
 while (my ($browser, $row) = each %{ $caniuse->{agents} }) {
-       $versions{$browser} = [
-               sort { paddedver($a) cmp paddedver($b) } grep { defined }
-               @{ $row->{versions} }
-       ];
+       $versions{$browser} = [@{ $row->{versions} }];
 }
 
-print <<'';
-<p id="intro">Alternate rendition of Fyrd's <a href="http://caniuse.com/">when can I use...</a> page
+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;
@@ -98,15 +94,15 @@ given ($get{usage} // 'wm') {
                        'Identifier must be alphanumeric name or <q>0</q>.',
                ]);
        }
-       $canihas = do "data/browser/usage-$_.inc.pl" or do {
-               Alert('Browser usage data not found', $@ || $!);
+       $canihas = eval { Data("data/browser/usage-$_") } or do {
+               Alert('Browser usage data not found', $@);
                break;
        };
        $usage = $_;
        my $ref = $canihas->{-title} || 'unknown';
        $ref = showlink($ref, $_)
                for $canihas->{-site} || $canihas->{-source} || ();
-       $ref .= " $_" for $canihas->{-date} || ();
+       $ref =~ s/(?=>)/ title="updated $_"/ for $canihas->{-date} || ();
        print "\nwith $ref browser usage statistics";
 }
 
@@ -195,7 +191,10 @@ $canihas ||= {
 
 # score multiplier for percentage of all browser versions
 my $usagepct = 99.99 / sum(
-       map { $_->{-total} // values %{$_} } values %{$canihas}
+       map { $_->{-total} // values %{$_} }
+       map { $canihas->{$_} }
+       grep { !/^-/ }
+       keys %{$canihas}
 );
 
 $_->{usage} = featurescore($_->{stats}) * $usagepct
@@ -234,13 +233,15 @@ print "\n<tr>";
 for my $browser (@browsers) {
        for my $span (@{ $versions{$browser} }) {
                my $lastver = first {
-                       !defined $caniuse->{agents}->{$browser}->{verrelease}->{$_} # stable
+                       $caniuse->{agents}->{$browser}->{version_list}->{$_}->{release_date} # stable
                } reverse @{$span};
                printf('<td title="%s"%s>%s',
                        join(' ',
                                sprintf('%.1f%%', sum(@{ $canihas->{$browser} }{ @{$span} }) * $usagepct),
                                'version ' . showversions(@{$span}, undef),
-                               $span->[-1] eq $lastver ? () : '(development)',
+                               (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]),
@@ -270,9 +271,12 @@ sub featurescore {
                if ($canihas) {
                        while (my ($browser, $versions) = each %$row) {
                                ref $versions eq 'HASH' or next;
-                               while (my ($version, $status) = each %$versions) {
+                               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;
@@ -304,6 +308,7 @@ sub formatnotes {
                s/(?<= [^.\n]) $/./gmx;  # consistently end each line by a period
                Entity($_);
                s{  ` ([^`]*)  ` }{<code>$1</code>}gx;
+               s{ \(\K (?: \Qhttps://caniuse.com\E )? (?: /? \#feat= | / ) }{#}gx;
                s{ \[ ([^]]*) \] \( ([^)]*) \) }{<a href="$2">$1</a>}gx;
        }
        return @html;
@@ -372,9 +377,9 @@ sub saybrowsercols {
        my $data = $feature->{stats}->{$browser};
        if (ref $data eq 'ARRAY') {
                # special case for unsupported
-               my $release = $caniuse->{agents}->{$browser}->{verrelease};
                $data = {
-                       map { $_ => defined $release->{$_} ? 'u' : 'n' } keys %$release
+                       map { $_ => 'n' }
+                       keys %{ $caniuse->{agents}->{$browser}->{version_list} }
                };
        }
 
@@ -383,15 +388,15 @@ sub saybrowsercols {
                my $compare = (
                        !defined $ver ? undef :      # last column if nameless
                        ref $data ne 'HASH' ? '' :   # unclassified if no support hash
-                       $data->{ $ver->[-1] } // $prev  # known or inherit from predecessor
-                       // (grep { defined } @{$data}{ map { $_->[0] } @{ $versions{$browser} } })[0]
-                          ~~ 'n' && 'n'             # first known version is unsupported
+                       (first { defined } @{$data}{ reverse @{$ver} })  # last known version
+                       // $prev                     # inherit from predecessor
                        || 'u'                       # unsure
                );
-               unless (!defined $prev or $prev ~~ $compare) {
-                       my @vercover = (map { @{$_} } @span);
+               if (defined $prev and not $prev ~~ $compare) {
+                       # different columns
+                       my @vercover = (map { @{$_} } @span);  # accumulated conforming versions
                        for ($ver ? @{$ver} : ()) {
-                               $data->{$_} eq $data->{$vercover[-1]} or last;
+                               last if defined $data->{$_};  # until different
                                push @vercover, $_;  # matches from next span start
                        }
                        my $usage = sum(@{ $canihas->{$browser} }{@vercover});
@@ -413,6 +418,7 @@ sub saybrowsercols {
                        ));
                        $title .= "\n$_" for notestotitle(@notes);
 
+                       $prev .= ' #' if @notes and $prev =~ /^y/;
                        printf('<td class="%s" colspan="%d" title="%s">%s',
                                join(' ',
                                        X => $CSTATS{$prev},
@@ -423,12 +429,16 @@ sub saybrowsercols {
                                ),
                                scalar @span,
                                $title,
-                               showversions($span[0]->[0], @span > 1 ? $span[-1]->[-1] : ()),
+                               showversions($span[0]->[0], @span > 1 && defined $ver ? $span[-1]->[-1] : ()),
                        );
                        undef $prev;
                        @span = ();
                }
-               push @span, $ver && [ grep { $data->{ $_ } eq $data->{ $ver->[-1] } } @{$ver} ];
+               if ($ver) {
+                       my $startversion = first { defined $data->{ $ver->[$_] } }
+                               reverse 0 .. $#{$ver};  # compare index
+                       push @span, [ @{$ver}[ $startversion .. $#{$ver} ] ];
+               }
                $prev = $compare;
        }
 }
@@ -457,7 +467,7 @@ sub paddedver {
        # normalised version number comparable as string (cmp)
        $_[0] =~ m/(?:.*-|^)(\d*)(.*)/;
        # matched (major)(.minor) of last value in range (a-B)
-       return sprintf('%02d', length $1 ? $1 : 99) . $2;
+       return sprintf('%03d', length $1 ? $1 : 999) . $2;
 }
 
 sub showversions {
@@ -474,6 +484,7 @@ sub showversions {
 <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
index 04420cd123a3558e7170f43b6e120d40dd20f158..91df3b83d817fd0640bc2c407a7b336292a0ec69 100644 (file)
--- a/chars.plp
+++ b/chars.plp
@@ -2,7 +2,7 @@
 
 Html({
        title => 'character support sheet',
-       version => '1.1',
+       version => '1.2',
        keywords => [qw'
                unicode glyph char character reference common ipa symbol sign mark table digraph
        '],
@@ -22,7 +22,7 @@ EOT
 use Shiar_Sheet::FormatChar;
 my $glyphs = Shiar_Sheet::FormatChar->new;
 
-my $groupinfo = do 'data/unicode-cover.inc.pl' or die $@ || $!;
+my $groupinfo = Data('data/unicode-cover');
 
 my @ossel = @{ $groupinfo->{osdefault} };
 my @fontlist = map { $_->{file} }
@@ -30,11 +30,10 @@ my @fontlist = map { $_->{file} }
 
 my %font;
 for my $fontid (@fontlist) {
-               my ($fontmeta, @fontrange) = do "data/font/$fontid.inc.pl";
-               $fontmeta or next;
+               my $fontmeta = eval { Data("data/font/$fontid") } or next;
                $font{$fontid} = {
                        (map { (-$_ => $fontmeta->{$_}) } keys %{$fontmeta}),
-                       map { (chr $_ => 1) } @fontrange
+                       map { (chr $_ => 1) } @{ $fontmeta->{cover} }
                };
 }
 
@@ -62,8 +61,7 @@ my $query = eval {
 say "<h1>$title</h1>";
 
 if (!$query) {
-       Alert('Unicode group not specified', $@);
-       exit;
+       Abort(["Unicode group not found", $@], '404 no matches');
 };
 
 for ($parent || 'Unicode range') {
@@ -82,15 +80,16 @@ for ($parent || 'Unicode range') {
 my @chars;
 for (map { split /[^\d-]/ } $query) {
        my @range = split /-/, $_, 2;
-       m/^[0-9]+$/ or die "Invalid code point $_ in query $query\n" for @range;
+       m/^[0-9]+$/ or Abort("Invalid code point $_ in query $query", 400)
+               for @range;
        push @chars, chr $_ for $range[0] .. ($range[1] // $range[0]);
 }
 
-@chars or die "No match for query $query\n";
+@chars or Abort("No match for query $query", '404 no results');
 
-@chars <= 1500 or die sprintf(
-       'Too many matches (%d) for query %s'."\n",
-       scalar @chars, $query,
+@chars <= 1500 or Abort(
+       sprintf('Too many matches (%d) for query', scalar @chars),
+       '403 not allowed', $query
 );
 
 # output character list
index 92330ab0f876092ee425d0412a28d989d1e4ecaa..8e748ed3f5e25cfe89e14c10098b075af0914f35 100644 (file)
@@ -9,7 +9,7 @@ use utf8;
        ebcdic     => [qw( cp37 cp500 cp1047 posix-bc cp1026 cp875 )],
        iso        => [map {"iso-8859-$_"} 1 .. 11, 13 .. 16],
        dos        => [qw( cp437 cp865 cp861 cp860 cp863 cp850 cp857 cp852 cp775
-                          cp737 cp869 cp866 cp855 cp862 cp864 )],
+                          cp737 cp869 cp866 MIK cp855 cp862 cp864 )],
        aix        => [qw( cp1006 )],
        win        => [qw( cp1252 cp1250 cp1254 cp1257 cp1258 cp1253 cp1251 cp1255 cp1256 cp874 )],
        mac        => [qw( MacRoman MacRomanian MacRumanian MacCroatian MacCentralEurRoman MacTurkish MacIcelandic MacSami
@@ -22,7 +22,7 @@ use utf8;
        norteur    => [qw( baltic nordic )],
        baltic     => [qw( iso-8859-4 iso-8859-13 cp1257 cp775 )],
        nordic     => [qw( iso-8859-10 cp865 cp861 MacIcelandic MacSami )],
-       cyrillic   => [qw( koi8-r koi8-u koi8-f iso-8859-5 cp1251 MacCyrillic cp866 cp855
+       cyrillic   => [qw( koi8-r koi8-u koi8-f iso-8859-5 cp1251 MacCyrillic cp866 MIK cp855
                           +400 +2DE0 +A640-A69F +500-52F )], # MacUkrainian is broken
        arabic     => [qw( iso-8859-6 cp1256 MacArabic cp864 cp1006 MacFarsi
                           +600 +8A0-8BF+8E0 +750-77F )],
@@ -71,8 +71,9 @@ use utf8;
                },
        },
        'adobesymbol'  => {inherit => ['symbol' => '20-7F+A0', '' => '20-7F+A0']}, # minor differences, irrelevant except for different '€'
-       'wingdings'    => {inherit => ['' => '20'], setup => sub {
-               $_[0]->{table} = [(map {chr} 0 .. 0x20), qw(
+       'wingdings'    => {
+               inherit => ['' => '20'],
+               table => [(map {chr} 0 .. 0x20), qw(
                          🖉 ✂ ✁ 👓 🕭 🕮 🕯 🕿 ✆ 🖂 🖃 📪 📫 📬 📭 📁 📂 📄 🗏 🗐 🗄 ⌛ 🖮 🖰 🖲 🖳 🖴 🖫 🖬 ✇ ✍
                        🖎 ✌ 👌 👍 👎 ☜ ☞ ☝ ☟ 🖐 ☺ 😐 ☹ 💣 ☠ 🏳 🏱 ✈ ☼ 💧 ❄ 🕆 ✞ 🕈 ✠ ✡ ☪ ☯ ॐ ☸ ♈ ♉
                        ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ 🙰 🙵 ● 🔾 ■ □ 🞐 ❑ ❒ ⬧ ⧫ ◆ ❖ ⬥ ⌧ ⮹ ⌘ 🏵 🏶 🙶 🙷 \7f
@@ -80,10 +81,11 @@ use utf8;
                        ▪ ⚪ 🞆 🞈 ◉ ◎ 🔿 ▪ ◻ 🟂 ✦ ★ ✶ ✴ ✹ ✵ ⯐ ⌖ ⟡ ⌑ ⯑ ✪ ✰ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘
                        🕙 🕚 🕛 ⮰ ⮱ ⮲ ⮳ ⮴ ⮵ ⮶ ⮷ 🙪 🙫 🙕 🙔 🙗 🙖 🙐 🙑 🙒 🙓 ⌫ ⌦ ⮘ ⮚ ⮙ ⮛ ⮈ ⮊ ⮉ ⮋ 🡨
                        🡪 🡩 🡫 🡬 🡭 🡯 🡮 🡸 🡺 🡹 🡻 🡼 🡽 🡿 🡾 ⇦ ⇨ ⇧ ⇩ ⬄ ⇳ ⬀ ⬁ ⬃ ⬂ 🢬 🢭 🗶 ✔ 🗷 🗹 
-               )];
-       }},
-       'wingdings2'   => {inherit => ['' => '20'], setup => sub {
-               $_[0]->{table} = [(map {chr} 0 .. 0x20), qw(
+               )],
+       },
+       'wingdings2'   => {
+               inherit => ['' => '20'],
+               table => [(map {chr} 0 .. 0x20), qw(
                          🖊 🖋 🖌 🖍 ✄ ✀ 🕾 🕽 🗅 🗆 🗇 🗈 🗉 🗊 🗋 🗌 🗍 📋 🗑 🗔 🖵 🖶 🖷 🖸 🖭 🖯 🖱 🖒 🖓 🖘 🖙
                        🖚 🖛 👈 👉 🖜 🖝 🖞 🖟 🖠 🖡 👆 👇 🖢 🖣 🖑 🗴 ✓ 🗵 ☑ ☒ ☒ ⮾ ⮿ ⦸ ⦸ 🙱 🙴 🙲 🙳 ‽ 🙹 🙺
                        🙻 🙦 🙤 🙥 🙧 🙚 🙘 🙙 🙛 ⓪ ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⓿ ❶ ❷ ❸ ❹ ❺ ❻ ❼ ❽ ❾ ❿ \7f
@@ -91,10 +93,11 @@ use utf8;
                        ■ ◼ ⬛ ⬜ 🞑 🞒 🞓 🞔 ▣ 🞕 🞖 🞗 ⬩ ⬥ ◆ ◇ 🞚 ◈ 🞛 🞜 🞝 ⬪ ⬧ ⧫ ◊ 🞠 ◖ ◗ ⯊ ⯋ ◼ ⬥
                        ⬟ ⯂ ⬣ ⬢ ⯃ ⯄ 🞡 🞢 🞣 🞤 🞥 🞦 🞧 🞨 🞩 🞪 🞫 🞬 🞭 🞮 🞯 🞰 🞱 🞲 🞳 🞴 🞵 🞶 🞷 🞸 🞹 🞺
                        🞻 🞼 🞽 🞾 🞿 🟀 🟂 🟄 ✦ 🟉 ★ ✶ 🟋 ✷ 🟏 🟒 ✹ 🟃 🟇 ✯ 🟍 🟔 ⯌ ⯍ ※ ⁂
-               )];
-       }},
-       'wingdings3'   => {inherit => ['' => '20'], setup => sub {
-               $_[0]->{table} = [(map {chr} 0 .. 0x20), qw(
+               )],
+       },
+       'wingdings3'   => {
+               inherit => ['' => '20'],
+               table => [(map {chr} 0 .. 0x20), qw(
                          ⭠ ⭢ ⭡ ⭣ ⭦ ⭧ ⭩ ⭨ ⭰ ⭲ ⭱ ⭳ ⭶ ⭸ ⭻ ⭽ ⭤ ⭥ ⭪ ⭬ ⭫ ⭭ ⭍ ⮠ ⮡ ⮢ ⮣ ⮤ ⮥ ⮦ ⮧
                        ⮐ ⮑ ⮒ ⮓ ⮀ ⮃ ⭾ ⭿ ⮄ ⮆ ⮅ ⮇ ⮏ ⮍ ⮎ ⮌ ⭮ ⭯ ⎋ ⌤ ⌃ ⌥ ⎵ ⏡ ⇪ ⮸ 🢠 🢡 🢢 🢣 🢤 🢥
                        🢦 🢧 🢨 🢩 🢪 🢫 ← → ↑ ↓ ↖ ↗ ↙ ↘ 🡘 🡙 ▲ ▼ △ ▽ ◄ ► ◁ ▷ ◣ ◢ ◤ ◥ 🞀 🞂 🞁 \7f
@@ -102,10 +105,11 @@ use utf8;
                        🠇 🠈 🠊 🠉 🠋 🠠 🠢 🠤 🠦 🠨 🠨 🠪 🢜 🢝 🢞 🢟 🠮 🠰 🠲 🠴 🠶 🠸 🠺 🠹 🠻 🢘 🢚 🢙 🢛 🠼 🠾 🠽
                        🠿 🡀 🡂 🡁 🡃 🡄 🡆 🡅 🡇 ⮨ ⮩ ⮪ ⮫ ⮬ ⮭ ⮮ ⮯ 🡠 🡢 🡡 🡣 🡤 🡥 🡧 🡦 🡰 🡲 🡱 🡳 🡴 🡵 🡷
                        🡶 🢀 🢂 🢁 🢃 🢄 🢅 🢇 🢆 🢐 🢒 🢑 🢓 🢔 🢖 🢕 🢗
-               )];
-       }},
-       'webdings'     => {inherit => ['' => '20'], setup => sub {
-               $_[0]->{table} = [(map {chr} 0 .. 0x20), qw(
+               )],
+       },
+       'webdings'     => {
+               inherit => ['' => '20'],
+               table => [(map {chr} 0 .. 0x20), qw(
                          🕷 🕸 🕲 🕶 🏆 🎖 🖇 🗨 🗩 🗰 🗱 🌶 🎗 ▞ 🙼 🗕 🗖 🗗 ⏴ ⏵ ⏶ ⏷ ⏪ ⏩ ⏮ ⏭ ⏸ ⏹ ⏺ 🗚 🗳
                        🛠 🏗 🏘 🏙 🏚 🏜 🏭 🏛 🏠 🏖 🏝 🛣 🔍 🏔 👁 👂 🏞 🏕 🛤 🏟 🛳 🕬 🕫 🕨 🔈 🎔 🎕 🗬 🙽 🗭 🗪 🗫
                        ⮔ ✔ 🚲 □ 🛡 📦 🛱 ■ 🚑 🛈 🛩 🛰 🟈 🕴 ⚫ 🛥 🚔 🗘 🗙 ❓ 🛲 🚇 🚍 ⛳ 🛇 ⊖ 🚭 🗮 | 🗯 🗲 \7f
@@ -113,8 +117,8 @@ use utf8;
                        🕵 🕰 🖽 🖾 📋 🗒 🗓 📖 📚 🗞 🗟 🗃 🗂 🖼 🎭 🎜 🎘 🎙 🎧 💿 🎞 📷 🎟 🎬 📽 📹 📾 📻 🎚 🎛 📺 💻
                        🖥 🖦 🖧 🕹 🎮 🕻 🕼 📟 🖁 🖀 🖨 🖩 🖿 🖪 🗜 🔒 🔓 🗝 📥 📤 🕳 🌣 🌤 🌥 🌦 ☁ 🌧 🌨 🌩 🌪 🌬 🌫
                        🌜 🌡 🛋 🛏 🍽 🍸 🛎 🛍 Ⓟ ♿ 🛆 🖈 🎓 🗤 🗥 🗦 🗧 🛪 🐿 🐦 🐟 🐕 🐈 🙬 🙮 🙭 🙯 🗺 🌍 🌏 🌎 🕊
-               )];
-       }},
+               )],
+       },
 
        'iso-8859-2'   => {inherit => ['iso-8859-1' => 'A0']},
        'iso-8859-3'   => {inherit => ['iso-8859-1' => 'A0']}, #TODO: also apply to iso-8859-9
@@ -161,6 +165,19 @@ use utf8;
 
        'koi8-u'       => {inherit => ['koi8-r' => '90-BF']},
        'koi8-f'       => {inherit => ['koi8-u' => '90-BF']},
+       'mik'          => {
+               inherit => ['cp437' => '80-D8', 'cp866' => 'B0'],
+               table => [(map {chr} 0 .. 0x7F), qw(
+                       А Б В Г Д Е Ж З И Й К Л М Н О П
+                       Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я
+                       а б в г д е ж з и й к л м н о п
+                       р с т у ф х ц ч ш щ ъ ы ь э ю я
+                       └ ┴ ┬ ├ ─ ┼ ╣ ║ ╚ ╔ ╩ ╦ ╠ ═ ╬ ┐
+                       ░ ▒ ▓ │ ┤ № § ╗ ╝ ┘ ┌ █ ▄ ▌ ▐ ▀
+                       α ß Γ π Σ σ µ τ Φ Θ Ω δ ∞ φ ε ∩
+                       ≡ ± ≥ ≤ ⌠ ⌡ ÷ ≈ ° ∙ · √ ⁿ ² ■
+               ), "\xA0"],
+       },
 
        'macromanian'  => {inherit => ['MacRoman' => 'A0-BF+D0-DF']},
        'macrumanian'  => {inherit => ['MacRomanian' => 'A0-BF+D0-DF', 'MacRoman' => 'A0-BF+D0-DF']},
@@ -206,54 +223,114 @@ use utf8;
        'cp1026'       => {inherit => ['cp37' => '40']},
        'cp875'        => {inherit => ['cp37' => '30']},
 
-       ''             => {setup => sub {
-               my $row = shift;
-               $row->{offset} = delete $row->{startpoint};
-               $row->{set} = 'Unicode characters';
-               my $block = $row->{offset} >> 8;
-               $row->{endpoint} ||= ($block + 1 << 8) - 1;
-               $block == ($row->{endpoint} >> 8) or undef $block;
-
-               $row->{table} = join '', map { chr } $row->{offset} .. $row->{endpoint};
-               utf8::upgrade($row->{table});  # prevent latin1 output
-
-               $row->{endpoint} -= $row->{offset};
-
-               if (defined $block) {
-                       $row->{set} = sprintf 'Unicode block U+%02Xxx', $block;
-                       $row->{offset} %= 0x100;
-               }
-
-               return $row;
-       }},
-       u              => {setup => sub {
-               my $row = shift;
-               state $celldata = do 'charset-unicode.inc.pl'
-                       or Alert('Table data could not be read', $@ || $!);
-               $row->{cell} = $celldata;
-
-               $row->{endpoint} ||= 0x1FFF;
-               $row->{set} = 'Unicode ' . (
-                       $row->{startpoint} <  0x1000 && $row->{endpoint} < 0x1000 ? 'BMP' :
-                       $row->{startpoint} >= 0x1000 && $row->{endpoint} < 0x2000 ? 'SMP' :
-                       'allocations'
-               );
-               return $row;
-       }},
-       uu             => {setup => sub {
-               my $row = shift;
-               $row->{cell} = do 'charset-ucplanes.inc.pl'
-                       or Alert('Table data could not be read', $@ || $!);
-               $row->{endpoint} ||= 0x3FF;
-               $row->{set} = 'Unicode planes';
-               return $row;
-       }},
-       utf8           => {setup => sub {
-               my $row = shift;
-               $row->{set} = 'UTF-8';
-               $row->{cell} = do 'charset-utf8.inc.pl'
-                       or Alert('Table data could not be read', $@ || $!);
-               return $row;
-       }},
-       'utf-8'        => 'utf8',
+       legacy     => [qw( cp437 ATASCII PETSCII MSX ZX-Spectrum ANSEL )],
+       'petscii'      => {
+               inherit => ['' => '40-7F+A0-BF'],
+               table => [(map {chr} 0 .. 0x3F), qw(
+                       @ a b c d e f g h i j k l m n o p q r s t u v w x y z [ £ ] ↑ ←
+                       🭹 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z ┼ 🮌 │ 🮖 🮘
+                       . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
+                         ▌ ▄ ▔ ▁ ▏ ▒ ▕ 🮏 🮙 🮇 ├ ▗ └ ┐ ▂ ┌ ┴ ┬ ┤ ▎ ▍ 🮈 🮂 🮃 ▃ ✓ ▖ ▝ ┘ ▘ ▚
+               )],
+       },
+       'atascii'      => {
+               inherit => ['' => '0-1F+60-7F'],
+               table => [qw(
+                       ♥ ├ 🮇 ┘ ┤ ┐ ╱ ╲ ◢ ▗ ◣ ▝ ▘ 🮂 ▂ ▖ ♣ ┌ ─ ┼ • ▄ ▎ ┬ ┴ ▌ └ ␛ ↑ ↓ ← →
+                       _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+                       _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+                       ♦ a b c d e f g h i j k l m n o p q r s t u v w x y z ♠ | 🢰 ◀ ▶
+               )],
+       },
+       'zx-spectrum'  => {
+               inherit => ['' => '50-8F'],
+               set => 'ascii',
+               replace => {
+                       ord('^') => '↑',
+                       ord('`') => '£',
+                       0x7F => '© ▝▘▀▗▐▚▜▖▞▌▛▄▟▙█',
+               },
+       },
+       'msx'          => {
+               inherit => ['cp437' => '80-FF'],
+               table => [(map {chr} 0 .. 0x7F), qw(
+                       Ç ü é â ä à å ç ê ë è ï î ì Ä Å É æ Æ ô ö ò û ù ÿ Ö Ü ¢ £ ¥ ₧ ƒ
+                       á í ó ú ñ Ñ ª º ¿ ⌐ ¬ ½ ¼ ¡ « » Ã ã Ĩ ĩ Õ õ Ũ ũ IJ ij ¾ ∽ ◊ ‰ ¶ §
+                       ▂ ▚ ▆ 🮂 ▬ 🮅 ▎ ▞ ▊ 🮇 🮊 🮙 🮘 🭭 🭯 🭬 🭮 🮚 🮛 ▘ ▗ ▝ ▖ 🮖 Δ ‡ ω █ ▄ ▌ ▐ ▀
+                       α ß Γ π Σ σ µ τ Φ Θ Ω δ ∞ ⌀ ∈ ∩ ≡ ± ≥ ≤ ⌠ ⌡ ÷ ≈ ° ∙ · √ ⁿ ² ■
+               )],
+       },
+       'brascii'      => {
+               inherit => ['' => 'D0-DF+F0-FF'],
+               table => [(map {chr} 0 .. 0xFF)],
+               replace => {
+                       0xD7 => 'Œ',
+                       0xF7 => 'œ',
+               },
+       },
+       'ansel'        => {
+               note => '+GEDCOM',
+               inherit => ['' => 'A0-CF+E0-FE'],
+               table => [
+                               (undef) x 0xA0,
+                               undef, qw( Ł Ø Đ Þ Æ Œ ʹ · ♭ ®    ±          Ơ Ư ʾ ), undef,
+                               qw( ʿ      ł ø đ þ æ œ ʺ ı £ ð ), undef, qw( ơ ư ), undef, undef,
+                               qw( °      ℓ ℗ © ♯ ¿ ¡ ), (undef) x 0x19,
+                               (map {$_ && chr}
+                                       0x309, 0x300, 0x0301, 0x0302, 0x0303, 0x304, 0x306, 0x307,
+                                       0x308, 0x30C, 0x030A, 0xFE20, 0xFE21, 0x315, 0x30B, 0x310,
+                                       0x327, 0x328, 0x0323, 0x0324, 0x0325, 0x333, 0x332, 0x326,
+                                       0x31C, 0x32E, 0xFE22, 0xFE23, undef,  undef, 0x313, undef,
+                               ),
+               ],
+               replace => {
+                       # GEDCOM extensions
+                       0xBE => '□',
+                       0xBF => '■',
+                       0xCD => 'e', # endowment?
+                       0xCE => 'o', # ordinance?
+                       0xCF => 'ß',
+                       0xFC => "\x{338}",
+                       # MARC21 extensions
+                       0xC7 => 'ß',
+                       0xC8 => '€',
+               },
+       },
+       'ti86'         => {
+               note => 'similar to TI85',
+               inherit => ['', => '0-1F+80-EC'],
+               table => [
+                               undef, qw(
+                                       𝐛 𝐨 𝐝 𝐡 ▶ ⬆ ⬇ ∫ × 𝐀 𝐁 𝐂 𝐃 𝐄 𝐅
+                                       √ ⁻¹ ² ∠ ° ʳ ᵀ ≤ ≠ ≥ ⁻ ᴇ → ⏨ ↑ ↓
+                               ),
+                               (undef) x 0x60,
+                               qw(
+                                       ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ Á À Â Ä á à
+                                       â ä É È Ê Ë é è ê ë Í Ì Î Ï í ì
+                                       î ï Ó Ò Ô Ö ó ò ô ö Ú Ù Û Ü ú ù
+                                       û ü Ç ç Ñ ñ ´ ` ¨ ¿ ¡ α β γ Δ δ
+                                       ϵ θ λ μ π ρ Σ σ τ ϕ Ω x̅ y̅ ˟ … ◀
+                                       ■ ∕ ‐ ² ° ³ :⃞ ➧ ⧵ 🙽 ◥ ◣ ⊸ ∘ ⋱ █
+                                       ⇧ A⃞ a⃞ _ ⇧̲ A̲ a̲ ▒ ⬞ ˖ · ⁴ ﹦
+                               ),
+               ],
+       },
+       'ti89'         => {
+               note => 'also TI92(+)',
+               inherit => ['', => '0-1F+7F-BE'],
+               table => [
+                               qw(
+                                       ▒ ␁ ␂ ␃ ␄ ␅ ␆ 🔔 ⌫ ⇥ ), chr(0xA), qw( ⬏ ⤒ ↵ 🔒 ✓
+                                       ■ ◂ ▸ ▴ ▾ ← → ↑ ↓ ◀ ▶ ⬆ ∪ ∩ ⊂ ∈
+                               ),
+                               (map {chr} 0x20 .. 0x7E), '◆',
+                               qw(
+                                       α β Γ γ Δ δ ε ζ θ λ ξ ∏ π ρ ∑ σ
+                                       τ ϕ ψ Ω ω ᴇ ℯ 𝐢 ʳ ᵀ x̅ y̅ ≤ ≠ ≥ ∠
+                                       … ¡ ¢ £ ¤ ¥ ¦ § √ © ª « ¬ ⁻ ® ¯
+                                       ° ± ² ³ ⁻¹ µ ¶ · ⁺ ¹ º » 𝑑 ∫ ∞ ¿
+                               ),
+               ],
+       },
 };
diff --git a/charset.inc.pl b/charset.inc.pl
new file mode 100644 (file)
index 0000000..020cb2d
--- /dev/null
@@ -0,0 +1,59 @@
+use 5.014;
+use warnings;
+use utf8;
+
++{
+       %{ Data('./charset-encoding') },
+
+       ''             => {setup => sub {
+               my $row = shift;
+               $row->{offset} = delete $row->{startpoint};
+               $row->{set} = 'Unicode characters';
+               my $block = $row->{offset} >> 8;
+               $row->{endpoint} ||= ($block + 1 << 8) - 1;
+               $block == ($row->{endpoint} >> 8) or undef $block;
+
+               $row->{table} = join '', map { chr =~ s/\A\p{Unassigned}\z/�/r }
+                       $row->{offset} .. $row->{endpoint};
+               utf8::upgrade($row->{table});  # prevent latin1 output
+
+               $row->{endpoint} -= $row->{offset};
+
+               if (defined $block) {
+                       $row->{set} = sprintf 'Unicode block U+%02Xxx', $block;
+                       $row->{offset} %= 0x100;
+               }
+
+               return $row;
+       }},
+       u              => {setup => sub {
+               my $row = shift;
+               state $celldata = eval { Data('charset-unicode') }
+                       or Alert('Table data could not be read', ref $@ && $@->[1]);
+               $row->{cell} = $celldata;
+
+               $row->{endpoint} ||= 0x1FFF;
+               $row->{set} = 'Unicode ' . (
+                       $row->{startpoint} <  0x1000 && $row->{endpoint} < 0x1000 ? 'BMP' :
+                       $row->{startpoint} >= 0x1000 && $row->{endpoint} < 0x2000 ? 'SMP' :
+                       'allocations'
+               );
+               return $row;
+       }},
+       uu             => {setup => sub {
+               my $row = shift;
+               $row->{cell} = eval { Data('charset-ucplanes') }
+                       or Alert('Table data could not be read', ref $@ && $@->[1]);
+               $row->{endpoint} ||= 0x3FF;
+               $row->{set} = 'Unicode planes';
+               return $row;
+       }},
+       utf8           => {setup => sub {
+               my $row = shift;
+               $row->{set} = 'UTF-8';
+               $row->{cell} = eval { Data('charset-utf8') }
+                       or Alert('Table data could not be read', ref $@ && $@->[1]);
+               return $row;
+       }},
+       'utf-8'        => 'utf8',
+};
index e9f097d2b1f79c45ed2c9bfb0196bd52fadf181e..bbab1d6b83868e67e4619e94db05b88e67d8ce65 100644 (file)
@@ -5,7 +5,8 @@ my @tablist = split m{/+}, $Request || 'default';
 
 Html({
        title => 'charset cheat sheet',
-       version => '1.1',
+       version => '1.3',
+       canonical => "/charset/$Request" . ($mode && '?compare'),
        description => [
                "Reference sheet with all glyphs in common character encoding tables,",
                "and an overview of Unicode ranges and UTF-8 bytes.",
@@ -14,9 +15,9 @@ Html({
                charset codepage unicode ascii utf8 latin glyph character encoding
                reference common overview table
        '],
-       stylesheet => [qw'light'],
+       (stylesheet => [qw'light']) x !$mode,
        data => [qw(
-               charset-encoding.inc.pl
+               charset.inc.pl charset-encoding.inc.pl
                charset-unicode.inc.pl charset-ucplanes.inc.pl charset-utf8.inc.pl
        )],
 });
@@ -55,6 +56,7 @@ print join " •\n", (
                dos      => 'DOS',
                mac      => 'Apple',
                ebcdic   => 'EBCDIC',
+               legacy   => 'legacy',
                $tablist[0] eq 'default' ? () : ('' => 'common'),
        ],
        [
@@ -78,8 +80,7 @@ use Shiar_Sheet::FormatChar;
 my $glyphs = Shiar_Sheet::FormatChar->new;
 my @request;
 
-my $charsets = do 'charset-encoding.inc.pl'
-       or Alert('Encoding metadata could not be read', $@ || $!);
+my $charsets = Data('charset');
 
 sub tabinput {
        # generate character table(s)
@@ -97,6 +98,7 @@ sub tabinput {
 
        state $visible = {'' => 1};  # all present tables
        my %row = (offset => 0, cols => 16);
+       $row{$_} = $charset->{$_} for qw( note table );  # copy metadata
 
        if (not defined $params) {
                my @parents = @{ $charset->{inherit} || [] };
@@ -131,7 +133,7 @@ sub tabinput {
                                # extend earlier range
                                my $skip = int(($row{endpoint} || $row{startpoint}) / $row{cols});
                                for ($skip + 1 .. (hex($+{start}) / $row{cols}) - 1) {
-                                       $row{skip}->{ $_ * $row{cols} - $row{startpoint} }++;
+                                       $row{skip}->{ $_ * $row{cols} }++;
                                }
                        }
                        else {
@@ -153,7 +155,7 @@ sub tabinput {
        if (defined $row{table} or defined $row{cell}) {
                $row{set} //= $input;
        }
-       elsif ($row{set} = Encode::resolve_alias($input)) {
+       elsif ($row{set} = Encode::resolve_alias($charset->{set} // $input)) {
                $row{offset} = delete $row{startpoint};
                if ($charset->{varchar}) {
                        # array of possibly multiple characters per code point
@@ -169,6 +171,7 @@ sub tabinput {
 
                $row{endpoint} -= $row{offset};
                $visible->{ascii}++;  # assume common base
+               $row{set} = $input if $charset->{set};  # base override
        }
        else {
                Alert("Encoding <q>$input</q> unknown");
@@ -289,8 +292,8 @@ for my $row (@request) {
 
        printf '<div class="section"><table class="glyphs%s">', !$row->{cell} && ' charmap';
        my $title = $row->{set};
-       $title .= " <aside>(over $_)</aside>"
-               for $row->{parent} || ();
+       $title .= " <aside>(over $_)</aside>" for $row->{parent} || ();
+       $title .= " <aside>($_)</aside>" for $row->{note} || ();
        printf '<caption>%s</caption>', $title;
        print '<col>' x ($cols + 1);
        for my $section (qw{thead}) {
@@ -301,13 +304,13 @@ for my $row (@request) {
 
        print '<tbody>';
        while ($offset <= $row->{endpoint} * $colsize) {
-               if ($row->{skip}->{$offset}) {
+               if ($row->{skip}->{$offset + $row->{offset}}) {
                        $offset += $cols * $colsize;
                        next;
                }
 
                print '<tr><th>';
-               if (defined $row->{skip}->{$offset}) {
+               if (defined $row->{skip}->{$offset + $row->{offset}}) {
                        print '⋮';
                }
                else {
@@ -340,15 +343,15 @@ for my $row (@request) {
                                        $cp == ord $glyph ? 'l4' :
                                        $row->{parent} && $glyph eq
                                                Encode::decode($row->{parent}, pack 'C', $cp) ? 'l3' :
-                                       !$class ? undef :
+                                       !defined $cell ? undef :
                                        $visible->{$glyph} ? 'l2' :
                                        'l1'
                                );
                                $visible->{$glyph}++;
                        }
 
-                       say sprintf $class ? '<td title="%s" class="X %s">%s' : '<td title="%s">',
-                               $name, $class, $cell;
+                       printf '<td title="%s"', $name;
+                       say $class ? sprintf(' class="X %s">%s', $class, $cell) : '>';
                }
                continue {
                        $offset += $colsize;
index aed61fcaed6f5eafbc58ccfc910d392c6e17cfae..58445298f426beb736ea31996bdfb5d3dd4cbeaa 100644 (file)
@@ -1,4 +1,4 @@
-@import url(light.css?1.10);
+@import url(light.css?1.11);
 
 .l1       {background: #F77}
 .l1:hover a, .l1:hover a:visited,
diff --git a/cli.plp b/cli.plp
new file mode 100644 (file)
index 0000000..96cd427
--- /dev/null
+++ b/cli.plp
@@ -0,0 +1,60 @@
+<(common.inc.plp)><:
+
+Html({
+       title => 'cli cheat sheet',
+       version => '1.0',
+       description => [],
+       keywords => [qw'
+       '],
+       data => ['data/cli.inc.pl'],
+});
+
+my $cmd = Data('data/cli');
+:>
+<h1>CLI options</h1>
+
+<style>
+       tbody td {font-size:70%}
+</style>
+<:
+sub showoption {
+       my ($info, $char, $span) = @_;
+       my ($alias, $help) = @{ $info // [] };
+       printf '<td class="l%s"', $info ? 5 : 1;
+       print ' rowspan="2"' if $span;
+       print '>';
+       $info or return;
+       my ($title) = $alias =~ m{--([\w-]+=?)}
+               or return print $char;
+       $title =~ s/-\K/<wbr>/g;
+       $title =~ s/deref\Kerence/./;
+       print $title;
+}
+
+my @colchars = ('a'..'z', '?');
+print '<table class="ccmap"><col>';
+print qq'<colgroup span="$_">' for scalar @colchars;
+#say '</colgroup><col>';
+for my $section (qw{thead tfoot}) {
+       print "<$section><tr><th>↳";
+       print '<th>', EscapeHTML($_) for @colchars;
+       say '';
+}
+print '<tbody>';
+for my $name (sort keys %$cmd) {
+       my $row = $cmd->{$name};
+       print '<tr><th rowspan="2">', $name;
+       showoption($row->{$_}, $_, !$row->{uc $_}) for @colchars;
+       print "\n\t<tr>";
+       $row->{$_} and showoption($row->{$_}, $_) for map {uc} @colchars;
+}
+say '</table>';
+:>
+
+<div class="legend">
+       <table class="glyphs"><tr>
+       <td class="X l5">supported
+       <td class="X l1">unsupported
+       <td class="X l0 ex">alias
+       </table>
+</div>
diff --git a/codec-audio.inc.pl b/codec-audio.inc.pl
new file mode 100644 (file)
index 0000000..330f3b5
--- /dev/null
@@ -0,0 +1,142 @@
+use utf8;
++{
+intro => 'Comparison of audio compression formats.',
+keywords => [qw( audio sound codec encoder encoding decode compression file format type mime )],
+codec => {
+       mp3 => {
+               name => '<abbr title="MPEG-1 Audio Layer III">MP3</abbr>',
+               available => 1991,
+               generation => 0,
+       },
+       vorbis => {
+               name => 'Vorbis',
+               available => 2000,
+               generation => 1,
+       },
+       opus => {
+               name => 'Opus',
+               available => 2012,
+               generation => 1,
+       },
+       aac => {
+               name => '<abbr title="Advanced Audio Coding">AAC</abbr>',
+               available => 1997,
+               generation => 1,
+       },
+       atrac => {
+               name => '<abbr title="Adaptive Transform Acoustic Coding">ATRAC</abbr>',
+               available => 1992,
+               generation => 0,
+       },
+       mpc => {
+               name => 'Musepack',
+               abbr => 'MPC',
+               mime => 'audio/musepack',
+               available => 1997,
+               generation => 1,
+       },
+},
+feature => {
+       default => {
+               children => [qw( quality limits royalties support )],
+       },
+       quality => {
+               name => 'compression quality',
+               children => [qw( quality_music quality_speech quality_ll )],
+               score => {
+               },
+       },
+       quality_music => {
+               name => 'music',
+               score => {
+                       mp3  => 3,
+                       atrac => 2,
+                       vorbis => 4,
+                       aac  => 5,
+                       opus => 5,
+               },
+       },
+       quality_speech => {
+               name => 'speech',
+               score => {
+                       mp3  => 3,
+               },
+       },
+       quality_ll => {
+               name => 'lossless',
+               score => {
+                       mp3  => 'n',
+                       vorbis => 'n',
+                       opus => 'n',
+                       flac => 4,
+               },
+       },
+       limits => {
+               children => [qw( channels bitrate latency peeling )],
+               score => {
+               },
+       },
+       channels => {
+               score => {
+                       mp3  => [3, 6, 'stereo, extended to upto 5.1'],
+                       vorbis => [4, 255],
+                       opus => [4, 255],
+               },
+       },
+       peeling => {
+               name => 'bitrate peeling',
+               score => {
+                       vorbis => [3, undef, 'yes but "unusable" quality'],
+                       mp3  => 'n',
+                       opus => 'n',
+                       aac  => 'n',
+               },
+       },
+       latency => {
+               name => 'frame size (ms)',
+               score => {
+                       mp3 => [3, 26, 'typical version and layer has 1152 samples at 44kHz'],
+                       opus => [5, 2.5],
+               },
+       },
+       bitrate => {
+               name => 'minimal bitrate',
+               score => {
+                       mp3 => [4, 8],
+                       opus => [4, 6],
+                       mpc => [5, 0],
+               },
+       },
+       steaming => {
+               name => 'Streamable',
+               score => {
+                       mpc => 'y',
+               },
+       },
+       seeking => {
+               name => 'Fast seeking',
+               score => {
+                       mpc => [5, undef, 'indexed'],
+                       opus => [3, undef, 'bisection seeking (usually 1 physical seek required if implemented correctly)'],
+               },
+       },
+       royalties => {
+               score => {
+                       mp3  => [5, undef, 'expired'],
+                       vorbis => 5,
+                       opus => [4, undef, 'open and free design'],
+                       atrac => [2, undef, 'proprietary, but the original patents have expired'],
+                       mpc => [5, undef, 'open format, bsd licensed implementation'],
+               },
+       },
+       support => {
+               score => {
+                       mp3  => 5,
+                       vorbis => 4,
+                       aac  => 4,
+                       opus => 4,
+                       atrac => 1,
+               },
+       },
+},
+}
diff --git a/codec-image.inc.pl b/codec-image.inc.pl
new file mode 100644 (file)
index 0000000..6c138ab
--- /dev/null
@@ -0,0 +1,561 @@
+use utf8;
++{
+intro => 'Comparison of image encoding formats, based on <a href="https://cloudinary.com/blog/one_pixel_is_worth_three_thousand_words">Cloudinary</a> research.',
+keywords => [qw( image picture codec encoder encoding decode compression file format type mime )],
+codec => {
+       jpeg => {
+               name => '<abbr title="Joint Photographic Experts Group">JPEG</abbr>',
+               available => 1992,
+               generation => 0,
+       },
+       gif => {
+               name => '<abbr title="Graphics Interchange Format">GIF</abbr>',
+               available => 1987,
+               generation => 0,
+       },
+       png => {
+               name => '<abbr title="Portable Network Graphics">PNG</abbr>',
+               available => 1996,
+               generation => 1,
+       },
+       jp2k => {
+               name => 'JPEG 2000',
+               available => 2000,
+               generation => 1,
+       },
+       webp => {
+               name => 'WebP',
+               available => 2010,
+               generation => 1,
+       },
+       heic => {
+               name => '<abbr title="High Efficiency Image Container (HEVC in HEIF)">HEIC</abbr>',
+               available => 2015,
+               generation => 2,
+       },
+       avif => {
+               name => '<abbr title="AV1 Image File Format">AVIF</abbr>',
+               available => 2019,
+               generation => 2,
+       },
+       jxl => {
+               name => 'JPEG XL',
+               available => 2021,
+               generation => 2,
+       },
+       pnm => {
+               name => '<abbr title="Portable aNyMap">PNM</abbr>',
+               available => 1988,
+               generation => 0,
+       },
+},
+feature => {
+       default => {
+               children => [qw( quality_photo quality_art speed limits features royalties overhead support web )],
+       },
+       quality_photo => {
+               name => 'compression (photo)',
+               score => {
+                       jpeg => 3,
+                       pnm  => 'n',
+                       gif  => 1,
+                       png  => 1,
+                       jp2k => 4,
+                       webp => 3,
+                       heic => 5,
+                       avif => 5,
+                       jxl  => 5,
+               },
+               children => [qw( quality_photo_ll quality_photo_3 quality_photo_2 quality_photo_1 quality_thumbs )],
+       },
+       quality_thumbs => {
+               parent => 'quality_photo',
+               name => 'thumbnails',
+               score => {
+                       jpeg => 1,
+                       pnm  => 1,
+                       gif  => 2,
+                       png  => 3,
+                       jp2k => 1,
+                       webp => 2,
+                       heic => 4,
+                       avif => 4,
+                       jxl  => 3,
+               },
+       },
+       quality_photo_1 => {
+               parent => 'quality_photo',
+               name => 'low fidelity',
+               score => {
+                       jpeg => 2,
+                       pnm  => 1,
+                       gif  => 1,
+                       png  => 1,
+                       jp2k => 3,
+                       webp => 4,
+                       heic => 5,
+                       avif => 5,
+                       jxl  => 3,
+               },
+       },
+       quality_photo_2 => {
+               parent => 'quality_photo',
+               name => 'medium fidelity',
+               score => {
+                       jpeg => 3,
+                       pnm  => 1,
+                       gif  => 1,
+                       png  => 1,
+                       jp2k => 4,
+                       webp => 3,
+                       heic => 4,
+                       avif => 5,
+                       jxl  => 5,
+               },
+       },
+       quality_photo_3 => {
+               parent => 'quality_photo',
+               name => 'high fidelity',
+               score => {
+                       jpeg => 3,
+                       pnm  => 1,
+                       gif  => 1,
+                       png  => 2,
+                       jp2k => 4,
+                       webp => 2,
+                       heic => 3,
+                       avif => 4,
+                       jxl  => 5,
+               },
+       },
+       quality_photo_ll => {
+               parent => 'quality_photo',
+               name => 'lossless',
+               score => {
+                       jpeg => 1,
+                       pnm  => 1,
+                       gif  => 1,
+                       png  => 2,
+                       jp2k => 4,
+                       webp => 3,
+                       heic => 3,
+                       avif => 2,
+                       jxl  => 5,
+               },
+       },
+       quality_art => {
+               name => 'compression (other images)',
+               score => {
+                       jpeg => 2,
+                       pnm  => 'n',
+                       gif  => 1,
+                       png  => 3,
+                       jp2k => 2,
+                       webp => 4,
+                       heic => 3,
+                       avif => 4.5,
+                       jxl  => 5,
+               },
+               children => [qw( quality_art_2 quality_art_ll quality_art_mixed )],
+       },
+       quality_art_2 => {
+               name => 'lossy non-photographic',
+               score => {
+                       jpeg => 2,
+                       pnm  => 1,
+                       gif  => 2,
+                       png  => 3,
+                       jp2k => 2,
+                       webp => 4,
+                       heic => 3,
+                       avif => 5,
+                       jxl  => 5,
+               },
+       },
+       quality_art_ll => {
+               name => 'lossless non-photographic',
+               score => {
+                       jpeg => 1,
+                       pnm  => 1,
+                       gif  => 1,
+                       png  => 4,
+                       jp2k => 2,
+                       webp => 5,
+                       heic => 2,
+                       avif => 3,
+                       jxl  => 5,
+               },
+       },
+       quality_art_mixed => {
+               name => 'mixed photo/nonphoto',
+               score => {
+                       jpeg => 2,
+                       pnm  => 1,
+                       gif  => 1,
+                       png  => 2,
+                       jp2k => 2,
+                       webp => 3,
+                       heic => 3,
+                       avif => 5,
+                       jxl  => 5,
+               },
+       },
+       speed => {
+               score => {
+                       jpeg => 5,
+                       pnm  => 5,
+                       gif  => 4,
+                       png  => 4,
+                       jp2k => 3,
+                       webp => 4,
+                       heic => 3,
+                       avif => 3,
+                       jxl  => 5,
+               },
+               children => [qw( speed_encode speed_decode speed_parallel )],
+       },
+       speed_encode => {
+               parent => 'speed',
+               name => 'single-core encode',
+               score => {
+                       jpeg => 5,
+                       pnm  => 0,
+                       gif  => 3, # palette conversion
+                       png  => 3,
+                       jp2k => 4,
+                       webp => 4,
+                       heic => 3,
+                       avif => 2,
+                       jxl  => 5,
+               },
+       },
+       speed_decode => {
+               parent => 'speed',
+               name => 'single-core decode',
+               score => {
+                       jpeg => 5,
+                       pnm  => 0,
+                       gif  => 5,
+                       png  => 5,
+                       jp2k => 4,
+                       webp => 5,
+                       heic => 3,
+                       avif => 3,
+                       jxl  => 5,
+               },
+       },
+       speed_parallel => {
+               parent => 'speed',
+               name => 'pararellizable',
+               score => {
+                       jpeg => 2,
+                       pnm  => 0,
+                       gif  => 2,
+                       png  => 2,
+                       jp2k => 4,
+                       webp => 2,
+                       heic => 4,
+                       avif => 4,
+                       jxl  => 5,
+               },
+       },
+       limits => {
+               score => {
+                       jpeg => 3,
+                       pnm  => 3,
+                       gif  => 2,
+                       png  => 4,
+                       jp2k => 5,
+                       webp => 2,
+                       heic => 4,
+                       avif => 4.5,
+                       jxl  => 5,
+               },
+               children => [qw( max_dimensions max_bitdepth color_444 hdr max_channels  )],
+       },
+       max_dimensions => {
+               parent => 'limits',
+               name => 'maximum image dimensions',
+               score => {
+                       jpeg => [3, '65k²'],   # 2**16
+                       pnm  => [5,    '∞'],
+                       gif  => [3, '65k²'],   # 2**16
+                       png  => [4,  '2G²'],   # 2**31
+                       jp2k => [4,  '4G²'],   # 2**32
+                       webp => [1, '16k²'],   # 2**14
+                       heic => [2,'8k×4k+', 'tilable, only 512×512 on Apple'], # 8193x4320
+                       avif => [3, '65k²+', 'tilable, 7680×4320 with Advanced profile'], # 2**16
+                       jxl  => [4,  '1G²'],   # 2**30
+               },
+       },
+       max_bitdepth => {
+               parent => 'limits',
+               name => 'precision (max. bit depth)',
+               score => {
+                       jpeg => [2,  8],
+                       pnm  => [2,  8, 'unofficial PFM extension for 32-bit'],
+                       gif  => [1,  8, '256 colour palette per frame'],
+                       png  => [4, 16],
+                       jp2k => [5, 38],
+                       webp => [2,  8],
+                       heic => [3, 10], #TODO 16?
+                       avif => [3, 12, '8, 10, 12 bit'],
+                       jxl  => [5, 32, '24-bit integer or 32-bit float'],
+               },
+       },
+       color_444 => {
+               parent => 'limits',
+               name => 'chroma subsampling',
+               score => {
+                       jpeg => ['y', undef, '4:2:0, 4:2:2, 4:4:4'],
+                       pnm  => [4, '✘'],
+                       gif  => [4, '✘'],
+                       png  => [4, '✘'],
+                       jp2k => 'y',
+                       webp => [1, '4:2:0'],
+                       heic => [1, '4:2:0'],
+                       avif => ['y', undef, '4:2:0, 4:2:2, 4:4:4'],
+                       jxl  => ['y', undef, 'for JPEG compatibility'],
+               },
+       },
+       hdr => {
+               parent => 'limits',
+               name => 'wide gamut/HDR',
+               score => {
+                       jpeg => 'n',
+                       pnm  => 'n',
+                       gif  => 'n',
+                       png  => 'y',
+                       jp2k => 'y',
+                       webp => 'n',
+                       heic => 'y',
+                       avif => 'y',
+                       jxl  => 'y',
+               },
+       },
+       max_channels => {
+               parent => 'limits',
+               name => 'maximum number of channels',
+               score => {
+                       jpeg => [3, 4, 'RGB or CMYK'],
+                       pnm  => [3, 3, 'RGB'],
+                       gif  => [3, 3, 'RGB palette'],
+                       png  => [3, 4, 'RGBA'],
+                       jp2k => [5, 2**15],
+                       webp => [3, 4, 'RGBA'],
+                       heic => [3, 3, 'RGB, separate alpha and depth'],
+                       avif => [3, 3, 'RGB, separate alpha and depth'],
+                       jxl  => [4, 4099, 'native XYB'],
+               },
+       },
+       features => {
+               score => {
+                       jpeg => 2,
+                       pnm  => [2, undef, 'great for simplicity and ASCII storage'],
+                       gif  => 2,
+                       png  => 3,
+                       jp2k => 4,
+                       webp => 2,
+                       heic => 4,
+                       avif => 4,
+                       jxl  => 5,
+               },
+               children => [qw( animation progressive alpha depthmap overlays vector authoring reencode compat_jpeg )],
+       },
+       animation => {
+               parent => 'features',
+               name => 'supports animation',
+               score => {
+                       jpeg => [2, 'MJPEG'],
+                       pnm  => 'n',
+                       gif  => 'y',
+                       png  => [4, 'APNG', 'later backwards-compatible extension'],
+                       jp2k => [2, 'MJP2'],
+                       webp => 'y',
+                       heic => 'y',
+                       avif => 'y',
+                       jxl  => 'y',
+               },
+       },
+       progressive => {
+               parent => 'features',
+               name => 'progressive decoding',
+               score => {
+                       jpeg => 4,
+                       pnm  => 'n',
+                       gif  => 2,
+                       png  => 2,
+                       jp2k => 5,
+                       webp => 'n',
+                       heic => 'n',
+                       avif => 'n',
+                       jxl  => 5,
+               },
+       },
+       vector => {
+               parent => 'features',
+               name => 'vector drawing',
+               score => {
+                       jpeg => 'n',
+                       pnm => 'n',
+                       gif => 'n',
+                       png => 'n',
+                       jp2k => 'n',
+                       webp => 'n',
+                       heic => 'n',
+                       avif => 'n',
+                       jxl => [2, undef, 'splines'],
+               },
+       },
+       alpha => {
+               parent => 'features',
+               name => 'alpha transparency',
+               score => {
+                       jpeg => 'n',
+                       pnm  => ['n', undef, 'PAM extension'],
+                       gif  => [3, '1 bit'],
+                       png  => 'y',
+                       jp2k => 'y',
+                       webp => 'y',
+                       heic => 'y',
+                       avif => 'y',
+                       jxl  => 'y',
+               },
+       },
+       depthmap => {
+               parent => 'features',
+               name => 'depth map',
+               score => {
+                       jpeg => 'n',
+                       pnm  => 'n',
+                       gif  => 'n',
+                       png  => 'n',
+                       jp2k => 'n',
+                       webp => 'n',
+                       heic => 'y',
+                       avif => 'y',
+                       jxl  => 'y',
+               },
+       },
+       overlays => {
+               parent => 'features',
+               name => 'overlays (layers)',
+               score => {
+                       jpeg => 'n',
+                       pnm  => 'n',
+                       gif  => 'y',
+                       png  => 'n',
+                       jp2k => 'n',
+                       webp => 'n',
+                       heic => 'y',
+                       avif => 'y',
+                       jxl  => 'y',
+               },
+       },
+       authoring => {
+               parent => 'features',
+               name => 'authoring workflow suitability',
+               score => {
+                       jpeg => 2,
+                       pnm  => 2,
+                       gif  => 2,
+                       png  => 3,
+                       jp2k => 3,
+                       webp => 2,
+                       heic => 2,
+                       avif => 2,
+                       jxl  => 5,
+               },
+       },
+       reencode => {
+               parent => 'features',
+               name => 'generation loss resilience',
+               score => {
+                       jpeg => 4,
+                       pnm  => 0,
+                       png  => 0,
+                       gif  => 0,
+                       jp2k => 3,
+                       webp => 2,
+                       heic => 3,
+                       avif => 3,
+                       jxl  => 4,
+               },
+       },
+       compat_jpeg => {
+               parent => 'features',
+               name => 'lossless JPEG recompression',
+               score => {
+                       jpeg => 0,
+                       pnm  => 'n',
+                       gif  => 'n',
+                       png  => 'n',
+                       jp2k => 'n',
+                       webp => 'n',
+                       heic => 'n',
+                       avif => 'n',
+                       jxl  => 'y',
+               },
+       },
+       royalties => {
+               name => 'royalty-free',
+               score => {
+                       jpeg => 5,
+                       pnm  => 5,
+                       gif  => [5, undef, 'patented before 2003'],
+                       png  => 5,
+                       jp2k => [3, undef, 'ISO specification not freely available'],
+                       webp => [4, undef, 'free format, low remaining risk of patent trolls'],
+                       heic => ['n', undef, 'heavily patented'],
+                       avif => [4, undef, 'free format, risk of patent trolls'],
+                       jxl  => [4, undef, 'free format, risk of patent trolls'],
+               },
+               children => [],
+       },
+       web => {
+               name => 'browser support',
+               score => {
+                       jpeg => [5, undef, 'ubiquitous since first inline images (1993)'],
+                       pnm  => [1, undef, 'rarely by (unix) systems'],
+                       xbm  => [2, undef, 'common before 200X'],
+                       gif  => [5, undef, 'predates the web, but unrestricted since 2004'],
+                       png  => [5, undef, 'problematic prior to IE7, currently ubiquitous'],
+                       jp2k => [2, undef, 'just Safari'],
+                       webp => [4, undef, 'widespread since 2020'],
+                       heic => [1, undef, 'no browser support'],
+                       avif => [3, undef, 'significant since 2021 (Safari 2023, no Edge yet)'],
+                       jxl  => [2, undef, 'experimental, blocked by chrome'],
+               },
+       },
+       support => {
+               name => 'adoption',
+               score => {
+                       jpeg => [5, undef, 'standard photography'],
+                       pnm  => [2, undef, 'uncomplicated bitmap interchange'],
+                       gif  => [4, undef, 'declining due to limitations'],
+                       png  => [5, undef, 'standard illustrations'],
+                       jp2k => [3, undef, 'limited beyond Apple'],
+                       webp => [4, undef, 'mostly online'],
+                       heic => [2, undef, 'stored by latest cameras, interchange unlikely'],
+                       avif => [3, undef, 'upcoming'],
+                       jxl  => [2, undef, 'ongoing'],
+               },
+       },
+       overhead => {
+               name => 'container overhead (file size)',
+               score => {
+                       png  => [3,  67, 'upto 70 bytes for specific RGBA'],
+                       jpeg => [2, 160, '159 bytes minimum for gray, 288 for specific colours'],
+                       gif  => [4,  35, '43 bytes for transparent'],
+                       webp => [4,  34, 'black or transparent lossless; 44-92 bytes lossy'],
+                       bpg  => [4,  31, 'lossy 29-62 bytes, lossless 37-160'],
+                       flif => [5,  14, 'black or transparent; 20 bytes for specific RGBA'],
+                       pnm  => [5,   8, 'monochrome text PBM; 12 bytes PPM; 69 bytes PAM'],
+                       jxl  => [5,  12, '512×256 black pixels'],
+                       avif => [1, 282, 'container overhead; 457 bytes with alpha'],
+                       jp2k => [2, 123, 'experimental results, likely not optimal'],
+                       heic => [1, 386],
+               },
+       },
+},
+}
diff --git a/codec.plp b/codec.plp
new file mode 100644 (file)
index 0000000..f13b93a
--- /dev/null
+++ b/codec.plp
@@ -0,0 +1,76 @@
+<(common.inc.plp)><:
+
+my ($page, @feat) = split m{/+}, $Request || 'image';
+$page !~ /\W/ or Html(), Abort('Invalid codec type request', 400);
+@feat or @feat = 'default';
+my $title = "$page codecs";
+my $info = eval { Data("codec-$page") };
+if ($@) {
+       $info = {};
+}
+
+Html({
+       title => "$title cheat sheet",
+       version => '1.1',
+       description => $info->{intro},
+       keywords => [@{ $info->{keywords} // [] }, qw' feature comparison support benchmark '],
+       stylesheet => [qw'light circus dark red'],
+       data => ["codec-$page.inc.pl"],
+       raw => '<style>td,th {width:8%} tbody th {white-space:nowrap}</style>',
+});
+
+%{$info}
+       or Abort("Requested codec type <q>$page</q> not available", '404 request not found');
+
+say "<h1>\u$title</h1>";
+say "<p>$_</p>" for $info->{intro} // ();
+
+my %BOOLSCORE = (y => [5, '✔'], n => [1, '✘'], 0 => [0, 'n/a']);
+:>
+
+<div class="section">
+<table class="mapped">
+<:
+my @codecs = sort {
+       $info->{codec}->{$a}->{available} <=> $info->{codec}->{$b}->{available}
+} keys $info->{codec}->%*;
+my @codeccols = @{$info->{codec}}{@codecs};
+
+{
+       print '<col>';
+       my @spans;
+       $spans[ $_->{generation} ]++ for @codeccols;
+       print "<colgroup span=$_>" for @spans;
+}
+say '<thead><tr><th rowspan=2>';
+say "\t", '<th>', $_->{name} for @codeccols;
+print '<tr>';
+print '<td>', $_->{available} for @codeccols;
+say '</thead>';
+
+while (defined (my $feat = shift @feat)) {
+       my $featinfo = $info->{feature}->{$feat} or next;
+       unshift @feat, @{$_} for $featinfo->{children} // ();
+       $featinfo->{score} or next;
+       print '<tbody>' if $featinfo->{children};
+       printf '<tr><th>%s', $featinfo->{name} // $feat;
+       for (@codecs) {
+               my ($score, $data, $title) = map { ref ? @$_ : $_ } $featinfo->{score}->{$_};
+               if (not defined $data) {
+                       if (my $override = $BOOLSCORE{$score}) {
+                               ($score, $data) = @{$override};
+                       }
+                       else {
+                               $data = '•' x ($score - 1);
+                       }
+               }
+               printf '<td class="l%d"', $score;
+               printf ' title="%s"', EscapeHTML($_) for $title // ();
+               print '>', $data;
+       }
+       say '</td>';
+}
+
+:></table>
+</div>
+
index 68ada33956a5a1dce7116da4d6181323ab8d4e88..a634733cba704de18de8e56605a78a796f8109c9 100644 (file)
@@ -1,5 +1,6 @@
 <:
 use 5.014;
+use strict;
 use utf8;
 use warnings;
 no  warnings 'qw';  # you know what you doing
@@ -10,42 +11,45 @@ use File::stat 'stat';
 use HTTP::Date;
 use Encode qw( decode_utf8 );
 
+our $Dev;
+
 sub Alert {
        my ($html, $debug) = @_;
        ref $html eq 'ARRAY' or $html = [$html];
        my ($title, @lines) = @{$html};
-       $body = "<h2>$title</h2>";
+       my $body = "<h2>$title</h2>";
        $body .= "\n<p>$_</p>" for @lines;
        $body .= "\n<pre>$debug</pre>" if $Dev and $debug;
        say "<div class=error>$body</div>\n";
 }
 
+sub Abort {
+       my ($html, $code, $debug) = @_;
+       unless ($PLP::sentheaders) {
+               $header{Status} = $code || 500;
+       }
+       elsif ($Dev) {
+               ref $html eq 'ARRAY' or $html = [$html];
+               push @{$html}, "Also failed to set HTTP status <q>$code</q>"
+                       . " after output!";
+       }
+       Alert($html, $debug);
+       exit;
+}
+
 BEGIN {
        require Time::HiRes;
        our $Time = [Time::HiRes::gettimeofday()];
 
-       $PLP::ERROR = sub {
-               my ($text, $html) = @_;
-               warn $text;
-               unless ($PLP::sentheaders and $PLP::sentheaders->[0] !~ m{/PLP\.pm$}) {
-                       Html({nocache => 1});
-                       say '<h1>Page unavailable</h1>';
-               }
-               Alert("<strong>Fatal error</strong>: $html.");
-       };
-
        push @INC, '.';
 
        # user request
        our $Dev = $ENV{HTTP_HOST} =~ /\bdev\./;
-       our ($file) = $ENV{SCRIPT_FILENAME} =~ m{ ([^/]+) \.plp$ }x;
 }
 
-our $Request = decode_utf8($ENV{PATH_INFO} =~ s{^/}{}r);
+our $Request //= decode_utf8($ENV{PATH_INFO} =~ s{^/}{}r);
 
 our $style;
-our $showkeys = !exists $get{keys} ? undef :
-       ($get{keys} ne '0' && ($get{keys} || 'always'));
 
 $header{content_type} = 'text/html; charset=utf-8';
 
@@ -72,7 +76,7 @@ sub stylesheet {
 
        return map { sprintf(
                '<link rel="%s" type="text/css" media="all" href="%s" title="%s">',
-               $_ eq $style ? 'stylesheet' : 'alternate stylesheet', "/$_.css?1.10", $_
+               $_ eq $style ? 'stylesheet' : 'alternate stylesheet', "/$_.css?1.18", $_
        ) } @avail;
 }
 
@@ -93,6 +97,24 @@ sub checkmodified {
        $header{'Last-Modified'} = time2str($lastmod);
 }
 
+sub Data {
+       my ($filename) = @_;
+       my @data = eval {
+               open my $cache, '<:raw', "data/$filename.json"
+                       or return do "./$filename.inc.pl"; # silent fallback to original code
+               require JSON;
+               local $/; # slurp
+               return JSON::decode_json(readline $cache);
+       };
+       if ($@ or !@data or !$data[0]) {
+               die ['Table data not found', $@ || $!];
+       }
+       if (@data == 1 and ref $data[0] eq 'HASH' and not %{$data[0]}) {
+               die ['Table data missing'];
+       }
+       return wantarray ? @data : $data[0]; # list compatibility like do does
+}
+
 sub Html {
        my ($meta) = @_;
 
@@ -109,6 +131,7 @@ sub Html {
        # default fallbacks
        $meta->{stylesheet} ||= [qw( light dark circus mono red )];
        $meta->{charset} ||= 'utf-8';
+       $meta->{lang} ||= 'en';
 
        # convert options to arrays
        ref $_ eq 'ARRAY' or $_ = [$_]
@@ -117,51 +140,61 @@ sub Html {
        # document headers before output
        $header{content_type} = "text/html; charset=$meta->{charset}"
                unless $PLP::sentheaders;
+       exit if $ENV{REQUEST_METHOD} eq 'HEAD';
        unshift @{ $meta->{raw} }, stylesheet($meta->{stylesheet});
 
        push @{ $meta->{raw} }, (
-               '<link rel="stylesheet" type="text/css" media="monochrome" href="/mono.css?1.10" title="light">',
+               '<link rel="stylesheet" type="text/css" media="monochrome" href="/mono.css?1.11" title="light">',
        );
 
-       # optional amends
-       push @{ $meta->{raw} }, (
-               '<!--[if lte IE 6]><style> .help dl.legend dt {margin:0 0 1px} </style><![endif]-->',
-               '<!--[if lte IE 7]><style> .help dl.legend dd {float:none} </style><![endif]-->',
-               !$showkeys ? '<style> .no {visibility:hidden} </style>' :
-               $showkeys eq 'ghost' ? '<style> .no, .alias {opacity:.5} </style>' : (),
-               '<script type="text/javascript" src="/keys.js?1.6" async></script>',
-       ) if $meta->{keys};
-
-       # leading output
-       say '<!DOCTYPE html>';
-       say '<html lang="en">';
-       say '';
-       say '<head>';
-       say sprintf '<meta http-equiv="content-type" content="%s">', $_
-               for $header{content_type};
-       say sprintf '<title>%s</title>', $meta->{title};
-       say sprintf '<meta name="description" content="%s">', EscapeHTML($_)
-               for join(' ', @{ $meta->{description} }) || ();
-       say sprintf '<meta name="keywords" content="%s">', EscapeHTML($_)
-               for join(', ', @{ $meta->{keywords} }) || ();
-       say '<meta name="viewport" content="width=device-width, initial-scale=1">';
-       say '<link rel="icon" type="image/png" href="/clip.png">';
-       say for map { @{$_} } $meta->{raw} || ();
-       say '<meta name="robots" content="noindex">' if $Dev;
-       say '</head>';
-       say '';
-       say sprintf '<body id="%s">', $file;
-
-       # development version indicator
-       printf '<p style="%s">beta</p>', join('; ',
-               'position: fixed',
-               'right: 1em',
-               'opacity: .5',
-               'border: 1ex solid red',
-               'border-width: 1ex 0',
-               'z-index: 1',
-               'background: inherit',
-       ) if $Dev;
+       if (my $img = $meta->{image}) {
+               my $proto = sprintf('http%s://', !!$ENV{HTTPS} && 's');
+               my $url = "$proto$ENV{HTTP_HOST}/$img";
+               push @{ $meta->{raw} }, (
+                       qq(<meta property="og:image" content="$url" />),
+               );
+       }
+
+       my ($file) = $ENV{SCRIPT_FILENAME} =~ m{ ([^/]+) \.plp$ }x;
+
+       $meta->{canonical} //= "/$file" . ($Request ne '' && "/$Request");
+       if (my $url = $meta->{canonical}) {
+               $url = "https://sheet.shiar.nl$url";
+               push @{ $meta->{raw} }, qq(<link rel="canonical" href="$url" />);
+       }
+
+       PLP_START {
+               # leading output
+               say '<!DOCTYPE html>';
+               say qq(<html lang="$meta->{lang}">);
+               say '';
+               say '<head>';
+               say sprintf '<meta http-equiv="content-type" content="%s">', $_
+                       for $header{content_type};
+               say sprintf '<title>%s</title>', $meta->{title};
+               say sprintf '<meta name="description" content="%s">', EscapeHTML($_)
+                       for join(' ', @{ $meta->{description} // [] }) || ();
+               say sprintf '<meta name="keywords" content="%s">', EscapeHTML($_)
+                       for join(', ', @{ $meta->{keywords} // [] }) || ();
+               say '<meta name="viewport" content="width=device-width, initial-scale=1">';
+               say '<link rel="icon" type="image/png" href="/clip.png">';
+               say for map { @{$_} } $meta->{raw} || ();
+               say '<meta name="robots" content="noindex">' if $Dev;
+               say '</head>';
+               say '';
+               say sprintf '<body id="%s">', $file;
+
+               # development version indicator
+               printf '<p style="%s">beta</p>', join('; ',
+                       'position: fixed',
+                       'right: 1em',
+                       'opacity: .5',
+                       'border: 1ex solid red',
+                       'border-width: 1ex 0',
+                       'z-index: 1',
+                       'background: inherit',
+               ) if $Dev;
+       };
 
        # prepare trailing output
        PLP_END {
@@ -176,6 +209,7 @@ sub Html {
         title="Licensed under the GNU Affero General Public License, version 3"
         rel="license">AGPLv3</a>
 EOT
+               our $Time;
                say sprintf '• %.3fs', Time::HiRes::tv_interval($Time) if $Dev and $Time;
                say '</p>';
                say '';
@@ -183,12 +217,30 @@ EOT
        };
 }
 
+BEGIN {
+       $PLP::ERROR = sub {
+               my ($message, $html) = @_;
+               if (ref $message) {
+                       warn join ': ', @{$message};
+                       $html = shift @{$message};
+               }
+               else {
+                       warn $message;
+                       $message = [];
+               }
+               unless ($PLP::sentheaders) {
+                       Html({nocache => 1});
+                       say '<h1>Page unavailable</h1>';
+               }
+               Alert("Fatal error: $html.", @{$message});
+       };
+}
+
 sub showlink {
        my ($title, $href, $selected) = @_;
-       return sprintf(
-               !$href ? '%s' :
-               $selected ? '<strong>%s</strong>' : '<a href="%2$s">%s</a>',
-               EscapeHTML($title), EscapeHTML($href)
-       );
+       EscapeHTML($title);
+       return $title if not $href;
+       return "<strong>$title</strong>" if $selected;
+       return sprintf '<a href="%s">%s</a>', EscapeHTML($href), $title;
 }
 
index 1ed8eef2740097e99d66f22718ea98008872b014..efd2f256407351c0c88973d6d165ccc398e56d32 100644 (file)
@@ -14,10 +14,10 @@ Html({
 <h1>ISO-3166-1α2 Country codes</h1>
 
 <:
-my $cc = do 'data/countries.inc.pl';
+my $cc = Data('data/countries');
 
 {
-       printf '<table class="mcmap">';
+       printf '<table class="ccmap">';
        print '<col><colgroup span="26">';
        for my $section (qw{thead}) {
                print "<$section><tr><th>↳";
@@ -46,7 +46,7 @@ my $cc = do 'data/countries.inc.pl';
                                }
 
                                $cell = showflag($code) // join(' ',
-                                       map { showflag($_) || $_ } split / /, $ref
+                                       map { showflag($_) || $_ } split(/ /, $ref)
                                );
                        }
                        else {
@@ -89,3 +89,26 @@ my $cc = do 'data/countries.inc.pl';
        </div>
 </div>
 
+<: exit unless exists $get{v}; :>
+<script type="text/javascript"><!--
+       const table = document.querySelector('.ccmap');
+       const label = Array.prototype.map.call(table.tHead.rows[0].children, i => i.textContent);
+       const flagchr = 0x1F1E5; // regional indicator symbol letter base
+       let nowidth;
+       for (let row = 0; row < label.length; row++) {
+               for (let col = 0; col < label.length; col++) {
+                       let cell = table.rows[row].cells[col];
+                       if (!cell.className) continue;
+                       let flag = String.fromCodePoint(flagchr + row) + String.fromCodePoint(flagchr + col);
+                       cell.innerHTML = `<big>${flag}</big>&nbsp;` + cell.innerHTML;
+                       if (nowidth === undefined) {
+                               // assume AA is invalid, compare its size to validate following glyphs
+                               nowidth = cell.firstChild.offsetWidth;
+                       }
+                       if (cell.firstChild.offsetWidth == nowidth) {
+                               cell.firstChild.remove();
+                       }
+               }
+       }
+//--></script>
+
index 6cb893f2c88cc68f115950a23054b47953a2bd36..e7df58b5fc8214c93852f3638f6626e06ec0f6f5 100644 (file)
--- a/dark.css
+++ b/dark.css
@@ -1,4 +1,4 @@
-@import url(light.css?1.10);
+@import url(light.css?1.11);
 
 body {
        background: #000;
@@ -108,6 +108,83 @@ th, td {
 .legend .ex:hover {background: #666}
 .X:hover small {color: #FFF}
 
+/* images */
+
+figcaption {
+       color: #FFF;
+       background: rgba(0, 0, 0, .5);
+}
+.gallery figure:hover ~ ul figcaption {
+       /* mark all children */
+       color: #000;
+       background: rgba(255, 255, 255, .5);
+}
+
+/* starcraft */
+
+.units tbody tr:hover:not(.race) {
+       background: #222;
+}
+.unit-gas {
+       color: #AC9;
+}
+.unit-min, .unit-min a:not(:hover) {
+       color: #ABC;
+}
+.unit-supply {
+       color: #8C6;
+}
+.unit-o {color: #C5A} /* organic */
+.unit-u {color: #66B} /* mechanic */
+.unit-p {color: #0A8} /* psionic */
+.unit-composed {
+       color: #A44;
+}
+.unit-air {
+       color: #4AC;
+}
+.unit-x {color: #666}
+.unit-s {color: #AC6}
+.unit-m {color: #C70}
+.unit-l {color: #C44}
+.unit-h {color: #C06}
+.magic-opt:before,
+.magic-opt:after {
+       color: #CCC;
+}
+.hurtrel, .units .hurtrel {
+       color: #887;
+}
+tbody .unit-shield {
+       color: #88A;
+}
+.unit-pdd {
+       color: #A8C;
+}
+.unit-splash {
+       color: #4A0;
+}
+.hurt-a {
+       color: #8AC;
+}
+.hurt-g {
+       color: #8CA;
+}
+.unit-massive {
+       color: #844;
+}
+.unit-detect::before {
+       color: #0A8;
+}
+.unit-jump {
+       color: #780;
+}
+body .magic-perma {
+               text-decoration-color: #460;
+          -moz-text-decoration-color: #460;
+       -webkit-text-decoration-color: #460;
+}
+
 /* keyboard */
 
 @media (max-width: 79em) {
index b704627e5367831dc0d3a0c017a39bdb1f558cd9..7f5c1133b19d0c34c3059d5eee7a3d27f3ce2a37 100644 (file)
@@ -1,4 +1,4 @@
-@import url(dark.css?1.10);
+@import url(dark.css?1.11);
 
 th, td {
        border-color: #333;
diff --git a/dieren.inc.pl b/dieren.inc.pl
new file mode 100644 (file)
index 0000000..169fb81
--- /dev/null
@@ -0,0 +1,30 @@
+#!/bin/env perl
+use 5.014;
+use warnings;
+use utf8;
+
+[map {[split ' ']} grep {$_} split /\n/, <<'.'];
+ :      origineel:      zee-:   meer_water:     land/aardig:    anders:        #:
+ hond: +hond   +zeehond        ?scheepshond    +prairiehond     vleerhond      #rodehond
+ kat:  +kat    +zeekat +meerkat        ?cat_325         vliegende_kat  #tijgerkat
+#haas:  haas    zeehaas         waterhaas      ?koolhaas       ?ossenhaas      #buidelhaas
+ muis: +muis   +zeemuis         waterspitsmuis  aardmuis       +vleermuis      #computermuis
+ rat:   rat     zeerat  waterrat        woestijnrat     buidelrat      #beverrat
+ egel: +egel   +zee-egel       ?wateregel      ?aardegel?=cactus       +mierenegel     #kegel
+ varken:       +varken +zeevarken=bruinvis      waterzwijn=capibara     aardvarken     +stekelvarken   #feestvarken
+ koe:  +koe    +zeekoe +meerkoetje     ?aardekoe?       koedoe #haiku
+ paard:        +paard  +zeepaardje     +nijlpaard      ?grasmodderpaard=草泥马#?(turn)paard  luipaard       #tijgerpaard
+#hoorn:         eenhoorn        zeehoorn       ?zee-eenhoorn=narwal     bergahorn=esdoorn       neushoorn      #eekhoorn
+#bra:  ?bra(ssière)    zebra   -       -       cobra  #sabra
+#olifant:      +olifant        +zeeolifant     +olifantsvis    ?kamerolifant    -      #olifantsoor
+ beer: +beer   +zeebeer         waterbeertje    ijsbeer        +wasbeer        #neusbeer
+ leeuw:        +leeuw  +zeeleeuw       ?waterleeuw?    ?aardleeuw?=kameleon    +mierenleeuw    #leeuwerik
+#vos:  vos     +zeevos=voshaai -       koolvos voskonijn       #Zorro
+ wolf: +wolf   +zeewolf        ?waterwolf?=snoek        aardwolf       +korenwolf      #bijenwolf
+ haan: +haan   +zeehaan         waterhaan       rotshaan       +sprinkhaan     #wilde_haan?=wildrooster
+#pad:   pad    ?zebrapad       rivierdonderpad  landpad         schildpad      #paddenstoel
+ draak:         draak_#draak    zeedraak        waterdraak=agame       ?aarddraak=戊辰        komododraak=varaan     #drakenkop
+#vlo:   vlo     zeevlo  watervlo        aardvlo         -      #vlok
+#mot:  +mot    +marmot  watermot       +bergamot       ?behemoth       #
+#bij:   bij     -       waterbij        aardbei         moerbei        #hommelbij
+.
diff --git a/dieren.jpg b/dieren.jpg
new file mode 100644 (file)
index 0000000..a4d0104
Binary files /dev/null and b/dieren.jpg differ
diff --git a/dieren.plp b/dieren.plp
new file mode 100644 (file)
index 0000000..8e66008
--- /dev/null
@@ -0,0 +1,146 @@
+<(common.inc.plp)><:
+use warnings;
+no warnings 'qw';
+
+my $intro = 'dieren die in het Nederlands vernoemd zijn naar andere dieren.';
+my %subpages = (
+       standaard => {
+               title => 'dieren',
+               intro => $intro,
+               altlink => 'Zie ook <a href="/dieren/uitgebreid">verdergezochte verbanden</a>' .
+                          ' of het <a href="/dieren/beknopt">beknopte overzicht</a>.',
+               prefix => qr/^(?!#)\+?/, # no # optional +
+               colfilter => 0,
+       },
+       uitgebreid => {
+               title => 'uitgebreid dieren',
+               intro => "$intro.. en dergelijke.",
+               altlink => 'Zie het <a href="/dieren">populaire overzicht</a> voor minder.',
+               prefix => qr/.*?[#]|^[#+]*/, # after optional # or +
+               secrets => 1,
+       },
+       beknopt => {
+               title => 'beknopt dieren',
+               intro => "een aantal $intro",
+               altlink => 'Zie het <a href="/dieren">populaire overzicht</a> voor meer.',
+               prefix => qr/^\+/, # only +
+               colfilter => 1,
+       },
+);
+
+$Request ||= 'standaard';
+my $pageinfo = $subpages{$Request}
+       or Html(), Abort("Onbekende dierenpagina <q>$Request</q>", '404 request not found');
+
+Html({
+       title => $pageinfo->{title}.' cheat sheet',
+       version => '1.2',
+       lang => 'nl',
+       description => "Tabeloverzicht met afbeeldingen van $pageinfo->{intro}",
+       keywords => [qw'
+               dier beest naam naamgeving woord taal nederlands gerelateerd
+               relatie vernoemd vernoeming combinatie samenstelling voorvoegsel onverwant
+               land zee lucht  animals dutch language
+       '],
+       image => 'dieren.jpg',
+       raw => <<"EOT",
+<style>
+figure[hidden] {
+       opacity: 0; /* secret */
+       transition: opacity 1s 0s;
+       display: block;
+}
+figure[hidden]:hover {
+       opacity: 1;
+       transition-delay: 1s;
+}
+figure[hidden]:hover > figcaption {
+       transition-delay: 2s;
+}
+
+\@media (max-width: 60em) {
+       td, th {
+               font-size: 50%;
+       }
+       figcaption small {
+               display: none;
+       }
+       th:first-child {
+               display: none;
+       }
+}
+</style>
+EOT
+});
+
+:>
+<h1>Dierennamen <small lang=en>(Dutch animal names)</small></h1>
+
+<p>
+<:
+say ucfirst $pageinfo->{intro};
+say $pageinfo->{altlink};
+:>
+</p>
+
+<:
+my $table = Data('dieren');
+
+if (exists $get{r}) {
+       use List::MoreUtils qw( part );
+       my @trans = (part { state $col; /^#?>/ ? ($col = 0) : ++$col } @{$table});
+       $table = [];
+       for (@trans) {
+               unshift @$_, '?:' if $_->[0] !~ /:$/;
+               $_->[0] =~ s/^#?\K>?/>>/;
+               for (@$_) {
+                       push @$table, s/^#?\K>/$1/r;
+               }
+       }
+}
+
+for my $prefix ($pageinfo->{prefix}) {
+       for my $col ($pageinfo->{colfilter} // ()) {
+               @{$table} = grep { $_->[$col] =~ $prefix } @{$table};
+       }
+       $_ = [ grep { s/$prefix// } @{$_} ] for @{$table};
+}
+
+say '<table class="gallery">';
+for my $row (@{$table}) {
+       print '<tr>';
+       for my $name (@{$row}) {
+               my $hidden = $name =~ s/^\?//;
+               $name =~ s/#.*//; # ignore prefixed part
+               $name =~ s/^-$//;
+               my ($img) = $name =~ /([\w-]+)/;
+               $name =~ y/_/ /;
+               if ($name =~ s/:$//) {
+                       # trailing colon indicates header text
+                       print "<th>$name</th>";
+                       next;
+               }
+               print '<td>';
+               my $alt = $1 if $name =~ s/=(.*)//;
+               $name = "<q>$name</q>" if $name =~ s/\?$//;
+               $name .= " <small>($alt)</small>" if $alt;
+
+               printf '<figure%s>', $hidden && !$pageinfo->{secrets} && ' hidden';
+               if ($img and -e ($img = lc "data/dieren/$img.jpg")) {
+                       printf '<img src="/%s"', $img;
+                       printf ' alt="%s"', $alt || $name;
+                       print ' />';
+                       print "<figcaption>$name</figcaption>";
+               }
+               elsif ($hidden) {
+                       printf '<figcaption>%s</figcaption>', "$name?";
+               }
+               else {
+                       print $name;
+               }
+               print '</figure>';
+               print '</td>';
+       }
+       say '</tr>';
+}
+say '</table>';
index a6a684e6ced5ba4d24d6539b990a8545c1adfcf8..a55b49a8ca6b8af706aa6e617021e4940ea3047b 100644 (file)
@@ -37,11 +37,10 @@ unless (exists $get{v}) {
        $glyphs->{style} = 'univer';
 }
 
-my $scriptname = do 'writing-script.inc.pl';
+my $scriptname = eval { Data('writing-script') };
 $_ = showlink($_, "/latin") for $scriptname->{latn} || ();
 
-my $table = do "writing-digits.inc.pl";
-die "Table data not found: $_\n" for $@ || $! || ();
+my $table = Data("writing-digits");
 
 sub printtable {
        say '<div class=section>', $glyphs->tabletag;
index 5e4a3341812d689f3c4599061efd1e1e1c7a67d6..9f96c502b0ebd820116105b89adbd96f39e01543 100644 (file)
@@ -1,51 +1,40 @@
 <(common.inc.plp)><:
 
-my $mode = ($Request // '') eq 'xorg' || exists $get{xorg};
-my $modename = $mode ? 'X.Org' : 'RFC-1345';
+my $mode = $Request || 'vim';
+my $include = 'digraphs' . ($mode ne 'vim' && "-$mode");
+my $cmp = exists $get{cmp} ? ($get{cmp} // 1) : !!$Request;
+
+my $di = eval { Data($include) } || {};
+warn "error in $include: ", @{$@} if ref $@;
 
 Html({
-       title => 'digraph cheat sheet',
-       version => '1.2',
-       description => [
-               "Complete table of digraph characters from $modename.",
+       title => "$mode digraph cheat sheet",
+       version => '1.4',
+       description => $di->{description} // [
+               "Complete table of digraph characters from",
+               ($di->{title} // $mode) . ".",
        ],
-       keywords => [qw'
+       keywords => [@{ $di->{keywords} // [] }, qw'
                digraph mnemonic compose composition pair
-               character char glyph table unicode vim xorg x11 x
+               character char glyph table unicode vim
        '],
        stylesheet => [qw'light'],
-       data => [qw( data/digraphs.inc.pl )],
+       data => ["data/$include.json"],
 });
 
-:>
-<h1><:= $modename :> Digraphs</h1>
-
-<p>Character mnemonics following compose key ⎄<:
-say join("\n",
-       $mode ? (
-               ' in the X Window System (Shift+AltGr by default).',
-               'Differences from <a href="/digraphs">RFC-1345</a> are indicated.',
-       ) : (':',
-               'i^k in <a href="/vi">Vim</a>,',
-               '^u^\ in <a href="/readline">Emacs</a>,',
-               '^a^v in <a href="/screen">Screen</a>.',
-               'Similar but different from <a href="/digraphs/xorg">X.Org</a>.',
-       ),
-       'Also see <a href="/unicode">common Unicode</a>.</p>',
+%{$di} or Abort(
+       "Requested digraphs <q>$mode</q> not available",
+       '404 request not found',
 );
-say '<p class="aside">Unofficial <span class="u-l2">proposals</span>',
-       ' are available as <a href="/digraphs.vim">ex commands</a>.' if not $mode;
-:>
 
-<:
-my $di = do 'data/digraphs.inc.pl'
-       or die "Error loading digraphs data: ", $@ // $!;
+say "<h1>$di->{title} Digraphs</h1>";
+say "<p>$_</p>" for $di->{intro} // ();
 
 if (exists $get{v}) {
        # show characters for inverted mnemonics (vim alternatives)
-       $di->{ substr($_, 1, 1) . substr($_, 0, 1) } ||=
-               [ $di->{$_}->[0], '', 'l0 ex', '', $di->{$_}->[4] ]
-               for grep { ref $di->{$_} } keys %{$di};
+       $di->{key}->{ substr($_, 1, 1) . substr($_, 0, 1) } ||= [
+               $di->{key}->{$_}->[0], '', 'l0 ex', '', $di->{key}->{$_}->[4]
+       ] for grep { ref $di->{key}->{$_} } keys %{ $di->{key} };
 }
 
 my @chars = (
@@ -58,36 +47,10 @@ my @chars2 = (['_'], @chars);  # trailing character (extended set)
 my @columns = !exists $get{split} ? \@chars2 :
        ([@chars2[0, 1, 3, 4, 6]], [@chars2[2, 5, 7]]);
 
-if ($mode) {
-       my $xorg = do 'data/digraphs-xorg.inc.pl'
-               or die "Error loading Xorg data: ", $@ // $!;
-       $_ = [ord $_] for values %{$xorg};
-       $xorg->{$_}->[2] = # class = compatibility
-               $di->{$_} ? $di->{$_}->[0] != $xorg->{$_}->[0] ? 'l1' :  # conflict
-               $di->{$_}->[2] eq 'l4' ? 'l5' : 'l3' : 'l2'  # rfc|any|none
-               for keys %{$xorg};
-
-       for my $cp (map {$_->[0]} values %{$xorg}) {
-               next if (state $seen = {})->{$cp}++;  # List::MoreUtils::uniq
-
-               # find multiple equivalent mnemonics
-               my @equiv = grep {$cp eq $_->[0]}
-                       map {$xorg->{$_}} sort keys %{$xorg}; # values ordered by mnem.
-
-               # search for the most compatible match
-               my ($compat) = sort {
-                       $equiv[$b]->[2] cmp $equiv[$a]->[2]  # highest level
-                       || $b <=> $a  # fallback to last mnemonic
-               } 0 .. $#equiv;
-
-               # reclassify all but one as level 0 (omitted)
-               splice @equiv, $compat // -1, 1, ();
-               $_->[2] = 'l0 ex' for @equiv;
-       }
-
+if ($mode eq 'xorg') {
+       #TODO determine character usage from declared keys
        $chars2[0] = [qw( # ^ _ ` ~ )];
        @chars = @chars2;
-       $di = $xorg;
 }
 
 for my $colchars (@columns) {
@@ -105,21 +68,23 @@ for my $c1group (@chars) {
                print '<tr><th>', EscapeHTML($c1);
                for my $c2 (map {@$_} @$colchars) {
                        my $mnem = $c1 . $c2;
-                       if (not defined $di->{$mnem}) {
+                       if (not defined $di->{key}->{$mnem}) {
                                print '<td>';
                                next;
                        }
-                       if (ref $di->{$mnem} ne 'ARRAY') {
+                       if (ref $di->{key}->{$mnem} ne 'ARRAY') {
                                printf '<td class="X Xr" title="%s">', EscapeHTML($mnem);
                                next;
                        }
-                       my ($codepoint, $name, $support, $script, $string) = @{ $di->{$mnem} };
+                       my ($codepoint, $name, $support, $script, $string) =
+                               @{ $di->{key}->{$mnem} };
 
-                       my $glyph = $string || chr $codepoint;
+                       my $glyph = $string || !!$codepoint && chr $codepoint;
                        utf8::upgrade($glyph);  # prevent latin1 output
                        my $desc = $mnem . ($name && " ($name)");
                        my @class = ('X', grep {$_} $script);
-                       push @class, $mode ? $support : "u-$support" if $support;
+                       push @class, $cmp ? $support :
+                               $di->{flagclass}->{$support} // "u-$support" if $support;
 
                        $glyph = EscapeHTML($glyph);
                        $glyph = "<span>$glyph</span>" if $script =~ /\bZs\b/;
@@ -134,19 +99,8 @@ say '</table>';
 print '<hr>' if exists $get{split};
 }
 
-if ($mode) {
 :>
-<div class="legend">
-       <table class="glyphs"><tr>
-       <td class="X l5">matching RFC-1345
-       <td class="X l3">matching proposal
-       <td class="X l2">unique to Xorg
-       <td class="X l1">conflict
-       <td class="X l0 ex">duplicate
-       </table>
-</div>
-<: } else { :>
-<div class="legend">
+<div class="legend"><: unless ($cmp) { :>
        <table class="glyphs"><tr>
        <td class="X Cc">control
        <td class="X Zs"><span>space</span>
@@ -167,14 +121,11 @@ if ($mode) {
        <td class="X Hiragana">japanese
        <td class="X Bopomofo">chinese
        </table>
-
-       <table class="glyphs"><tr>
-       <td class="X u-l4">full support
-       <td class="X u-l3">vim extension
-       <td class="X u-l3 ex">vim v8.0
-       <td class="X u-l2">proposal
-       <td class="X u-l1">not in vim
+<: } :>
+       <table class="glyphs"><tr><:
+       printf qq(\n\t<td class="X %s">%s), $cmp ? $_ : $di->{flagclass}{$_} // "u-$_", $di->{flag}->{$_}
+               for sort keys %{ $di->{flag} };
+:>
        </table>
 </div>
 
-<: }
index 9883031bc1d03bf127a9f67115b5c07d8a862927..c659288debfccb14654971d81b02e6a8c869548c 100644 (file)
@@ -6,7 +6,7 @@ use open IO => ':utf8';
 our $VERSION = 'v1.0';
 
 $header{content_type} = 'text/plain; charset=us-ascii';
-say '" vim digraph proposals <http://sheet.shiar.nl/digraphs>';
+say '" vim digraph proposals <https://sheet.shiar.nl/digraphs>';
 PLP_END { print "\n" };
 
 open my $include, '<', 'shiar.inc.txt' or do {
index 74ab9391e0f3ebe1035a3aed9979d7e2b2464ef2..34972f3bc74a10c52a7c9dfd0e81181cf6c8b30d 100644 (file)
@@ -1,55 +1,50 @@
 {
        name => 'Gmail',
-       icon => 'http://mail.google.com/mail/help/images/screenshots/chat/%s.gif',
-       iconext => 'http://usefulshortcuts.com/imgs/gtalk-hidden/%s.gif',
+       icon => '/data/emoji/gmail/%s.gif',
+       iconext => '/data/emoji/gmail/%s.png',
        source => 'http://mail.google.com/support/bin/answer.py?answer=34056',
 },
 
 'official',
 
-heart  => ['<3',       '',     0x02665,        "heart/love"],
-monkey => [':(|)',     '',     0x1F435,        "it's a monkey!"],
-rockout        => ['\m/',      'fuzzy',        0x0270A,        "rock out."],
-shocked        => [':-o',      '',     0x1F632,        "shocked"],
-grin   => [':D',       '',     0x1F603,        "grin"],
-frown  => [':(',       '',     0x02639,        "frown"],
-angry  => ['x-(',      '',     0x1F623,        "angry"],
-cool   => ['B-)',      '',     0x1F60E,        "cool"],
-cry    => [":'(",      '',     0x1F622,        "cry"],
-equal_grin     => ['=D',       '',     0,      "equal grin"],
-wink   => [';)',       '',     0x1F609,        "wink"],
-straightface   => [':-|',      '',     0x1F610,        "straight face"],
-equal_smile    => ['=)',       '',     0,      "equal smile"],
-nose_grin      => [':-D',      '',     0,      "nose grin"],
-wink_big_nose  => [';^)',      '',     0,      "big nose wink"],
-wink_nose      => [';-)',      '',     0,      "nose wink"],
-nose_smile     => [':-)',      '',     0,      "nose smile"],
-slant  => [':-/',      'fuzzy',        0x1F616,        "slant"],
-tongue => [':P',       '',     0x1F61D,        "tongue"],
+[heart => '<3',        '',     0x02665,        "heart/love"],
+[monkey        => ':(|)',      '',     0x1F435,        "it's a monkey!"],
+[rockout       => '\m/',       '',     0x1F918,        "rock out."],
+[shocked       => ':-o',       '',     0x1F632,        "shocked"],
+[grin  => ':D',        '',     0x1F603,        "grin"],
+[nose_grin     => ':-D',       '',     0,      "nose grin"],
+[equal_grin    => '=D',        '',     0,      "equal grin"],
+[frown => ':( :-( =(', '',     0x02639,        "frown"],
+[angry => 'x-(',       '',     0x1F623,        "angry"],
+[cool  => 'B-)',       '',     0x1F60E,        "cool"],
+[cry   => ":'(",       '',     0x1F622,        "cry"],
+[wink  => ';)',        '',     0x1F609,        "wink"],
+[wink_nose     => ';-)',       '',     0,      "nose wink"],
+[wink_big_nose => ';^)',       'fuzzy',        0x1F925,        "big nose wink"],
+[straightface  => ':-|',       '',     0x1F610,        "straight face"],
+[equal_smile   => '=)',        '',     0,      "equal smile"],
+[nose_smile    => ':-)',       '',     0,      "nose smile"],
+[slant => ':-/ =/',    'fuzzy',        0x1F616,        "slant"],
+[tongue        => ':P :-P =P', '',     0x1F61D,        "tongue"],
 
 # http://tkhere.blogspot.com/2007/12/brand-new-google-chat-emoticons-no-one.html
 
 'undocumented',
 
-cowbell        => ["+/'\\",    '',     0x1F514,        "cowbell"],
-crab   => ['V.v.V',    'ext',  0,      "crab"],
-devil  => ['}:-)',     'ext',  0x1F608,        "devil"],
-frown  => ['=(',       '',     0,      "equal sad"],
-slant  => ['=/',       '',     0,      "equal slant"],
-tongue => ['=P',       '',     0,      "equal tongue"],
-frown  => [':-(',      '',     0,      "nose sad"],
-smile  => [':)',       '',     0x0263A,        "smile"],
-wince  => ['>.<',      'ext',  0,      "wince"],
-tongue => [':-P',      '',     0,      "nose tongue"],
-pig    => [':(:)',     'ext',  0x1F437,        "pig"],
-brokenheart    => ['</3',      'ext',  0x1F494,        "broken heart"],
-kissx  => [':-x',      'ext',  0x1F618,        "kiss"],
-kissstar       => [':*',       'ext',  0x1F61A,        "kiss"],
-mustache       => [':{',       'ext',  0,      "mustache"],
+[cowbell       => "+/'\\",     '',     0x1F514,        "cowbell"],
+[crab  => 'V.v.V',     'ext',  0x1F980,        "crab"],
+[devil => '}:-)',      'ext',  0x1F608,        "devil"],
+[smile => ':)',        '',     0x0263A,        "smile"],
+[wince => '>.<',       'ext',  0x1F623,        "wince"],
+[pig   => ':(:)',      'ext',  0x1F437,        "pig"],
+[brokenheart   => '</3',       'ext',  0x1F494,        "broken heart"],
+[kissx => ':-x',       'ext',  0x1F618,        "kiss"],
+[kissstar      => ':*',        'ext',  0x1F61A,        "kiss"],
+[mustache      => ':{',        'ext fuzzy',    0x1F978,        "mustache"],
 
 # http://www.gtricks.com/google-talk-tricks/google-talk-hidden-emoticons/
 
-robot  => ['[:|]',     'ext',  0],
-poo    => ['~@~',      'ext',  0x1F4A9],
+[robot => '[:|]',      'ext',  0x1F916],
+[poo   => '~@~',       'ext',  0x1F4A9],
 
-# vi:ts=15
+# vi:ts=16
index a0064a84357ff0cec517071ab0865248e5573f5a..d2a7c298ca66a8844d2c8be529238c09d4b932d6 100644 (file)
@@ -6,80 +6,80 @@
 
 'faces',
 
-regular_smile  => [':-) :)',   '',     0x0263A,        "smile"],
-teeth_smile    => [':-D :d',   '',     0x1F603,        "open-mouthed"],
-omg_smile      => [':-O :o',   '',     0x1F632,        "surprised"],
-tongue_smile   => [':-P :p',   '',     0x1F61C,        "tongue out"],
-wink_smile     => [';-) ;)',   '',     0x1F609,        "wink"],
-sad_smile      => [':-( :(',   '',     0x02639,        "sad"],
-confused_smile => [':-S :s',   '',     0x1F616,        "confused"],
-what_smile     => [':-| :|',   '',     0x1F61E,        "disappointed"],
-cry_smile      => [":'(",      '',     0x1F62D,        "crying"],
-red_smile      => [':-$ :$',   '',     0x1F633,        "embarrassed"],
-shades_smile   => ['(H) (h)',  '',     0x1F60E,        "hot"],
-angry_smile    => [':-@ :@',   '',     0x1F620,        "angry"],
-angel_smile    => ['(A) (a)',  '',     0x1F607,        "angel"],
-devil_smile    => ['(6)',      '',     0x1F608,        "devil"],
-'47_47'        => [':-#',      'fuzzy',        0x1F64A,        "don't tell anyone"],
-'48_48'        => ['8o|',      'todo', 0,      "baring teeth"],
-'49_49'        => ['8-|',      'fuzzy',        0x1F453,        "nerd"],
-'50_50'        => ['^o)',      '',     0,      "sarcastic"],
-'51_51'        => [':-*',      'fuzzy',        0x1F442,        "secret telling"],
-'52_52'        => ['+o(',      'fuzzy',        0x1F637,        "sick"],
-'71_71'        => [':^)',      'fuzzy',        0x1F610,        "i don't know"],
-'72_72'        => ['*-)',      '',     0x1F614,        "thinking"],
-'74_74'        => ['<:o)',     'fuzzy',        0x1F389,        "party"],
-'75_75'        => ['8-)',      'fuzzy',        0x1F612,        "eye-rolling"],
-'77_77'        => ['|-)',      '',     0x1F629,        "sleepy"],
+[regular_smile => ':-) :)',    '',     0x0263A,        "smile"],
+[teeth_smile   => ':-D :d',    '',     0x1F603,        "open-mouthed"],
+[omg_smile     => ':-O :o',    '',     0x1F632,        "surprised"],
+[tongue_smile  => ':-P :p',    '',     0x1F61C,        "tongue out"],
+[wink_smile    => ';-) ;)',    '',     0x1F609,        "wink"],
+[sad_smile     => ':-( :(',    '',     0x02639,        "sad"],
+[confused_smile        => ':-S :s',    '',     0x1F616,        "confused"],
+[what_smile    => ':-| :|',    '',     0x1F61E,        "disappointed"],
+[cry_smile     => ":'(",       '',     0x1F62D,        "crying"],
+[red_smile     => ':-$ :$',    '',     0x1F633,        "embarrassed"],
+[shades_smile  => '(H) (h)',   '',     0x1F60E,        "hot"],
+[angry_smile   => ':-@ :@',    '',     0x1F620,        "angry"],
+[angel_smile   => '(A) (a)',   '',     0x1F607,        "angel"],
+[devil_smile   => '(6)',       '',     0x1F608,        "devil"],
+['47_47'       => ':-#',       '',     0x1F910,        "don't tell anyone"],
+['48_48'       => '8o|',       'todo', 0,      "baring teeth"],
+['49_49'       => '8-|',       'fuzzy',        0x1F453,        "nerd"],
+['50_50'       => '^o)',       '',     0x1F928,        "sarcastic"],
+['51_51'       => ':-*',       'fuzzy',        0x1F442,        "secret telling"],
+['52_52'       => '+o(',       'fuzzy',        0x1F637,        "sick"],
+['71_71'       => ':^)',       'fuzzy',        0x1F610,        "i don't know"],
+['72_72'       => '*-)',       '',     0x1F614,        "thinking"],
+['74_74'       => '<:o)',      '',     0x1F973,        "party"],
+['75_75'       => '8-)',       'fuzzy',        0x1F612,        "eye-rolling"],
+['77_77'       => '|-)',       '',     0x1F629,        "sleepy"],
 
 'objects',
 
-coffee => ['(C) (c)',  '',     0x02615,        "coffee cup"],
-thumbs_up      => ['(Y) (y)',  '',     0x1F44D,        "thumbs up"],
-thumbs_down    => ['(N) (n)',  '',     0x1F44E,        "thumbs down"],
-beer_mug       => ['(B) (b)',  '',     0x1F37A,        "beer mug"],
-martini        => ['(D) (d)',  '',     0x1F378,        "martini glass"],
-girl   => ['(X) (x)',  '',     0x1F467,        "girl"],
-guy    => ['(Z) (z)',  '',     0x1F466,        "boy"],
-guy_hug        => ['({)',      '',     0,      "left hug"],
-girl_hug       => ['(})',      '',     0,      "right hug"],
-bat    => [':-[ :[',   '',     0,      "vampire bat"],
-cake   => ['(^)',      '',     0x1F382,        "birthday cake"],
-heart  => ['(L) (l)',  '',     0x02665,        "red heart"],
-broken_heart   => ['(U) (u)',  '',     0x1F494,        "broken heart"],
-kiss   => ['(K) (k)',  '',     0x1F48B,        "red lips"],
-present        => ['(G) (g)',  '',     0x1F381,        "gift with a bow"],
-rose   => ['(F) (f)',  '',     0x1F339,        "red rose"],
-wilted_rose    => ['(W) (w)',  '',     0,      "wilted rose"],
-camera => ['(P) (p)',  '',     0x1F4F7,        "camera"],
-film   => ['(~)',      '',     0x1F3A5,        "filmstrip"],
-cat    => ['(@)',      '',     0x1F431,        "cat face"],
-dog    => ['(&)',      '',     0x1F436,        "dog face"],
-phone  => ['(T) (t)',  '',     0x1F4DE,        "telephone receiver"],
-lightbulb      => ['(I) (i)',  '',     0x1F4A1,        "light bulb"],
-note   => ['(8)',      '',     0x0266A,        "note"],
-moon   => ['(S)',      '',     0x1F31C,        "sleeping half-moon"],
-star   => ['(*)',      '',     0x02606,        "star"],
-envelope       => ['(E) (e)',  '',     0x1F4E7,        "e-mail"],
-clock  => ['(O) (o)',  '',     0x023F0,        "clock"],
-messenger      => ['(M) (m)',  'fuzzy',        0x1F465,        "MSN Messenger icon"],
+[coffee        => '(C) (c)',   '',     0x02615,        "coffee cup"],
+[thumbs_up     => '(Y) (y)',   '',     0x1F44D,        "thumbs up"],
+[thumbs_down   => '(N) (n)',   '',     0x1F44E,        "thumbs down"],
+[beer_mug      => '(B) (b)',   '',     0x1F37A,        "beer mug"],
+[martini       => '(D) (d)',   '',     0x1F378,        "martini glass"],
+[girl  => '(X) (x)',   '',     0x1F467,        "girl"],
+[guy   => '(Z) (z)',   '',     0x1F466,        "boy"],
+[guy_hug       => '({)',       '',     0,      "left hug"],
+[girl_hug      => '(})',       '',     0,      "right hug"],
+[bat   => ':-[ :[',    '',     0x1F987,        "vampire bat"],
+[cake  => '(^)',       '',     0x1F382,        "birthday cake"],
+[heart => '(L) (l)',   '',     0x02665,        "red heart"],
+[broken_heart  => '(U) (u)',   '',     0x1F494,        "broken heart"],
+[kiss  => '(K) (k)',   '',     0x1F48B,        "red lips"],
+[present       => '(G) (g)',   '',     0x1F381,        "gift with a bow"],
+[rose  => '(F) (f)',   '',     0x1F339,        "red rose"],
+[wilted_rose   => '(W) (w)',   '',     0x1F940,        "wilted rose"],
+[camera        => '(P) (p)',   '',     0x1F4F7,        "camera"],
+[film  => '(~)',       'fuzzy',        0x1F3A5,        "filmstrip"],
+[cat   => '(@)',       '',     0x1F431,        "cat face"],
+[dog   => '(&)',       '',     0x1F436,        "dog face"],
+[phone => '(T) (t)',   '',     0x1F4DE,        "telephone receiver"],
+[lightbulb     => '(I) (i)',   '',     0x1F4A1,        "light bulb"],
+[note  => '(8)',       '',     0x0266A,        "note"],
+[moon  => '(S)',       '',     0x1F31C,        "sleeping half-moon"],
+[star  => '(*)',       '',     0x02606,        "star"],
+[envelope      => '(E) (e)',   '',     0x1F4E7,        "e-mail"],
+[clock => '(O) (o)',   '',     0x023F0,        "clock"],
+[messenger     => '(M) (m)',   'fuzzy',        0x1F465,        "MSN Messenger icon"],
 
 'secondary',
 
-'53_53'        => ['(sn)',     '',     0x1F40C,        "snail"],
-'70_70'        => ['(bah)',    'fuzzy',        0x1F411,        "black sheep"],
-'55_55'        => ['(pl)',     '',     0x1F374,        "plate"],
-'56_56'        => ['(||)',     '',     0x1F35C,        "bowl"],
-'57_57'        => ['(pi)',     '',     0x1F355,        "pizza"],
-'58_58'        => ['(so)',     '',     0x026BD,        "soccer ball"],
-'59_59'        => ['(au)',     '',     0x1F697,        "auto"],
-'60_60'        => ['(ap)',     '',     0x02708,        "airplane"],
-'61_61'        => ['(um)',     '',     0x02602,        "umbrella"],
-'62_62'        => ['(ip)',     '',     0x1F334,        "island with a palm tree"],
-'63_63'        => ['(co)',     '',     0x1F4BB,        "computer"],
-'64_64'        => ['(mp)',     '',     0x1F4F1,        "mobile phone"],
-'66_66'        => ['(st)',     '',     0x02601,        "stormy cloud"],
-'73_73'        => ['(li)',     'fuzzy',        0x02607,        "lightning"],
-'69_69'        => ['(mo)',     '',     0x1F4B0,        "money"],
+['53_53'       => '(sn)',      '',     0x1F40C,        "snail"],
+['70_70'       => '(bah)',     'fuzzy',        0x1F411,        "black sheep"],
+['55_55'       => '(pl)',      '',     0x1F37D,        "plate"],
+['56_56'       => '(||)',      '',     0x1F35C,        "bowl"],
+['57_57'       => '(pi)',      '',     0x1F355,        "pizza"],
+['58_58'       => '(so)',      '',     0x026BD,        "soccer ball"],
+['59_59'       => '(au)',      '',     0x1F697,        "auto"],
+['60_60'       => '(ap)',      '',     0x02708,        "airplane"],
+['61_61'       => '(um)',      '',     0x02602,        "umbrella"],
+['62_62'       => '(ip)',      '',     0x1F334,        "island with a palm tree"],
+['63_63'       => '(co)',      '',     0x1F4BB,        "computer"],
+['64_64'       => '(mp)',      '',     0x1F4F1,        "mobile phone"],
+['66_66'       => '(st)',      '',     0x1F327,        "stormy cloud"],
+['73_73'       => '(li)',      '',     0x1F329,        "lightning"],
+['69_69'       => '(mo)',      '',     0x1F4B0,        "money"],
 
-# vi:ts=15
+# vi:ts=16
index bb75cefd6b9c320aaa8151e613cfa3adf4c7144a..62a8361d706fb654e778be3a936cf3fad1cc3e19 100644 (file)
 
 'part 1',
 
-1      => [':)',       'eminent v6',   0x0263A,        "happy"],
-2      => [':(',       'eminent v6',   0x02639,        "sad"],
-3      => [';)',       'eminent v6',   0x1F609,        "winking"],
-4      => [':D',       'eminent v6',   0x1F601,        "big grin"],
-5      => [';;)',      'v6',   0,      "batting eyelashes"],
-6      => ['>:D<',     'v6',   0x1F450,        "big hug"],
-7      => [':-/',      'eminent v6 fuzzy',     0x1F616,        "confused"],
-8      => [':x',       'v6',   0x1F60D,        "love struck"],
-9      => [':">',      'eminent v6',   0x1F633,        "blushing"],
-10     => [':P',       'eminent v6',   0x1F61C,        "tongue"],
-11     => [':-*',      'eminent v6',   0x1F61A,        "kiss"],
-12     => ['=((',      'v6',   0x1F494,        "broken heart"],
-13     => [':-O',      'eminent v6',   0x1F632,        "surprise"],
-14     => ['X(',       'eminent v6',   0x1F620,        "angry"],
-15     => [':>',       'eminent v6 fuzzy',     0x1F624,        "smug"], # triumph
-16     => ['B-)',      'eminent v6',   0x1F60E,        "cool"],
-17     => [':-S',      'eminent v6',   0x1F628,        "worried"],
-18     => ['#:-S',     'v6',   0x1F623,        "whew!"], # U+1F60C is too happy
-19     => ['>:)',      'eminent v6',   0x1F608,        "devil"],
-20     => [':((',      'eminent v6',   0x1F62D,        "crying"],
-21     => [':))',      'eminent v6',   0,      "laughing"],
+[1     => ':)',        'eminent v6',   0x0263A,        "happy"],
+[2     => ':(',        'eminent v6',   0x02639,        "sad"],
+[3     => ';)',        'eminent v6',   0x1F609,        "winking"],
+[4     => ':D',        'eminent v6',   0x1F601,        "big grin"],
+[5     => ';;)',       'v6',   0,      "batting eyelashes"],
+[6     => '>:D<',      'v6',   0x1F450,        "big hug"],
+[7     => ':-/',       'eminent v6 fuzzy',     0x1F616,        "confused"],
+[8     => ':x',        'v6',   0x1F60D,        "love struck"],
+[9     => ':">',       'eminent v6',   0x1F633,        "blushing"],
+[10    => ':P',        'eminent v6',   0x1F61C,        "tongue"],
+[11    => ':-*',       'eminent v6',   0x1F61A,        "kiss"],
+[12    => '=((',       'v6',   0x1F494,        "broken heart"],
+[13    => ':-O',       'eminent v6',   0x1F632,        "surprise"],
+[14    => 'X(',        'eminent v6',   0x1F620,        "angry"],
+[15    => ':>',        'eminent v6 fuzzy',     0x1F624,        "smug"], # triumph
+[16    => 'B-)',       'eminent v6',   0x1F60E,        "cool"],
+[17    => ':-S',       'eminent v6',   0x1F628,        "worried"],
+[18    => '#:-S',      'v6',   0x1F623,        "whew!"], # U+1F60C is too happy
+[19    => '>:)',       'eminent v6',   0x1F608,        "devil"],
+[20    => ':((',       'eminent v6',   0x1F62D,        "crying"],
+[21    => ':))',       'eminent v6',   0,      "laughing"],
 
 'part 2',
 
-22     => [':|',       'eminent v6',   0x1F610,        "straight face"],
-23     => ['/:)',      'v6',   0,      "raised eyebrows"],
-24     => ['=))',      'v6',   0,      "rolling on the floor"],
-25     => ['O:-)',     'v6',   0x1F607,        "angel"],
-26     => [':-B',      'v6 fuzzy',     0x1F453,        "nerd"],
-27     => ['=;',       'v6',   0x0270B,        "talk to the hand"],
-101    => [':-c',      '',     0,      "call me"],
-100    => [':)]',      '',     0,      "on the phone"],
-102    => ['~X(',      '',     0,      "at wits' end"],
-103    => [':-h',      '',     0x1F44B,        "wave"],
-104    => [':-t',      '',     0,      "time out"],
-105    => ['8->',      '',     0,      "day dreaming"],
-28     => ['I-)',      'v6',   0x1F62A,        "sleepy"],
-29     => ['8-|',      'v6 fuzzy',     0x1F612,        "rolling eyes"],
-30     => ['L-)',      'v6',   0,      "loser"],
-31     => [':-&',      'eminent v6 fuzzy',     0x1F637,        "sick"],
-32     => [':-$',      'v6 fuzzy',     0x1F64A,        "don't tell anyone"],
-33     => ['[-(',      'v6',   0,      "no talking"],
-34     => [':O)',      'v6',   0,      "clown"],
-35     => ['8-}',      'v6',   0,      "silly"],
-36     => ['<:-P',     'v6 fuzzy',     0x1F389,        "party"],
+[22    => ':|',        'eminent v6',   0x1F610,        "straight face"],
+[23    => '/:)',       'v6',   0,      "raised eyebrows"],
+[24    => '=))',       'v6',   0,      "rolling on the floor"],
+[25    => 'O:-)',      'v6',   0x1F607,        "angel"],
+[26    => ':-B',       'v6 fuzzy',     0x1F453,        "nerd"],
+[27    => '=;',        'v6',   0x0270B,        "talk to the hand"],
+[101   => ':-c',       '',     0,      "call me"],
+[100   => ':)]',       '',     0,      "on the phone"],
+[102   => '~X(',       '',     0,      "at wits' end"],
+[103   => ':-h',       '',     0x1F44B,        "wave"],
+[104   => ':-t',       '',     0,      "time out"],
+[105   => '8->',       '',     0,      "day dreaming"],
+[28    => 'I-)',       'v6',   0x1F62A,        "sleepy"],
+[29    => '8-|',       'v6 fuzzy',     0x1F612,        "rolling eyes"],
+[30    => 'L-)',       'v6',   0,      "loser"],
+[31    => ':-&',       'eminent v6 fuzzy',     0x1F637,        "sick"],
+[32    => ':-$',       'v6 fuzzy',     0x1F64A,        "don't tell anyone"],
+[33    => '[-(',       'v6',   0,      "no talking"],
+[34    => ':O)',       'v6',   0,      "clown"],
+[35    => '8-}',       'v6',   0,      "silly"],
+[36    => '<:-P',      'v6 fuzzy',     0x1F389,        "party"],
 
 'part 3',
 
-37     => ['(:|',      'eminent v6',   0x1F629,        "yawn"],
-38     => ['=P~',      'v6',   0x1F60B,        "drooling"],
-39     => [':-?',      'eminent v6',   0x1F614,        "thinking"],
-40     => ['#-o',      'v6',   0,      "d'oh"],
-41     => ['=D>',      'v6',   0x1F44F,        "applause"],
-42     => [':-SS',     'v6',   0,      "nail biting"],
-43     => ['@-)',      'v6',   0,      "hypnotized"],
-44     => [':^o',      'v6',   0,      "liar"],
-45     => [':-w',      'v6',   0,      "waiting"],
-46     => [':-<',      'v6',   0,      "sigh"],
-47     => ['>:P',      'v6',   0,      "phbbbbt"],
-48     => ['<):)',     'v6',   0,      "cowboy"],
-109    => ['X_X',      '',     0x1F648,        "I don't want to see"],
-110    => [':!!',      '',     0,      "hurry up!"],
-111    => ['\m/',      '',     0,      "rock on!"],
-112    => [':-q',      '',     0x1F44E,        "thumbs down"],
-113    => [':-bd',     '',     0x1F44D,        "thumbs up"],
-114    => ['^#(^',     '',     0,      "it wasn't me"],
-pirate_2       => [':ar!',     'ext',  0,      "pirate"],
+[37    => '(:|',       'eminent v6',   0x1F629,        "yawn"],
+[38    => '=P~',       'v6',   0x1F60B,        "drooling"],
+[39    => ':-?',       'eminent v6',   0x1F614,        "thinking"],
+[40    => '#-o',       'v6',   0,      "d'oh"],
+[41    => '=D>',       'v6',   0x1F44F,        "applause"],
+[42    => ':-SS',      'v6',   0,      "nail biting"],
+[43    => '@-)',       'v6',   0,      "hypnotized"],
+[44    => ':^o',       'v6',   0,      "liar"],
+[45    => ':-w',       'v6',   0,      "waiting"],
+[46    => ':-<',       'v6',   0,      "sigh"],
+[47    => '>:P',       'v6',   0,      "phbbbbt"],
+[48    => '<):)',      'v6',   0,      "cowboy"],
+[109   => 'X_X',       '',     0x1F648,        "I don't want to see"],
+[110   => ':!!',       '',     0,      "hurry up!"],
+[111   => '\m/',       '',     0,      "rock on!"],
+[112   => ':-q',       '',     0x1F44E,        "thumbs down"],
+[113   => ':-bd',      '',     0x1F44D,        "thumbs up"],
+[114   => '^#(^',      '',     0,      "it wasn't me"],
+[pirate_2      => ':ar!',      'ext',  0,      "pirate"],
 
 # http://messenger.yahoo.com/features/hiddenemoticons/
 
 'hidden 1',
 
-108    => [':o3',      'hidden fuzzy', 0x1F436,        "puppy dog eyes"],
-106    => [':-??',     'hidden',       0,      "I don't know"],
-107    => ['%-(',      'hidden',       0x1F649,        "not listening"],
-49     => [':@)',      'hidden v6',    0x1F437,        "pig"],
-50     => ['3:-O',     'hidden v6',    0x1F42E,        "cow"],
-51     => [':(|)',     'hidden v6',    0x1F435,        "monkey"],
-52     => ['~:>',      'hidden v6',    0x1F414,        "chicken"],
-53     => ['@};-',     'hidden v6',    0x1F339,        "rose"],
-54     => ['%%-',      'hidden v6',    0x1F340,        "good luck"],
-55     => ['**==',     'hidden v6',    0x02690,        "flag"],
-56     => ['(~~)',     'hidden v6',    0x1F383,        "pumpkin"],
-57     => ['~O)',      'hidden v6',    0x02615,        "coffee"],
-58     => ['*-:)',     'hidden v6',    0x1F4A1,        "idea"],
+[108   => ':o3',       'hidden fuzzy', 0x1F436,        "puppy dog eyes"],
+[106   => ':-??',      'hidden',       0,      "I don't know"],
+[107   => '%-(',       'hidden',       0x1F649,        "not listening"],
+[49    => ':@)',       'hidden v6',    0x1F437,        "pig"],
+[50    => '3:-O',      'hidden v6',    0x1F42E,        "cow"],
+[51    => ':(|)',      'hidden v6',    0x1F435,        "monkey"],
+[52    => '~:>',       'hidden v6',    0x1F414,        "chicken"],
+[53    => '@};-',      'hidden v6',    0x1F339,        "rose"],
+[54    => '%%-',       'hidden v6',    0x1F340,        "good luck"],
+[55    => '**==',      'hidden v6',    0x02690,        "flag"],
+[56    => '(~~)',      'hidden v6',    0x1F383,        "pumpkin"],
+[57    => '~O)',       'hidden v6',    0x02615,        "coffee"],
+[58    => '*-:)',      'hidden v6',    0x1F4A1,        "idea"],
 
 'hidden 2',
 
-59     => ['8-X',      'hidden v6',    0x1F480,        "skull"],
-60     => ['=:)',      'hidden v6',    0x1F41C,        "bug"],
-61     => ['>-)',      'hidden v6',    0x1F47D,        "alien"],
-62     => [':-L',      'hidden v6',    0x1F612,        "frustrated"],
-63     => ['[-O<',     'hidden v6',    0x1F64F,        "praying"],
-64     => ['$-)',      'hidden v6',    0,      "money eyes"],
-65     => [':-"',      'hidden v6',    0x0266B,        "whistling"],
-66     => ['b-(',      'hidden v6',    0,      "feeling beat up"],
-67     => [':)>-',     'hidden v6',    0x0262E,        "peace sign"], # U+270C
-68     => ['[-X',      'hidden v6',    0,      "shame on you"],
-69     => ['\:D/',     'hidden v6',    0x1F483,        "dancing"],
+[59    => '8-X',       'hidden v6',    0x1F480,        "skull"],
+[60    => '=:)',       'hidden v6',    0x1F41C,        "bug"],
+[61    => '>-)',       'hidden v6',    0x1F47D,        "alien"],
+[62    => ':-L',       'hidden v6',    0x1F612,        "frustrated"],
+[63    => '[-O<',      'hidden v6',    0x1F64F,        "praying"],
+[64    => '$-)',       'hidden v6',    0,      "money eyes"],
+[65    => ':-"',       'hidden v6',    0x0266B,        "whistling"],
+[66    => 'b-(',       'hidden v6',    0,      "feeling beat up"],
+[67    => ':)>-',      'hidden v6',    0x0262E,        "peace sign"], # U+270C
+[68    => '[-X',       'hidden v6',    0,      "shame on you"],
+[69    => '\:D/',      'hidden v6',    0x1F483,        "dancing"],
 
 'hidden 3',
 
-70     => ['>:/',      'hidden v6',    0,      "bring it on"],
-71     => [';))',      'hidden v6',    0x1F60F,        "hee hee"],
-76     => [':-@',      'hidden v6',    0,      "chatterbox"],
-77     => ['^:)^',     'hidden v6',    0x1F647,        "not worthy"],
-78     => [':-j',      'hidden v6',    0,      "oh go on"],
-79     => ['(*)',      'hidden v6',    0x02606,        "star"],
-72     => ['o->',      'hidden v6',    0,      "hiro"],
-73     => ['o=>',      'hidden v6',    0,      "billy"],
-74     => ['o-+',      'hidden v6',    0,      "april"],
-75     => ['(%)',      'hidden v6',    0x0262F,        "yin yang"],
-115    => [':bz',      'hidden',       0x1F41D,        "bee"],
-transformer    => ['[..]',     'hidden ext',   0,      "transformer"],
+[70    => '>:/',       'hidden v6',    0,      "bring it on"],
+[71    => ';))',       'hidden v6',    0x1F60F,        "hee hee"],
+[76    => ':-@',       'hidden v6',    0,      "chatterbox"],
+[77    => '^:)^',      'hidden v6',    0x1F647,        "not worthy"],
+[78    => ':-j',       'hidden v6',    0,      "oh go on"],
+[79    => '(*)',       'hidden v6',    0x02606,        "star"],
+[72    => 'o->',       'hidden v6',    0,      "hiro"],
+[73    => 'o=>',       'hidden v6',    0,      "billy"],
+[74    => 'o-+',       'hidden v6',    0,      "april"],
+[75    => '(%)',       'hidden v6',    0x0262F,        "yin yang"],
+[115   => ':bz',       'hidden',       0x1F41D,        "bee"],
+[transformer   => '[..]',      'hidden ext',   0,      "transformer"],
 
 # http://www.wackyb.co.nz/Archive_Yahoo_Messenger_Smiley_History/
 
index 2a546b406efe045351345a9e558852ba7735861a..63572c727c2a97dad467d5d0b528a4d41ac09273 100644 (file)
--- a/emoji.plp
+++ b/emoji.plp
@@ -2,7 +2,7 @@
 
 Html({
        title => 'emoji cheat sheet',
-       version => '1.0',
+       version => '1.1',
        description => [
                "Emoticons overview and Unicode equivalents"
                . " of MSN, Y!M, and Gmail icons.",
@@ -12,6 +12,7 @@ Html({
                chat im messenger msn yahoo ym gmail google
        '],
        stylesheet => [qw'light'],
+       data => ['emoji-gmail.inc.pl'],
 });
 
 :>
@@ -21,21 +22,20 @@ Html({
 say '<div class="section">';
 
 for my $system (qw'gmail msn yahoo') {
-       my @info = do "emoji-$system.inc.pl";
-       my $meta = shift @info or die $@;
-       ref $meta eq 'HASH' or die "invalid $system definitions";
+       my @info = Data("emoji-$system");
+       my $meta = shift @info;
+       ref $meta eq 'HASH' or Abort("Invalid $system definitions", 404);
        my $title = $meta->{name} // $system;
        $title = showlink($title, $_) for $meta->{source} || ();
 
        say sprintf '<div class="section"><h2>%s</h2>', $meta->{name} // $system;
        say '<table><tbody>';
-       for (my $i = 0; $i <= $#info; $i++) {
-               my $name = $info[$i];
-               unless (ref $info[$i+1] eq 'ARRAY') {
-                       say sprintf '</table><table><tbody>', $name;
+       for my $row (@info) {
+               unless (ref $row eq 'ARRAY') {
+                       say '</table><table><tbody>';
                        next;
                }
-               my ($input, $flags, $char, $desc) = @{ $info[++$i] };
+               my ($name, $input, $flags, $char, $desc) = @{$row};
                say sprintf('<tr><th><img src="%s" alt="%s"><td><kbd>%s</kbd><td>%s%s',
                        sprintf($meta->{ $flags =~ /\bext\b/ ? 'iconext' : 'icon' } // '%s', $name),
                        EscapeHTML($name),
index 3a39c2e1e71a0f199e6ae1fd22236aa8e7e369b9..701957305cdcee045ee06d6b4b0156ffaff9400d 100644 (file)
--- a/font.plp
+++ b/font.plp
@@ -4,7 +4,7 @@ my $font = $Request;
 
 Html({
        title => 'font coverage '.($font ? "for $font" : 'sheet'),
-       version => '1.2',
+       version => '1.4',
        keywords => [qw(
                unicode font glyph char character support overview cover coverage
                script block symbol sign mark reference table
@@ -14,13 +14,13 @@ Html({
 });
 
 if ($font) {
-       my ($fontmeta, @cover) = do "data/font/$font.inc.pl";
-       $fontmeta or die "Unknown font $font\n";
+       my $fontmeta = eval { Data("data/font/$font") }
+               or Abort("Unknown font $font", '404 font not found', ref $@ && $@->[1]);
 
        my $map = eval {
                $get{map} or return;
 
-               my $groupinfo = do 'data/unicode-cover.inc.pl' or die $@ || $!;
+               my $groupinfo = Data('data/unicode-cover');
 
                my ($cat, $name) = split m{/}, $get{map}, 2 or die "invalid map\n";
                if (!$name) {
@@ -39,7 +39,7 @@ if ($font) {
                }
                return \@map;
        };
-       die $@ if $@;
+       Abort($@, '404 invalid query') if $@;
 
        require Unicode::UCD;
 
@@ -52,14 +52,14 @@ if ($font) {
                return $_->[0]->[0] for Unicode::UCD::charblock(ucfirst) || ();  # block
                die "Unknown offset query '$_'\n";
        };
-       die $@ if $@;
+       Abort($@, '400 invalid offset') if $@;
 
        say "<h1>Font coverage</h1>";
        say "<h2>$_</h2>" for EscapeHTML($fontmeta->{name});
        printf("<p>Version <strong%s>%s</strong> released %s contains %d glyphs.",
                !!$_->[2] && qq( title="revision $_->[2]"),
                $_->[1], $_->[0],
-               scalar @cover,
+               scalar @{ $fontmeta->{cover} },
        ) for [
                grep { $_ }
                ($fontmeta->{date} || '?') =~ s/T.*//r,
@@ -86,7 +86,7 @@ if ($font) {
        require Shiar_Sheet::FormatChar;
        my $glyphs = Shiar_Sheet::FormatChar->new;
 
-       my %cover = map { ($_ => 1) } @cover;  # lookup map
+       my %cover = map { ($_ => 1) } @{ $fontmeta->{cover} };  # lookup map
 
        say <<"EOT";
 
@@ -156,12 +156,15 @@ EOT
                my ($class, $name, $mnem, $entity, $string) = @{$info};
                my $np = $class =~ /\bC\S\b/;  # noprint if control or invalid
                # display literal character, with placeholder circle if non-spacing/enclosing
-               my $html = ($class =~ /\bM[ne]\b/ && chr 9676) . EscapeHTML(chr $cp);
+               $string ||= ($class =~ /\bM[ne]\b/ && chr 9676) . chr($cp);
+               my $html = $np ? !!$cover{$cp} && sprintf("&#%d;", $cp) :
+                       EscapeHTML($string);
                say sprintf '<td class="%s" title="U+%04X%s">%s',
-                       !$class ? ('l0', $cp, '', '') :
+                       !$class ? ('l0', $cp, '', '') : (
                        $cover{$cp} ? $np ? 'l2' : 'l5' : $np ? 'Xi' : 'l1',
                        $cp, !!$name && ": $name",
-                       ($cover{$cp} || !$np) && $html;
+                       $html
+                       );
        }
        say '</table>';
 
@@ -180,7 +183,7 @@ Character support of Unicode
 
 <:
 
-my $cover = do 'data/unicode-cover.inc.pl' or die $@ || $!;
+my $cover = Data('data/unicode-cover');
 
 my @ossel = @{ $cover->{osdefault} };
 my @fontlist = map { @{ $cover->{os}->{$_} } } @ossel;
@@ -218,7 +221,7 @@ my @rows = (
 
 if (my $group = $get{q}) {
        my $grouprows = $cover->{$group}
-               or die "Unknown character category $_\n";
+               or Abort("Unknown character category $_", 404);
        @rows = map { "$group/$_" } sort keys %{$grouprows};
 }
 
diff --git a/graffiti.ttf b/graffiti.ttf
new file mode 100644 (file)
index 0000000..303498d
Binary files /dev/null and b/graffiti.ttf differ
index 81895192ba172dcd937df77c9980da8794cf214c..f05c02afb49d372b0e235b5b83802697f0a941cc 100644 (file)
--- a/index.plp
+++ b/index.plp
@@ -2,7 +2,8 @@
 
 Html({
        title => 'cheat sheets',
-       version => '1.10',
+       canonical => '/',
+       version => '1.18',
        description => [
                "Cheat sheets summarising various software programs and standards.",
        ],
@@ -15,8 +16,8 @@ Html({
                ' title="RSS feed of repository updates"',
                ' href="http://git.shiar.nl/sheet.git/rss">',
        ],
+       data => ['UPDATE'],
 });
-
 :>
 <h1>Shiar's cheat sheets</h1>
 
@@ -28,12 +29,15 @@ Originally created by Mischa <span class="family-name">Poslawsky</span>,
 but you're free to use, print, alter, and redistribute under the AGPL license.
 </p>
 <:
-my @format = ('--date=short', "--pretty=%ad (%ar)\t%s");
-if (open my $log, '-|', git => 'log', -1, @format) {{
+if (open my $log, '<', 'UPDATE') {{
        my $line = readline $log;
        $line or next;  # explicitly ignore empty input
+       EscapeHTML $line;
        my ($date, $subject) = split /[\t\n]/, $line;
        $date =~ s/ \K/<small>/ and $date .= '</small>';
+       $subject =~ s{\A (\w+) (?= (?:/\w+)* :\h )}{
+               showlink($1, -e "$1.plp" && "/$1");
+       }ex;
        say "<p><strong>Last update</strong>: $date $subject</p>";
 }}
 
@@ -49,7 +53,8 @@ if (open my $log, '-|', git => 'log', -1, @format) {{
 <li><a href="/vimperator">vimperator</a>
 <li><a href="/mutt">mutt</a>
 <li><a href="/nethack">nethack</a>
-<li><a href="/mplayer">mplayer</a>
+<li><a href="/mplayer/mpv">mplayer/mpv</a>
+<li><a href="/keyboard/altgr">altgr/option</a>
 </ul>
 </div>
 
@@ -75,9 +80,9 @@ if (open my $log, '-|', git => 'log', -1, @format) {{
 <li><a href="/perl">perl versions</a>
 <li><a href="/apl">apl symbols</a>
 <li><a href="/termcol">terminal colours</a>
-<li><a href="/sc/2">starcraft 2 units</a>
-       (<a href="/sc" title="StarCraft: Brood War">bw</a>)
+<li><a href="/sc/lotv">starcraft units</a>
 <li><a href="/emoji">emoticons</a>
+<li><a href="/dieren">dieren (Dutch animals)</a>
 </ul>
 </div>
 </nav>
index 2fe9c073bf656fc2db61df1a262b468d05343b56..fa2aaf25d19ef2c7c0c461cd5e8e29cbb43b44c0 100644 (file)
@@ -1,67 +1,84 @@
 <(common.inc.plp)><:
 
+$Request ||= 'altgr/windows';
+my $mode = lc $Request;
+my $include = "keyboard/$mode.eng";
+
+if (-e (my $page = "keyboard/$Request/index.inc.plp")) {
+       Include $page;
+       exit;
+}
+
+my $info = eval { Data($include) } || {};
+warn "error in $include: ", @{$@} if ref $@;
+$mode = $info->{title} // $mode;
+
+my $showkeys //= !exists $get{keys} ? undef :
+       ($get{keys} ne '0' && ($get{keys} || 'always'));
+my @keystyle = (
+       '<!--[if lte IE 6]><style> .help dl.legend dt {margin:0 0 1px} </style><![endif]-->',
+       '<!--[if lte IE 7]><style> .help dl.legend dd {float:none} </style><![endif]-->',
+       !$showkeys ? '<style> .no {visibility:hidden} </style>' :
+       $showkeys eq 'ghost' ? '<style> .no, .alias {opacity:.5} </style>' : (),
+       '<script type="text/javascript" src="/keys.js?1.6" async></script>',
+);
+
 Html({
-       title => 'keyboard cheat sheet',
-       version => '1.0',
+       title => "\L$mode\E keyboard cheat sheet",
+       version => $info->{version} || '0.1',
+       canonical => -e "$Request.plp" ? "/$Request" : undef, # historic shorthand
+       description => $info->{description} //
+               ["Keyboard cheat sheet for the default controls of $mode."],
+       keywords => [@{ $info->{keywords} // [] }, qw'
+               sheet cheat reference overview keyboard control commands shortkey
+       '],
+       image => $info->{image},
        stylesheet => [qw( light dark circus mono red )],
-       keys => 1,
+       data => ["$include.inc.pl"],
+       raw => \@keystyle,
 });
 
-:>
-<h1>keyboard cheat sheet</h1>
+%{$info} or Abort(
+       "Requested keyboard <q>$mode</q> not available",
+       '404 request not found',
+);
 
-<h2>normal mode (default)</h2>
+say "<h1>$mode keyboard</h1>";
+say "<p>$_</p>" for $info->{intro} // ();
+say "<h2>", $info->{mode}->{''}, " (default)</h2>"
+       if $info->{mode} and %{ $info->{mode} } > 1;
 
-<:
-use Shiar_Sheet::Keyboard 2.07;
-use Unicode::Normalize qw( NFKD );
-use Text::Unidecode qw( unidecode );
+use Shiar_Sheet::Keyboard 2.08;
+my $keys = Shiar_Sheet::Keyboard->new($info);
+$keys->map($get{map}) or undef $get{map};
+$keys->print_rows($get{rows} || $info->{moderows}, $info->{rows});
 
-my @usintrows = (
-       [  'a' ..                                            'z'],
-       [qw(Á B ¢ Ð É F G H Í J Œ Ø µ Ñ Ó Ö Ä ® § Þ Ú V Å X Ü Æ)],
-       [qw(á b © ð é f g h í j œ ø µ ñ ó ö ä ® ß þ ú v å x ü æ)],
-       [qw(Å ı Ç ð ´ ̉ ˝ ̣ ˆ ½  Þ ¾ ˜ Ø ∏ Œ ‰ / ˇ ¨ ◊ „ ˛ ¼ ¸)],
-       [qw(å ∫ ç ∂ ́ ƒ © ˙ ̂ ∆ ° ¬ µ ̃ ø π œ ® ß † ̈ √ ∑ ≈ ¥ Ω)],
-);
-my @usint = (
-       map {
-               my $c = $_;
-               [ map { $usintrows[$_]->[$c] } 0 .. 2 ]
-       } 0 .. $#{ $usintrows[0] }
-);
+{
+       say "<hr/>\n";
+       say '<div class="help">';
 
-my $keys = Shiar_Sheet::Keyboard->new({
-       def => {
-               '' => {
-                       map {
-                               my @row = @{$_};
-                               my $class = (
-                                         !defined $row[2] || $row[0] eq $row[2] ? 1 # identical
-                                       : NFKD($row[2]) =~ $row[0] ? 2 # decomposed equivalent
-                                       : $row[2] =~ /^\p{Latin}/ ? 4 # latin script
-                                       : unidecode($row[2]) =~ /^\W*\Q$row[0]/ ? 5 # transliterated
-                                       : $row[2] =~ /^\p{Mn}/ ? 8 # combining accent
-                                       : 7
-                               );
-                               $row[0] => "g$class"
-                       } @usint
-               },
-       },
-       key => {
-               map {
-                       $_->[0] => "$_->[1]<br>$_->[2]"
-               } @usint
-       },
-       flag => {
-               g1 => ['unaltered'],
-               g2 => ['accented'],
-               g4 => ['latin'],
-               g7 => ['other'],
-               g8 => ['combining'],
-       },
-});
-$keys->map($get{map}) or undef $get{map};
-$keys->print_rows($get{rows}, [0]);
-$keys->print_legends(\%get);
+       use List::MoreUtils qw( part );
+       my @gflags = part {/^g\d/} sort keys %{ $keys->{flag} };
+
+       say "\t", '<div class="left">';
+       $keys->print_legend('legend-types', $gflags[1]);
+       say "\t</div>\n";
+
+       say "\t", '<div class="right">';
+       $keys->print_legend('legend-options', $gflags[0]);
+       say '';
+
+       say "\t\t", '<ul class="legend legend-set">';
+
+       say "\t\t<li>keyboard <strong>map</strong> is ",
+               ($get{map} ? 'set to ' : ''), "<em>$keys->{map}</em>";
+       say "\t\t<li><strong>keys</strong> are ",
+               "<em>", ($showkeys ? 'always shown' : 'hidden if unassigned'), "</em>",
+               (!defined $showkeys && ' by default');
+       say "\t\t<li>default <strong>style</strong> is ",
+               (defined $get{style} && 'set to '), "<em>$style</em>";
 
+       say "\t\t</ul>";
+       say "\t</div>\n";
+       say "</div>\n";
+}
diff --git a/keyboard/altgr/apl.eng.inc.pl b/keyboard/altgr/apl.eng.inc.pl
new file mode 100644 (file)
index 0000000..c796cb3
--- /dev/null
@@ -0,0 +1,117 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbchars';
+
+my %dyalogx = (
+       'Q' => '⍰',
+       'R' => '⌾',
+       'G' => '⍢',
+       'B' => '⍭',
+       'N' => '⍡',
+       'M' => '∥',
+);
+my %rows = (
+       '~' => '⌺',
+       '!' => '⌶',
+       '@' => '⍫',
+       '#' => '⍒',
+       '$' => '⍋',
+       '%' => '⌽',
+       '^' => '⍉',
+       '&' => '⊖',
+       '*' => '⍟',
+       '(' => '⍱',
+       ')' => '⍲',
+       '_' => '!',
+       '+' => '⌹',
+       '`' => '⋄',
+       '1' => '¨',
+       '2' => '¯',
+       '3' => '<',
+       '4' => '≤',
+       '5' => '=',
+       '6' => '≥',
+       '7' => '>',
+       '8' => '≠',
+       '9' => '∨',
+       '0' => '∧',
+       '-' => '×',
+       '=' => '÷',
+       'E' => '⍷',
+       'T' => '⍨',
+       'I' => '⍸',
+       'O' => '⍥',
+       'P' => '⍣',
+       '{' => '⍞',
+       '}' => '⍬',
+       '|' => '⊣',
+       'q' => '?',
+       'w' => '⍵',
+       'e' => '∊',
+       'r' => '⍴',
+       't' => '∼', # ~
+       'y' => '↑',
+       'u' => '↓',
+       'i' => '⍳',
+       'o' => '○',
+       'p' => '⋆', # *
+       '[' => '←',
+       ']' => '→',
+       '\\'=> '⊢',
+       'J' => '⍤',
+       'K' => '⌸',
+       'L' => '⌷',
+       ':' => '≡',
+       '"' => '≢',
+       'a' => '⍺',
+       's' => '⌈',
+       'd' => '⌊',
+       'f' => '_',
+       'g' => '∇',
+       'h' => '∆',
+       'j' => '∘',
+       'k' => "'",
+       'l' => '⎕',
+       ';' => '⍎',
+       "'" => '⍕',
+       'Z' => '⊆',
+       '<' => '⍪',
+       '>' => '⍙',
+       '?' => '⍠',
+       'z' => '⊂',
+       'x' => '⊃',
+       'c' => '∩',
+       'v' => '∪',
+       'b' => '⊥',
+       'n' => '⊤',
+       'm' => '|',
+       ',' => '⍝',
+       '.' => '⍀',
+       '/' => '⌿',
+       %dyalogx,
+);
+
+my $groups = kbchars(\%rows);
+$groups->{def}{''}{$_} .= ' ext' for keys %dyalogx;
+$groups->{flag}{ext} = ['extended', 'optional operators not available in all variants'];
+
++{
+       %{$groups},
+       version => '1.0',
+       title => 'APL',
+       category => 'specialised',
+       intro => join("\n",
+               'Resulting <a href="/charset">Unicode</a> characters',
+               'of a typical <a href="/apl">APL</a> keyboard layout',
+               'derived from IBM System/360 terminals.',
+               'Usually obtained by prefixing <code>`</code> (Dyalog)',
+               'and/or pressing AltGr (APLX).',
+       ),
+       description => [
+               'Typical IBM-derived APL keyboard layout,',
+               'as found in APLX and Dyalog implementations.',
+       ],
+       image => 'data/keyboard/thumb/unicomp-apl.jpg',
+       imagealt => 'Alt on a custom keyboard with APL labels',
+}
diff --git a/keyboard/altgr/boyeg.eng.inc.pl b/keyboard/altgr/boyeg.eng.inc.pl
new file mode 100644 (file)
index 0000000..63bfa49
--- /dev/null
@@ -0,0 +1,95 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbchars';
+
+my %rows = (
+       '~' => "\N{COMBINING CEDILLA}",
+       '`' => '§',
+       '!' => "\N{COMBINING GRAVE ACCENT}",
+       '1' => '¬',
+       '@' => "\N{COMBINING ACUTE ACCENT}",
+       '2' => '¤',
+       '#' => "\N{COMBINING VERTICAL LINE BELOW}",
+       '3' => '₵',
+       '$' => '€',
+       '4' => '£',
+       '%' => '°', # assume misaligned
+       '5' => '₦',
+       '^' => "\N{COMBINING DOT ABOVE}",
+       '6' => "\N{COMBINING DOT BELOW}",
+       '&' => "\N{COMBINING DIAERESIS}",
+       '7' => "\N{COMBINING DIAERESIS BELOW}",
+       '*' => "\N{COMBINING TILDE}",
+       '8' => "\N{COMBINING TILDE BELOW}",
+       '(' => "\N{COMBINING CIRCUMFLEX ACCENT}",
+       '9' => "\N{COMBINING CIRCUMFLEX ACCENT BELOW}",
+       ')' => "\N{COMBINING CARON}",
+       '0' => "\N{COMBINING CARON BELOW}",
+       '-' => "\N{COMBINING MACRON BELOW}",
+       '_' => "\N{COMBINING MACRON}",
+       '+' => "\N{COMBINING BRIDGE ABOVE}",
+       '=' => "\N{COMBINING BRIDGE BELOW}",
+
+       'Q' => 'Ɵ',
+       'W' => 'Ɛ',
+       'E' => 'Ǝ',
+       'R' => 'Ɍ',
+       'T' => 'Ʈ',
+       'Y' => 'Ƴ', # subtle
+       'U' => 'Ʊ',
+       'I' => 'Ɨ',
+       'O' => 'Ɖ',
+       'P' => 'Ƥ', # different lowercase
+       '{' => '¶',
+       '}' => 'μ',
+
+       'A' => 'Ʌ',
+       'S' => 'Ʃ',
+       'D' => 'Ɗ',
+       'F' => 'Ƒ',
+       'G' => 'Ɂ', # probably caseless ʔ
+       'H' => 'Ħ', # different uppercase
+       'J' => 'Ɉ', # lowercase shown dotless
+       'K' => 'Ƙ', # subtle
+       'L' => 'Ɩ',
+       ':' => 'Œ',
+       ';' => 'œ',
+
+       'Z' => 'Ʒ',
+       'X' => 'Ɣ',
+       'C' => 'Ɔ',
+       'V' => 'Ʋ',
+       'B' => 'Ɓ',
+       'N' => 'Ŋ',
+       'M' => 'Ɲ',
+       '<' => '«',
+       '>' => '»',
+       '?' => 'Æ',
+       '/' => 'æ',
+);
+
+$rows{lc $_} //= lc $rows{$_} for 'A'..'Z';
+
+my $groups = kbchars(\%rows);
+
++{
+       %{ $groups },
+       version => '1.0',
+       title => 'Boyeg',
+       category => 'latin',
+       intro => join("\n",
+               'Commercial product by',
+               '<a href="https://keyboardafrica.com/" target="_blank">Keyboard Africa</a>',
+               'providing <a href="/unicode">Unicode</a> characters',
+               'for various African languages while pressing',
+               # Ewe, Baoule, Akan, Dagbani, Hausa, Temme, Ewondo, Igbo, Fon, Wolof, Fulfide, Berber, IIshan, Fula, Dyula, Yoruba, Itsekiri, Konkomba, Kanuri, Dan, Luhya, Bukusu, Gusii, Meru, Kikuyu, English, French, Spanish and more
+               '<em title="additional key left of right Ctrl">Fn</em>.',
+       ),
+       description => [
+               "Boyeg Keyboard layout table with its Fn modifier key",
+               "providing latin letters and accents for various African languages.",
+       ],
+       image => 'data/keyboard/thumb/boyeg.jpg',
+       imagealt => 'Fn on the Boyeg Office Keyboard',
+}
diff --git a/keyboard/altgr/drix.eng.inc.pl b/keyboard/altgr/drix.eng.inc.pl
new file mode 100644 (file)
index 0000000..c36f53b
--- /dev/null
@@ -0,0 +1,125 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbchars';
+
+my %rows = (
+       '!' => "\N{COMBINING GRAVE ACCENT}",
+       '#' => "\N{COMBINING CIRCUMFLEX ACCENT}",
+       '$' => "\N{COMBINING DIAERESIS}",
+       '%' => "\N{COMBINING RING ABOVE}",
+       '&' => "\N{COMBINING CEDILLA}",
+       '~' => "\N{COMBINING TILDE}",
+       '(' => "\N{COMBINING CARON}",
+       ')' => "\N{COMBINING DOUBLE ACUTE ACCENT}",
+       '*' => "\N{COMBINING OGONEK}",
+       '+' => "\x{2260}",
+       '-' => "\"",
+       '0' => "\x{2070}",
+       '1' => "\xB9",
+       '2' => "\xB2",
+       '3' => "\xB3",
+       '4' => "\x{2074}",
+       '5' => "\x{2075}",
+       '6' => "\x{2076}",
+       '7' => "\x{2077}",
+       '8' => "\x{2078}",
+       '9' => "\x{2079}",
+       '=' => "'",
+       '@' => "\N{COMBINING ACUTE ACCENT}",
+       '[' => "{",
+       ']' => "}",
+       '^' => "\N{COMBINING MACRON}",
+       '_' => "\xB1",
+       '`' => "\xA3",
+       'A' => "\x{2190}",
+       'a' => "\xE6",
+       'B' => "\x{20bf}",
+       'b' => "\x{2642}",
+       'c' => "\xA9",
+       'C' => "\xA2",
+       'd' => "\x{394}",
+       'D' => "\x{2192}",
+       'e' => "\x{20AC}",
+       'E' => "\x{20AC}",
+       'f' => "\x{192}",
+       'F' => "\x{191}",
+       #'g' => "g",
+       #'G' => "G",
+       #'H' => "H",
+       #'h' => "h",
+       'i' => "\x{2018}",
+       'I' => "\x{2019}",
+       'j' => "\x{201C}",
+       'J' => "\x{201D}",
+       'k' => "\x{201A}",
+       'K' => "\x{201E}",
+       'L' => "\x{3BB}",
+       'l' => "\xA3",
+       #'M' => "M",   # at :
+       'm' => "\xB5", # at ;
+       #'N' => "N",
+       'n' => "\x{26a5}",
+       'o' => "\x{153}",
+       'O' => "\x{3A9}",
+       'P' => "\x{20b1}",
+       'p' => "\x{3C0}",
+       'q' => "\xF8",
+       'Q' => "\xD8",
+       'R' => "\x{20bd}",
+       'r' => "\xAE",
+       's' => "\xDF",
+       'S' => "\x{2193}",
+       'T' => "\x{3C4}",
+       't' => "\x{2122}",
+       #'u' => "u",
+       #'U' => "U",
+       #'V' => "V",
+       'v' => "\x{2640}",
+       'W' => "\x{2191}",
+       #'w' => "w",
+       'x' => "\xD7",
+       'X' => "\xF7",
+       'y' => "\xA5",
+       'Y' => "\xA5",
+       'z' => "\xA7",
+       'Z' => "\xB6",
+       '{' => "\xAB",
+       '}' => "\xBB",
+
+       '"' => "\x{2030}", # unshifted %
+       "'" => "~",        # unshifted #
+       #'|' => '×',       # unshifted & at BKSL same as x
+       #'\\'=> '$',       # unshifted * at bksl
+       '|' => "≤",        # unshifted < at LSGT
+       '\\'=> "≥",        # unshifted > at lsgt
+       '<' => "\xB7",     # unshifted ,
+       ',' => "\x{2026}", # unshifted .
+       '>' => "\xB4",     # unshifted ;
+       '.' => "`",        # unshifted :
+       '?' => "\xA6",     # unshifted \
+       '/' => "|",
+       ':' => "\xBF",     # unshifted ? at M
+       ';' => "\xA1",     # unshifted ! at m
+);
+
+my $groups = kbchars(\%rows);
+$groups->{def}{''}{$_} .= ' ext' for qw( ` E Y );
+
++{
+       %{$groups},
+       version => '1.0',
+       title => 'Drix',
+       category => 'latin/xorg',
+       intro => join("\n",
+               "European Latin layout version 3.1 providing",
+               '<a href="/unicode">Unicode</a> characters while pressing AltGr,',
+               'developed by Jerome Leclanche',
+               'for <abbr title="distributed with X since 2019">Linux</abbr>.',
+       ),
+       description => [
+               "Drix EU Latin keyboard layout table",
+               "with the AltGr modifier key: provides miscellaneous symbols",
+               "and accents for some European languages.",
+       ],
+}
diff --git a/keyboard/altgr/emojiworks.eng.inc.pl b/keyboard/altgr/emojiworks.eng.inc.pl
new file mode 100644 (file)
index 0000000..dca101c
--- /dev/null
@@ -0,0 +1,99 @@
+use utf8;
+use strict;
+use warnings;
+no warnings 'qw';
+use Shiar_Sheet::KeyboardChars 'kbchars';
+
+my %rows = qw(
+       ` 🤖
+       1 🦄
+       2 🔥
+       3 🎉
+       4 💰
+       ~ 🏻 ! 🏼 @ 🏽 # 🏾 $ 🏿
+       5 🎶 % 🍿
+       6 💩 ^ 🍑
+       7 🙈 & 🌭
+       8 ☀️ * ☔
+       9 👀 ( ❄️
+       0 💯 ) 🇺🇸
+       - 🔫 _ 🕊️
+       = 💁 + 🙅
+       q 👉 Q 👈 ^q ☝
+       w ✌ W 🤘 ^w ✊
+       e 🙌 E 💪 ^e 🏋
+       r 👌 R 👋 ^r 💦
+       t 👍 T 👎 ^t 👑
+       y ♥ Y 💔 ^y 💙
+       u 💕 U 💋 ^u 💏
+       i 👏 I ⚡ ^i 🎁
+       o 🙏 O 👊 ^o 🕴
+       p ✋ P 👆 ^p 🍔
+       [ 🌮 { 🍕
+       ] ☕ } 🍻
+       \ ✨ | 🌟
+       a 😍 A 😻 ^a 🏀
+       s 😘 S 👫 ^s 🏈
+       d 😳 D 😨 ^d ⚽
+       f 😜 F 😋 ^f 😝
+       g 😊 G 🤗 ^g 🏃
+       h 😂 H 😆 ^h 💐
+       j 😄 J 🙃 ^j 🌹
+       k 😉 K 👯 ^k 🌈
+       l 😌 L 💃 ^l 🍭
+       ; 😎 : 🤓
+       ' 🤔 " 🎅
+       z 😈 Z ⛄ ^z 👻
+       x 😡 X ☠️ ^x ⚔️
+       c 😱 C 🙊 ^c 👽
+       v 😬 V 👼 ^v 🛠️
+       b 😑 B 😶 ^b 😷
+       n 😒 N 🙄 ^n 😏
+       m 😢 M 👶 ^m 🐶
+       , 😭 < 😖
+       . 😔 > 😩
+       / 😴 ? 😞
+);
+
+my $groups = kbchars(\%rows);
+
+while (my ($k, $c) = each %rows) {
+       # override letter-based classes by unicode versions
+       $groups->{def}{''}{$k} = (
+               $c =~ /\p{General_Category=Modifier_Symbol}/ ? 'g9' :
+               $c =~ /\p{In=1.1}/ ? 'g2' :
+               $c =~ /\p{In=5.2}/ ? 'g3' :
+               $c =~ /\p{In=6.0}/ ? 'g4' :
+               $c =~ /\p{In=7.0}/ ? 'g5' :
+               $c =~ /\p{In=8.0}/ ? 'g7' :
+               'g0' # unexpectedly newer
+       );
+}
+
++{
+       %{$groups},
+       title => 'EmojiWorks',
+       version => '1.0',
+       category => 'legacy/emoji',
+       tableclass => 'keys big',
+       intro => join("\n",
+               "Commercial product from 2015 (no longer available)",
+               "with <em>emoji</em> (Alt) buttons",
+               "to insert various Unicode emoticons and other symbols.",
+       ),
+       description => [
+               "Legacy EmojiWorks keyboard layout",
+               "for typing a selection of Unicode emoji symbols.",
+       ],
+       rows => [2, 1, 0],
+       moderows => '321-21',
+       image => 'data/keyboard/thumb/emojiworks.jpg',
+       flag => {
+               g2 => ['legacy'   => "Already in Unicode 1.1 released in 1993 as text symbols"],
+               g3 => ['predated' => "Updates up to Unicode 5.2 between, retroactively emojified"],
+               g4 => ['first'    => "Initial emoji support with Unicode 6.0 in 2010"],
+               g5 => ['update'   => "Extensions in Unicode 6.1 and 7.0 (2014)"],
+               g7 => ['latest'   => "Added in Unicode 8.0, in 2015 when these characters were selected"],
+               g9 => ['modifier' => "Fitzpatrick skin colour selection marks in Unicode 8.0"],
+       },
+}
diff --git a/keyboard/altgr/eurkey.eng.inc.pl b/keyboard/altgr/eurkey.eng.inc.pl
new file mode 100644 (file)
index 0000000..5b9702c
--- /dev/null
@@ -0,0 +1,296 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbmodes';
+
+my $V = v1.3;
+
+my $presymbol = $V ge v1.3 ? '\\' : '-';
+my %rows = (
+       '' => {
+       '1' => '¡',
+       '!' => '¹',
+       '2' => 'ª',
+       '@' => '²',
+       '3' => 'º',
+       '#' => '³',
+       '4' => $V ge v1.1 ? '£' : '€',
+       '$' => '¥',
+       '5' => $V ge v1.1 ? '€' : '£',
+       '%' => '¢',
+       '6' => "\N{COMBINING CIRCUMFLEX ACCENT}",
+       '^' => "\N{COMBINING CARON}",
+       '7' => "\N{COMBINING RING ABOVE}",
+       '&' => "\N{COMBINING MACRON}",
+       '8' => "\N{DOUBLE LOW-9 QUOTATION MARK}",
+       '*' => "\N{SINGLE LOW-9 QUOTATION MARK}",
+       '9' => "\N{LEFT DOUBLE QUOTATION MARK}",
+       '(' => "\N{LEFT SINGLE QUOTATION MARK}",
+       '0' => "\N{RIGHT DOUBLE QUOTATION MARK}",
+       ')' => "\N{RIGHT SINGLE QUOTATION MARK}",
+       '-' => $V ge v1.3 ? '✓' : '©',
+       '_' => $V ge v1.3 ? '✗' : '№',
+       '=' => '×',
+       '+' => '÷',
+       'q' => 'æ',
+       'Q' => 'Æ',
+       'w' => 'å',
+       'W' => 'Å',
+       'e' => 'ë',
+       'E' => 'Ë',
+       'r' => 'ý',
+       'R' => 'Ý',
+       't' => 'þ',
+       'T' => 'Þ',
+       'y' => 'ÿ',
+       'Y' => 'Ÿ',
+       'u' => 'ü',
+       'U' => 'Ü',
+       'i' => 'ï',
+       'I' => 'Ï',
+       'o' => 'ö',
+       'O' => 'Ö',
+       'p' => 'œ',
+       'P' => 'Œ',
+       '[' => '«',
+       '{' => '‹',
+       ']' => '»',
+       '}' => '›',
+
+       'a' => 'ä',
+       'A' => 'Ä',
+       's' => 'ß',
+       'S' => $V ge v1.3 ? 'ẞ' : '¶',
+       'd' => $V ge v1.2 ? 'đ' : 'ð',
+       'D' => $V ge v1.2 ? 'Đ' : 'Ð',
+       'f' => 'è',
+       'F' => 'È',
+       'g' => 'é',
+       'G' => 'É',
+       'h' => 'ù',
+       'H' => 'Ù',
+       'j' => 'ú',
+       'J' => 'Ú',
+       'k' => 'ij',
+       'K' => 'IJ',
+       'l' => 'ø',
+       'L' => 'Ø',
+       ';' => $V ge v1.2 ? '°' : "\N{COMBINING DIAERESIS}",
+       ':' => '·',
+       "'" => "\N{COMBINING ACUTE ACCENT}",
+       '"' => $V ge v1.2 ? "\N{COMBINING DIAERESIS}" : '†',
+       '`' => "\N{COMBINING GRAVE ACCENT}",
+       '~' => "\N{COMBINING TILDE}",
+
+       '\\'=> '¬',
+       '|' => '¦',
+       'z' => 'à',
+       'Z' => 'À',
+       'x' => 'á',
+       'X' => 'Á',
+       'c' => 'ç',
+       'C' => 'Ç',
+       'v' => 'ì',
+       'V' => 'Ì',
+       'b' => 'í',
+       'B' => 'Í',
+       'n' => 'ñ',
+       'N' => 'Ñ',
+       'm' => 'Ω',
+       'M' => '√', # ±
+       ',' => 'ò',
+       '<' => 'Ò',
+       '.' => 'ó',
+       '>' => 'Ó',
+       '/' => '¿',
+       '?' => '…',
+       },
+
+       # greek
+       'm' => {
+               'a' => 'α',
+               'b' => 'β', # v
+               'g' => 'γ',
+               'd' => 'δ',
+               'e' => 'ε',
+               'z' => 'ζ',
+               'i' => 'η',
+               'h' => 'θ',
+               'j' => 'ι',
+               'k' => 'κ',
+               'l' => 'λ',
+               'm' => 'μ',
+               'n' => 'ν',
+               'x' => 'ξ',
+               'o' => 'ο',
+               'p' => 'π',
+               'r' => 'ρ',
+               's' => 'σ',
+               't' => 'τ',
+               'y' => 'υ',
+               'f' => 'φ',
+               'c' => 'χ',
+               'w' => 'ψ',
+               'q' => 'ω', # u
+
+               # suþscript
+               '1' => '¹',
+               '2' => '²',
+               '3' => '³',
+               '4' => '⁴',
+               '5' => '⁵',
+               '6' => '⁶',
+               '7' => '⁷',
+               '8' => '⁸',
+               '9' => '⁹',
+               '0' => '⁰',
+               '!' => '₁',
+               '@' => '₂',
+               '#' => '₃',
+               '$' => '₄',
+               '%' => '₅',
+               '^' => '₆',
+               '&' => '₇',
+               '*' => '₈',
+               '(' => '₉',
+               ')' => '₀',
+       },
+
+       # maths
+       'M' => {
+               '!' => '≠',
+               '~' => '≈',
+               '=' => '≝',
+               '>' => '≥',
+               '<' => '≤',
+               '-' => '±',
+               'i' => '∞',
+               'n' => 'ⁿ',
+               'r' => '√',
+               '3' => '∛',
+               '4' => '∜',
+               '%' => '‰',
+
+               'f' => 'ƒ',
+               'S' => '∫',
+               "'" => '′',
+               '"' => '″',
+               'p' => '∂',
+               'd' => 'Δ',
+               'D' => '∇',
+               '+' => '⊕',
+               '*' => '⊗',
+               '^' => '℘',
+
+               's' => '∩',
+               'u' => '∪',
+               'U' => '∖',
+               'O' => '∅',
+               'g' => '⊂',
+               'G' => '⊃',
+               'h' => '⊄',
+               'H' => '⊅',
+               'b' => '⊆',
+               'B' => '⊇',
+               'm' => '∈',
+               'M' => '∉',
+               'k' => '∋',
+               'K' => '∌',
+
+               'A' => '∀',
+               'E' => '∃',
+               'X' => '∄',
+               '&' => '∧',
+               '|' => '∨',
+               'c' => '∝',
+               '.' => '⋅',
+               'o' => '∘',
+               ':' => '∴',
+               ';' => '∵',
+               'z' => '↯',
+               'F' => '∎',
+
+               'R' => 'ℝ',
+               'C' => 'ℂ',
+               'N' => 'ℕ',
+               'P' => 'ℙ',
+               'Q' => 'ℚ',
+               'Z' => 'ℤ',
+               '9' => '∟',
+               '8' => '∠',
+               '7' => '∡',
+               'l' => '∥',
+               'L' => '∦',
+       },
+
+       # symbols
+       $presymbol => {
+               't' => '™',
+               'c' => '©',
+               'p' => '℗',
+               'r' => '®',
+                       $V lt v1.3 ? (
+               '1' => '¼',
+               '2' => '½',
+               '3' => '¾',
+               '4' => '⅓',
+               '5' => '⅔',
+               's' => '℠',
+                       ) : (
+               's' => '§',
+               '1' => '№',
+               '2' => '½',
+               '3' => '⅓',
+               '4' => '¼',
+               '5' => '⅔',
+               '6' => '¾',
+               'T' => '℠',
+                       ),
+
+               # arrows
+               'h' => '←',
+               'H' => '⇐',
+               'j' => '↓',
+               'J' => '⇓',
+               'k' => '↑',
+               'K' => '⇑',
+               'l' => '→',
+               'L' => '⇒',
+               'u' => '↖',
+               'U' => '⇖',
+               'i' => '↗',
+               'I' => '⇗',
+               'n' => '↙',
+               'N' => '⇙',
+               'm' => '↘',
+               'M' => '⇘',
+               '=' => '↔',
+               '+' => '⇔',
+       },
+);
+
++{
+       %{ kbmodes(\%rows) },
+       mode => {
+               ''  => 'option-shifted ⌥',
+               'm' => 'Ω greek prefix ⌥m',
+               'M' => '√ maths prefix ⌥M',
+               $presymbol => "$rows{''}{$presymbol} symbol prefix ⌥$presymbol",
+       },
+       version => '1.1',
+       title => 'EurKEY',
+       category => 'latin/thirdparty',
+       intro => join("\n",
+               'Third-party proposal <a href="https://eurkey.steffen.bruentjen.eu/">EurKEY</a>',
+               '<abbr title="last updated 2017-05-15">v1.3</abbr> by Steffen Brüntjen',
+               'supporting most European languages while pressing AltGr or ⌥ Option.',
+               'Selectable in <abbr title="distributed with X since 2014">Linux</abbr>',
+               'and available for <a href="/keyboard/altgr/windows">Windows</a>',
+               'or <a href="/keyboard/altgr/macos">macOS</a>.',
+       ),
+       description => [
+               "An interactive map of EurKEY, the European Keyboard Layout.",
+       ],
+       image => 'data/keyboard/thumb/eurkeyboard.jpg',
+       imagealt => 'Right alt on the EurKEYboard created by Psy-Q',
+}
diff --git a/keyboard/altgr/index.inc.plp b/keyboard/altgr/index.inc.plp
new file mode 100644 (file)
index 0000000..d4d8bde
--- /dev/null
@@ -0,0 +1,173 @@
+<: # included from keyboard.plp
+use 5.014;
+use warnings;
+use utf8;
+
+my @incs = glob 'keyboard/altgr/*.eng.inc.pl';
+
+Html({
+       title => "altgr keyboard cheat sheets",
+       version => '1.2',
+       description => [
+               "Overview of alternate keyboard modes,",
+               "offering extended Unicode characters if a modifier key",
+               "(such as AltGr or option) is pressed.",
+       ],
+       keywords => [qw'
+               sheet cheat reference overview keyboard altgr option
+       '],
+       image => 'data/keyboard/thumb/ibm-m.jpg',
+       stylesheet => [qw( light dark circus mono red )],
+       data => ['keyboard/altgr/index.inc.plp', @incs],
+       raw => <<'.',
+<style>
+.keys.cmp {
+       display: inline-table; /* centered */
+       float: none;
+       margin: 0;
+       border-collapse: separate;
+       border-spacing: 2px;
+       border-spacing: .4vw; /* inline td margin equivalent */
+}
+.keys.big.cmp tbody {
+       font-size: 150%;
+}
+.keys.cmp tbody {
+       display: table-row-group;
+}
+.keys.cmp tr {
+       display: table-row;
+}
+.keys.cmp tr > * {
+       display: table-cell;
+       position: static; /* under sticky thead */
+}
+.keys.cmp tbody th {
+       padding-right: 1ex;
+       text-align: right;
+}
+
+@font-face {
+       font-family: osicons;
+       src: url(/osicon.ttf);
+}
+.icon {
+       font-family: osicons;
+}
+.keys.cmp tbody .ni {
+       font-size: 80%;
+       padding: 0 .2em;
+}
+
+.graph {
+       display: block;
+       line-height: 1ex;
+       height: 1.2ex;
+       margin-top: .4ex;
+}
+.graph ~ .graph {
+       /* subsequent graphs */
+       height: .7ex;
+}
+.graph > * {
+       display: inline-block;
+       height: 100%;
+       vertical-align: top;
+}
+.graph > label {
+       font-size: 75%;
+       margin-right: .2em;
+}
+.graph > span {
+       border: 1px solid #000;
+       border-right-width: 0;
+       font-size: 0;
+}
+.graph > :last-of-type {
+       border-right-width: 1px;
+}
+.graph > .ext {
+       border-left: 0; /* assume following unext */
+}
+
+img {
+       object-fit: cover;
+       height: 100%;
+       vertical-align: middle;
+}
+</style>
+.
+});
+
+:>
+<h1>Extended keyboards</h1>
+
+<p>Overview of available key layouts with AltGr or similar modifier keys.</p>
+
+<:
+my @sample = split /(?<!\+)/, $get{sample} // 'asSci1!+1';
+require Shiar_Sheet::Keyboard;
+use List::Util qw( uniq max );
+
+my %caticon = (
+       legacy  => qq{<span class=icon title="deprecated">\N{TOP HAT}</span>},
+       windows => qq{<span class=icon title="Windows">\x{1FA9F}</span>}, # \N{WINDOW}
+       macos   => qq{<span class=icon title="MacOS">\N{RED APPLE}</span>},
+       xorg    => qq{<span class=icon title="Xorg">\N{PENGUIN}</span>},
+);
+
+printf '<section class="%s">', @sample ? 'section' : 'gallery';
+if (@sample) {
+       print '<table class="big keys cmp">';
+       print '<thead><tr><th colspan=2>';
+       print "<th>$_" for @sample;
+       say '</tr></thead>';
+}
+my %idx = map {s/\Q.inc.pl\E$//; ($_ => eval{ Data($_) })} @incs;
+my $most = max(map { scalar keys %{$_->{def}{''}} } values %idx);
+for my $inc (sort {
+       $idx{$a}{category} cmp $idx{$b}{category} || $a cmp $b
+} keys %idx) {
+       print @sample ? '<tr><th>' : '<figure>';
+       printf '<a href="/%s">', $inc =~ s/\.eng$//r;
+       my $table = $idx{$inc};
+       my $title = $table && $table->{title} || $inc;
+
+       unless (@sample) {
+               if ($table and my $img = $table->{image}) {
+                       EscapeHTML $name = $table->{imagealt} // $img =~ m{.*/([^/.]*)};
+                       print qq{<img src="/$img" alt="$name" />};
+               }
+               printf '<figcaption>%s</figcaption>', $title;
+               say '</a></figure>';
+       }
+       else {
+               print $title;
+               print '</a>', "\n\t";
+               my $keys = Shiar_Sheet::Keyboard->new($table);
+               for my $mode ($keys->{mode} ? sort keys %{ $keys->{mode} } : '') {
+                       my %inventory;
+                       $inventory{$_}++ for grep { /^g[2-9]/ } map { s/ (?!ext).*//r }
+                               values %{ $keys->{def}{$mode} };
+                       print '<span class=graph>';
+                       print "<label>$_</label>" for $keys->{key}{$mode} =~ s/\s.*//r || ();
+                       for my $g (sort keys %inventory) {
+                               printf '<span class="%s" style="width:%.0f%%" title="%3$d %4$s"> %s</span>',
+                                       $g, $_/$most*100, $_,
+                                       join(' ', map {
+                                               $keys->{flag}{$_}[0] || 'extra'  # legend label of each class
+                                       } reverse split / /, $g)
+                                       for $inventory{$g};
+                       }
+                       say '</span>';
+               }
+               print "\t<td class=ni>";
+               print join ' ', map { $caticon{$_} // () } split m{/}, $keys->{category};
+               say '';
+               $keys->print_key('', $_, $keys->{def}{''}{$_} // 'ni') for @sample;
+               say '</tr>';
+       }
+}
+print '</table>' if @sample;
+:></section>
+
diff --git a/keyboard/altgr/ipa.eng.inc.pl b/keyboard/altgr/ipa.eng.inc.pl
new file mode 100644 (file)
index 0000000..4fc3cd5
--- /dev/null
@@ -0,0 +1,147 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbmodes';
+
+my %rows = (
+       '' => {
+               '`' => 'ǀ',
+               '+`' => "\N{MODIFIER LETTER RHOTIC HOOK}",
+               '~' => "\N{COMBINING LEFT ANGLE ABOVE}",
+               '+1' => 'ɨ',
+               '1' => 'ɴ',
+               '!' => 'ǃ',
+               '+2' => 'ø',
+               '2' => 'ǁ',
+               '@' => 'ˈ',
+               '#' => 'ɹ',
+               '3' => 'ɻ',
+               '+3'=> 'ɜ',
+               '4' => 'ɽ',
+               '+4' => 'ɾ',
+               '$' => '$',
+               '+5' => 'ɫ',
+               '5' => 'ʟ',
+               '%' => 'ˌ',
+               '+6' => 'ɐ',
+               '6' => 'ɓ',
+               '^' => "\N{COMBINING INVERTED BREVE BELOW}",
+               '+7' => 'ɤ',
+               '7' => 'ˠ',
+               '&' => 'ɶ',
+               '+8' => 'ɵ',
+               '8' => 'ɞ',
+               '*' => '*',
+               '9' => 'ɠ',
+               '+9' => 'œ',
+               '(' => '(',
+               '0' => "\N{COMBINING RING ABOVE}",
+               '+0' => "\N{COMBINING RING BELOW}",
+               ')' => ')',
+               '-' => "\N{COMBINING DOUBLE INVERTED BREVE}",
+               '_' => '‿',
+               '+-'=> '-',
+               '+' => '+',
+               '+=' => "\N{COMBINING VERTICAL LINE BELOW}",
+               '=' => 'ǂ',
+
+               'Q' => 'ɒ',
+               'q' => "\N{COMBINING UP TACK BELOW}",
+               'W' => 'ʍ',
+               'w' => 'ʷ',
+               'E' => 'ɛ',
+               'e' => 'ɘ',
+               'r' => 'ʀ',
+               'R' => 'ʁ',
+               'T' => 'θ',
+               't' => 'ʈ',
+               'Y' => 'ʏ',
+               'y' => "\N{COMBINING DOWN TACK BELOW}",
+               'u' => 'ɦ',
+               'U' => 'ʊ',
+               'I' => 'ɪ',
+               'i' => "\N{COMBINING PLUS SIGN BELOW}",
+               'O' => 'ɔ',
+               'o' => 'ʘ',
+               'P' => 'ʋ',
+               'p' => 'ɸ',
+               '{' => 'æ',
+               '[' => 'ɗ',
+               '}' => 'ʉ',
+               ']' => "\N{COMBINING BRIDGE BELOW}",
+
+               'A' => 'ɑ',
+               'a' => "\N{COMBINING MINUS SIGN BELOW}",
+               'S' => 'ʃ',
+               's' => 'ʂ',
+               'D' => 'ð',
+               'd' => 'ɖ',
+               'F' => 'ɱ',
+               'f' => 'ɟ',
+               'g' => 'ɢ',
+               'G' => 'ɣ',
+               'H' => 'ɥ',
+               'h' => 'ʰ',
+               'J' => 'ɲ',
+               'j' => 'ʝ',
+               'K' => 'ɬ',
+               'k' => 'ɮ',
+               'L' => 'ʎ',
+               'l' => 'ɭ',
+               "'" => 'ɚ',
+               "+'" => 'ʲ',
+               '"' => 'ə',
+               ';' => "\N{COMBINING DIAERESIS}",
+               ':' => 'ː',
+               '|' => "\N{COMBINING TILDE}",
+               '\\'=> "\N{COMBINING TILDE BELOW}",
+
+               'z' => 'ʐ',
+               'Z' => 'ʒ',
+               'X' => 'χ',
+               'x' => 'ħ',
+               'C' => 'ç',
+               'c' => 'ɕ',
+               'v' => 'ʑ',
+               'V' => 'ʌ',
+               'b' => 'ʙ',
+               'B' => 'β',
+               'N' => 'ŋ',
+               'n' => 'ɳ',
+               'M' => 'ɯ',
+               'm' => 'ɰ',
+               '<' => "\N{COMBINING BREVE}",
+               ',' => 'ʼ',
+               '.' => "\N{COMBINING DIAERESIS BELOW}",
+               '>' => '→',
+               '?' => 'ʔ',
+               '/' => 'ʕ',
+       },
+);
+
+# missing: ʤ ɜ ɝ ʄ ɡ ʛ ɧ ʜ ɺ ʧ ⱱ ʡ ʢ
+
++{
+       %{ kbmodes(\%rows) },
+       version => '1.0',
+       title => 'UCL phonetic',
+       category => 'specialised',
+       intro => join("\n",
+               'Unicode Phonetic Keyboard',
+               '<abbr title="dated 2009-06-28">v1.10</abbr> by Mark Huckvale',
+               'from UCL, available for',
+               '<a href="https://www.phon.ucl.ac.uk/resource/phonetics/">Windows</a>,',
+               'providing <a href="/unicode">Unicode symbols</a>',
+               'with Shift (top row) and Altgr',
+               'to transcribe (at least English) sounds',
+               'in <abbr title="International Phonetic Alphabet">IPA</abbr>.',
+       ),
+       description => [
+               "UCL Unicode Phonetic Keyboard layout table",
+               "with the AltGr modifier key.",
+       ],
+       image => 'data/keyboard/thumb/uclphonetics.jpg',
+       imagealt => 'Mechanical typewriter somehow wrote ˈɪŋglɪʃ fəˈnɛtɪks',
+       rows => [3,1,0],
+       moderows => '21-241',
+}
diff --git a/keyboard/altgr/macos-abc.eng.inc.pl b/keyboard/altgr/macos-abc.eng.inc.pl
new file mode 100644 (file)
index 0000000..2c9f729
--- /dev/null
@@ -0,0 +1,214 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbmodes';
+
+my %rows = (
+       '' => {
+       'A' => "\N{MACRON}",
+       'a' => "\N{COMBINING MACRON}",
+       'B' => "\N{BREVE}",
+       'b' => "\N{COMBINING BREVE}",
+       'C' => "\N{CEDILLA}",
+       'c' => "\N{COMBINING CEDILLA}",
+       'D' => 'Ð',
+       'd' => 'ð',
+       'E' => "\N{ACUTE ACCENT}",
+       'e' => "\N{COMBINING ACUTE ACCENT}",
+       'f' => 'ƒ',
+       'F' => "\N{COMBINING TILDE BELOW}",
+       'G' => "\N{COMBINING CIRCUMFLEX ACCENT BELOW}",
+       'g' => '©',
+       'H' => "\N{MODIFIER LETTER LOW MACRON}",
+       'h' => "\N{COMBINING MACRON BELOW}",
+       'I' => "\N{MODIFIER LETTER APOSTROPHE}",
+       'i' => "\N{COMBINING COMMA ABOVE}",
+       'J' => "\N{DOUBLE ACUTE ACCENT}",
+       'j' => "\N{COMBINING DOUBLE ACUTE ACCENT}",
+       'K' => '°',
+       'k' => "\N{COMBINING RING ABOVE}",
+       'L' => '-',
+       'l' => "\N{COMBINING SHORT STROKE OVERLAY}", #XXX
+       'M' => "\N{OGONEK}",
+       'm' => "\N{COMBINING OGONEK}",
+       'N' => "\N{SMALL TILDE}",
+       'n' => "\N{COMBINING TILDE}",
+       'O' => 'Ø',
+       'o' => 'ø',
+       'P' => "\N{SINGLE LOW-9 QUOTATION MARK}", #XXX
+       'p' => "\N{COMBINING COMMA BELOW}",
+       'Q' => 'Œ',
+       'q' => 'œ',
+       'R' => '‰',
+       'r' => '®',
+       'S' => "\N{COMBINING INVERTED BREVE}",
+       's' => 'ß',
+       'T' => 'Þ',
+       't' => 'þ',
+       'U' => "\N{DIAERESIS}",
+       'u' => "\N{COMBINING DIAERESIS}",
+       'V' => "\N{CARON}",
+       'v' => "\N{COMBINING CARON}",
+       'W' => "\N{DOT ABOVE}",
+       'w' => "\N{COMBINING DOT ABOVE}",
+       'X' => "\N{MODIFIER LETTER LOW RING}", #XXX
+       'x' => "\N{COMBINING DOT BELOW}",
+       'Y' => "\N{COMBINING DOUBLE GRAVE ACCENT}",
+       'y' => '¥',
+       'Z' => "\N{MODIFIER LETTER GLOTTAL STOP}",
+       'z' => "\N{COMBINING HOOK ABOVE}",
+       '[' => "\N{LEFT DOUBLE QUOTATION MARK}",
+       '{' => "\N{RIGHT DOUBLE QUOTATION MARK}",
+       ']' => "\N{LEFT SINGLE QUOTATION MARK}",
+       '}' => "\N{RIGHT SINGLE QUOTATION MARK}",
+       ';' => '…',
+       ':' => '№',
+       '"' => 'Æ',
+       "'" => 'æ',
+       '|' => '»',
+       '\\'=> '«',
+       '<' => "\N{DOUBLE LOW-9 QUOTATION MARK}",
+       ',' => '≤',
+       '>' => 'ʔ',
+       '.' => '≥',
+       '/' => '÷',
+       '?' => '¿',
+       '~' => "\N{GRAVE ACCENT}",
+       '`' => "\N{COMBINING GRAVE ACCENT}",
+       '1' => '¡',
+       '!' => '⁄',
+       '2' => '™',
+       '@' => '€',
+       '3' => '£',
+       '#' => '‹',
+       '4' => '¢',
+       '$' => '›',
+       '5' => '§',
+       '%' => '†',
+       '6' => "\N{COMBINING CIRCUMFLEX ACCENT}",
+       '^' => "\N{MODIFIER LETTER CIRCUMFLEX ACCENT}", #XXX
+       '7' => '¶',
+       '&' => '‡',
+       '8' => '•',
+       '*' => '°',
+       '9' => 'ª',
+       '(' => '·',
+       '0' => 'º',
+       ')' => '‚',
+       '-' => '–',
+       '_' => '—',
+       '+' => '±',
+       '=' => '≠',
+       },
+
+       ':' => {
+               '@' => 'Ƨ',
+               '2' => 'ƨ',
+               '#' => 'Ɛ',
+               '3' => 'ɛ',
+               '%' => 'Ƽ',
+               '5' => 'ƽ',
+               '^' => 'Ƅ',
+               '6' => 'ƅ', #XXX
+               '7' => '⁊',
+               '*' => 'Ȣ',
+               '8' => 'ȣ',
+               'Q' => 'Ƣ',
+               'q' => 'ƣ',
+               'W' => 'Ƿ',
+               'w' => 'ƿ',
+               'E' => 'Ǝ',
+               'e' => 'ǝ', #XXX ə
+               'R' => 'Ʀ',
+               'r' => 'ʀ', #XXX
+               'k' => 'ĸ',
+               'Y' => 'Ɜ',
+               'y' => 'ɜ',
+               'U' => 'Ʊ',
+               'u' => 'ʊ',
+               'A' => 'Ə',
+               'a' => 'ə',
+               's' => 'ſ',
+               'G' => 'Ɣ',
+               'g' => 'ɣ',
+               'H' => 'Ƕ',
+               'h' => 'ƕ',
+               'J' => 'Ƞ',
+               'j' => 'ƞ', #XXX ɳ
+               'K' => 'Ǩ',
+               'Z' => 'Ʒ',
+               'z' => 'ʒ',
+               'C' => 'Ɔ',
+               'c' => 'ɔ',
+               'v' => 'ʌ',
+               'N' => 'Ŋ', #XXX
+               'n' => 'ŋ',
+               'M' => 'Ɯ',
+               'm' => 'ɯ',
+               '"' => '″', #XXX ʺ
+               "'" => '′', #XXX ʹ
+       },
+
+       '>' => {
+               'q' => 'ʠ',
+               'R' => 'Ʈ', #XXX mirrored?
+               'r' => 'ʈ',
+               'T' => 'Ƭ',
+               't' => 'ƭ',
+               'Y' => 'Ƴ',
+               'y' => 'ƴ',
+               'U' => 'Ʋ',
+               'u' => 'ʋ',
+               'I' => 'Ɩ',
+               'i' => 'ɩ',
+               'P' => 'Ƥ',
+               'p' => 'ƥ',
+               'S' => 'Ʃ',
+               's' => 'ʃ',
+               'D' => 'Ɗ',
+               'd' => 'ɗ',
+               'F' => 'Ƒ',
+               'f' => 'ƒ',
+               'G' => 'Ɠ',
+               'g' => 'ɠ',
+               'h' => 'ɦ',
+               'K' => 'Ƙ',
+               'k' => 'ƙ',
+               'Z' => 'Ȥ',
+               'z' => 'ȥ',
+               'X' => 'Ɖ',
+               'x' => 'ɖ',
+               'C' => 'Ƈ',
+               'c' => 'ƈ',
+               'N' => 'Ɲ',
+               'n' => 'ɲ',
+               'B' => 'Ɓ',
+               'b' => 'ɓ',
+       },
+);
+
++{
+       %{ kbmodes(\%rows) },
+       mode => {
+               ''  => 'option-shifted ⌥',
+               ':' => '№ number prefix ⌥:',
+               '>' => 'ʔ hook prefix ⌥>',
+       },
+       version => '1.0',
+       title => 'ABC option',
+       category => '2/latin/macos',
+       intro => join("\n",
+               'Resulting <a href="/unicode">selection</a> of <a href="/charset">Unicode</a> characters',
+               "while pressing ⌥ Option (Alt) with Apple's ABC Extended",
+               "(formerly US Extended and Extended Roman) layout",
+               'on <abbr title="at least in Ventura 13.2">macOS</abbr>.',
+               'Significant changes from standard',
+               '<a href="/keyboard/altgr/macos">US</a> or local options.',
+       ),
+       description => [
+               "Apple ABC Extended keyboard layout table",
+               "with the Option modifier key.",
+       ],
+       image => 'data/keyboard/thumb/macbook-gray.jpg',
+       imagealt => 'Option key on a black MacBook',
+}
diff --git a/keyboard/altgr/macos.eng.inc.pl b/keyboard/altgr/macos.eng.inc.pl
new file mode 100644 (file)
index 0000000..15b0a7f
--- /dev/null
@@ -0,0 +1,121 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbchars';
+
+my %rows = (
+       'A' => 'Å',
+       'a' => 'å',
+       'B' => 'ı',
+       'b' => '∫',
+       'C' => 'Ç',
+       'c' => 'ç',
+       'D' => 'Î',
+       'd' => '∂',
+       'E' => "\N{ACUTE ACCENT}",
+       'e' => "\N{COMBINING ACUTE ACCENT}",
+       'f' => 'ƒ',
+       'F' => 'Ï',
+       'G' => "\N{DOUBLE ACUTE ACCENT}",
+       'g' => '©',
+       'H' => 'Ó',
+       'h' => "\N{DOT ABOVE}",
+       'I' => "\N{MODIFIER LETTER CIRCUMFLEX ACCENT}",
+       'i' => "\N{COMBINING CIRCUMFLEX ACCENT}",
+       'J' => 'Ô',
+       'j' => '∆',
+       'K' => '',
+       'k' => '°',
+       'L' => 'Ò',
+       'l' => '¬',
+       'M' => 'Â',
+       'm' => 'µ',
+       'N' => "\N{SMALL TILDE}",
+       'n' => "\N{COMBINING TILDE}",
+       'O' => 'Ø',
+       'o' => 'ø',
+       'P' => '∏',
+       'p' => 'π',
+       'Q' => 'Œ',
+       'q' => 'œ',
+       'R' => '‰',
+       'r' => '®',
+       'S' => 'Í',
+       's' => 'ß',
+       'T' => "\N{CARON}",
+       't' => '†',
+       'U' => "\N{DIAERESIS}",
+       'u' => "\N{COMBINING DIAERESIS}",
+       'V' => '◊',
+       'v' => '√',
+       'W' => '„',
+       'w' => '∑',
+       'X' => "\N{OGONEK}",
+       'x' => '≈',
+       'Y' => 'Á',
+       'y' => '¥',
+       'Z' => "\N{CEDILLA}",
+       'z' => 'Ω',
+       '[' => "\N{LEFT DOUBLE QUOTATION MARK}",
+       '{' => "\N{RIGHT DOUBLE QUOTATION MARK}",
+       ']' => "\N{LEFT SINGLE QUOTATION MARK}",
+       '}' => "\N{RIGHT SINGLE QUOTATION MARK}",
+       ':' => 'Ú',
+       ';' => '…',
+       '"' => 'Æ',
+       "'" => 'æ',
+       '|' => '»',
+       '\\'=> '«',
+       '<' => "\N{MACRON}",
+       ',' => '≤',
+       '>' => "\N{BREVE}",
+       '.' => '≥',
+       '/' => '÷',
+       '?' => '¿',
+       '~' => "\N{GRAVE ACCENT}",
+       '`' => "\N{COMBINING GRAVE ACCENT}",
+       '1' => '¡',
+       '!' => '⁄',
+       '2' => '™',
+       '@' => '€',
+       '3' => '£',
+       '#' => '‹',
+       '4' => '¢',
+       '$' => '›',
+       '5' => '∞',
+       '%' => 'fi',
+       '6' => '§',
+       '^' => 'fl',
+       '7' => '¶',
+       '&' => '‡',
+       '8' => '•',
+       '*' => '°',
+       '9' => 'ª',
+       '(' => '·',
+       '0' => 'º',
+       ')' => '‚',
+       '-' => '–',
+       '_' => '—',
+       '+' => '±',
+       '=' => '≠',
+);
+
++{
+       %{ kbchars(\%rows) },
+       version => '1.3',
+       title => 'US option',
+       category => '2/latin/macos',
+       intro => join("\n",
+               'Resulting <a href="/unicode">selection</a> of <a href="/charset">Unicode</a> characters',
+               "while pressing ⌥ Option (Alt) with Apple's US (or US International) layout",
+               'on <abbr title="at least in Ventura 13.2">macOS</abbr>.',
+               q{An alternative <a href="/keyboard/altgr/macos-abc">ABC Extended</a> is also available.},
+               q{Different from <a href="/keyboard/altgr/windows">AltGr</a> on Windows.},
+       ),
+       description => [
+               "Apple US International keyboard layout table",
+               "with the Option modifier key.",
+       ],
+       image => 'data/keyboard/thumb/matias-fk302.jpg',
+       imagealt => 'Option key on a Matias Tactile Pro keyboard with USA keycaps',
+}
diff --git a/keyboard/altgr/msx-graph.eng.inc.pl b/keyboard/altgr/msx-graph.eng.inc.pl
new file mode 100644 (file)
index 0000000..3bcf5d1
--- /dev/null
@@ -0,0 +1,158 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbchars';
+our %get;
+
+my %rows = (
+       '1' => '¼',
+       '@' => '²',
+       '2' => '½',
+       '#' => 'ⁿ',
+       '3' => '¾',
+       '4' => '∩',
+       '5' => '‰',
+       '^' => '⌡',
+       '6' => '⌠',
+       '7' => '√',
+       '8' => '∞',
+       '(' => '◘', # inverted ·
+       '9' => '·', # smaller than •
+       ')' => '◙',
+       '0' => '○',
+       '_' => '🮯',
+       '-' => '─',
+       '+' => '≡',
+       '=' => '±',
+       '~' => '≈',
+       '`' => '∽',
+
+       'Q' => '🮙',
+       'q' => '🮘',
+       'W' => '🭮',
+       'w' => '🭬',
+       'E' => '🭯',
+       'e' => '🭭',
+       'R' => '⌐',
+       'r' => '┌',
+       't' => '┬',
+       'Y' => '¬',
+       'y' => '┐',
+       'U' => '🮅',
+       'u' => '▂',
+       'I' => '▀',
+       'i' => '▄',
+       'O' => '🮂',
+       'o' => '▆',
+       'P' => '🮖',
+       'p' => '█',
+       '[' => '☺',
+       '{' => '☻',
+       ']' => '♪',
+       '}' => '♫',
+
+       'A' => '▮',
+       'a' => '▬',
+       'S' => '🮚',
+       's' => '🮛',
+       'D' => '▚',
+       'd' => '▞',
+       'F' => '▗',
+       'f' => '├',
+       'G' => '⟊', # ┼ without connecting right
+       'g' => '┼',
+       'H' => '▖',
+       'h' => '┤',
+       'J' => '🮊',
+       'j' => '▎',
+       'K' => '▐',
+       'k' => '▌',
+       'L' => '🮇',
+       'l' => '▊',
+       '"' => '♥',
+       "'" => '♣',
+       ':' => '♦',
+       ';' => '♠',
+       '|' => '│',
+       '\\'=> '╲',
+
+       'Z' => '◦', # small white circle
+       'z' => '☼',
+       'X' => '•', # small black circle
+       'x' => '╳',
+       'C' => '⁃',
+       'c' => '◇', # ◊
+       'V' => '▝',
+       'v' => '└',
+       'B' => '▬',
+       'b' => '┴',
+       'N' => '▘',
+       'n' => '┘',
+       'M' => '♀',
+       'm' => '♂',
+       '<' => '«',
+       ',' => '≤',
+       '>' => '»',
+       '.' => '≥',
+       '?' => '÷',
+       '/' => '╱',
+
+);
+
+my %compat = (
+       'Q' => '▨',
+       'q' => '▧',
+       'W' => '◀', #
+       'w' => '▶',
+       'E' => '▲',
+       'e' => '▼', #
+       'U' => '▓',
+       'J' => '░',
+       'O' => '▔',
+       'P' => '▒',
+       'S' => '⧗',
+       's' => '⧓',
+       'G' => '╂',
+       '_' => '┿',
+       'L' => '▕',
+);
+
+if (exists $get{compat}) {
+       %rows = (%rows, %compat);
+}
+
+my $groups = kbchars(\%rows);
+
+# replace rare punctuation distinctions by symbols
+$groups->{def}{''}{$_} =~ s/g[78]/g6/ for keys %rows;
+
+$groups->{def}{''}{$_} = 'g7'
+       for grep { $rows{$_} =~ /[\x{2500}-\x{259F}]/ } keys %rows;
+$groups->{flag}{g7} = ['drawing', 'box drawing or block elements'];
+
+$groups->{def}{''}{$_} = 'g8' for keys %compat; # mostly U+1FBxx
+$groups->{flag}{g8} = ['legacy',
+       'drawing symbols best represented by Unicode 13.0'
+       . (exists $get{compat} && ', converted to compatible equivalents')
+];
+
++{
+       %{ $groups },
+       version => '1.0',
+       title => 'MSX graph',
+       category => 'legacy/msx',
+       intro => join("\n",
+               'Resulting <a href="/unicode">selection</a>',
+               'of equivalent <a href="/charset">Unicode</a> characters',
+               "when the <em>graph</em> key is pressed on an MSX home computer",
+               "(International model such as Toshiba HX10 or Phillips NMS 8245).",
+               'See also <a href="/keyboard/altgr/msx">letters and symbols</a>',
+               "from pressing <em>code</em>.",
+       ),
+       description => [
+               "MSX keyboard layout table",
+               "with the graph modifier key.",
+       ],
+       image => 'data/keyboard/thumb/msxgraph.jpg',
+       imagealt => 'Graph key on a Toshiba HX10 with graph labels',
+}
diff --git a/keyboard/altgr/msx.eng.inc.pl b/keyboard/altgr/msx.eng.inc.pl
new file mode 100644 (file)
index 0000000..a7d8c0b
--- /dev/null
@@ -0,0 +1,116 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbchars';
+
+my %rows = (
+       '!' => '¡',
+       '1' => 'ƒ',
+       '@' => '₧',
+       '2' => '‡',
+       '#' => '¶',
+       '3' => '§',
+       '$' => '£',
+       '4' => '¢',
+       '%' => '¥',
+       '5' => 'ÿ',
+       '6' => 'α',
+       '7' => 'ß',
+       '*' => 'Γ',
+       '8' => 'γ',
+       '(' => 'Ç',
+       '9' => 'ç',
+       ')' => 'Δ',
+       '0' => 'δ',
+       '-' => '∈',
+       '=' => 'ϴ',
+       '\\'=> '£',
+
+       'q' => 'â',
+       'w' => 'ê',
+       'e' => 'î',
+       'r' => 'ô',
+       't' => 'û',
+       'y' => 'á',
+       'U' => 'É',
+       'u' => 'é',
+       'i' => 'í',
+       'o' => 'ó',
+       'P' => 'Π',
+       'p' => 'ú',
+       '{' => 'Φ',
+       '[' => 'ø',
+       '}' => 'Ω',
+       ']' => 'ω',
+
+       'A' => 'Ä',
+       'a' => 'ä',
+       's' => 'ë',
+       'd' => 'ï',
+       'F' => 'Ö',
+       'f' => 'ö',
+       'G' => 'Ü',
+       'g' => 'ü',
+       'H' => 'Ã',
+       'h' => 'ã',
+       'J' => 'Æ',
+       'j' => 'æ',
+       'K' => 'Ĩ',
+       'k' => 'ĩ',
+       'L' => 'Õ',
+       'l' => 'õ',
+       ':' => 'Ũ',
+       ';' => 'ũ',
+       '"' => 'IJ',
+       "'" => 'ij',
+       '~' => 'Σ',
+       '`' => 'σ',
+
+       'z' => 'à',
+       'x' => 'è',
+       'c' => 'ì',
+       'v' => 'ò',
+       'b' => 'ù',
+       'N' => 'Ñ',
+       'n' => 'ñ',
+       'm' => 'μ',
+       '<' => 'Å',
+       ',' => 'å',
+       '.' => 'ª',
+       '?' => '¿',
+       '/' => 'º',
+);
+
+my %uc = (
+       (map { (uc $_ => uc $rows{$_}) } grep {
+               !defined $rows{uc $_}
+       } keys %rows),
+       '|' => '€',
+       'M' => 'Ú',
+       '+' => 'Ø',
+);
+
+my $groups = kbchars({%rows, %uc});
+$groups->{def}{''}{$_} .= ' ext' for keys %uc;
+$groups->{flag}{ext} = ['anachrone', 'expected uppercase variants if allowed by charset'];
+
++{
+       %{ $groups },
+       version => '1.2',
+       title => 'MSX code',
+       category => 'legacy/msx/latin',
+       intro => join("\n",
+               'Resulting <a href="/unicode">selection</a>',
+               'of equivalent <a href="/charset">Unicode</a> characters',
+               "when the <em>code</em> key is pressed on an MSX home computer",
+               "(International model such as Toshiba HX10 or Phillips NMS 8245).",
+               'See also <a href="/keyboard/altgr/msx-graph">block graphics</a>',
+               "from pressing <em>graph</em>.",
+       ),
+       description => [
+               "MSX keyboard layout table",
+               "with the code modifier key.",
+       ],
+       image => 'data/keyboard/thumb/msxcode.jpg',
+       imagealt => 'Code key on a Toshiba HX10 with graph labels',
+}
diff --git a/keyboard/altgr/olpc.eng.inc.pl b/keyboard/altgr/olpc.eng.inc.pl
new file mode 100644 (file)
index 0000000..3d72e36
--- /dev/null
@@ -0,0 +1,94 @@
+use utf8;
+use strict;
+use warnings;
+no warnings 'qw';
+use Shiar_Sheet::KeyboardChars 'kbchars';
+
+my %rows = (
+       '1' => '¡',
+       '2' => '¬',
+       '3' => "\N{COMBINING GRAVE ACCENT}",
+       '4' => "\N{COMBINING ACUTE ACCENT}",
+       '5' => "\N{COMBINING BREVE}",
+       '6' => "\N{COMBINING RING ABOVE}",
+       '7' => "\N{COMBINING CIRCUMFLEX ACCENT}",
+       '8' => "\N{COMBINING CARON}",
+       '9' => "\N{COMBINING DOT ABOVE}",
+       '0' => "\N{COMBINING DIAERESIS}",
+       '-' => "\N{COMBINING MACRON}",
+       '=' => "\N{COMBINING TILDE}",
+
+       'q' => 'ω',
+       'Q' => 'Ω',
+       'w' => 'ø',
+       'W' => 'Ø',
+       'e' => 'œ',
+       'E' => 'Œ',
+       'r' => "\N{COMBINING CEDILLA}",
+       't' => "\N{COMBINING BREVE BELOW}",
+       'y' => "\N{COMBINING RING BELOW}",
+       'u' => "\N{COMBINING CIRCUMFLEX ACCENT BELOW}",
+       'i' => "\N{COMBINING CARON BELOW}",
+       'o' => "\N{COMBINING DOT BELOW}",
+       'p' => "\N{COMBINING DIAERESIS BELOW}",
+       '[' => "\N{COMBINING MACRON BELOW}",
+       ']' => "\N{COMBINING TILDE BELOW}",
+
+       'a' => 'æ',
+       'A' => 'Æ',
+       's' => 'ß',
+       'S' => 'ẞ',
+       'd' => 'ð',
+       'D' => 'Ð',
+       'f' => 'þ',
+       'F' => 'Þ',
+       'h' => '£',
+       'j' => '€',
+       ';' => 'º',
+       ':' => 'ª',
+       "'" => '¤',
+       '\\'=> '§',
+
+       'c' => 'ç',
+       'C' => 'Ç',
+       'n' => 'ñ',
+       'N' => 'Ñ',
+       'm' => "\N{MICRO SIGN}",
+       ',' => '«',
+       '.' => '»',
+       '/' => '¿',
+);
+
+my %shift = (
+       qw[ ! 1  @ 2  # 3  $ 4  % 5  ^ 6  & 7  * 8  ( 9  ) 0  _ -  + = ],
+       qw( { [  } ]  " '  | \  < ,  > .  ? /  ` 3  ~ = ),
+       (map {uc, lc} qw[ r t y u i o p  h j  m  ]),
+);
+$rows{$_} = $rows{ $shift{$_} } for keys %shift; # alias shifted
+
+my $groups = kbchars(\%rows);
+$groups->{flag}{ext} = ['alias', 'identical results from unshifted key'];
+$groups->{def}{''}{$_} .= ' ext' for keys %shift; # mark aliases
+
++{
+       %{ $groups },
+       version => '1.0',
+       title => 'OLPC',
+       category => 'legacy/latin/xorg',
+       intro => join("\n",
+               "International US English developed for the OLPC project,",
+               'providing mostly European',
+               '<a href="/unicode">Unicode</a> characters while pressing AltGr,',
+               'entirely different from the <a href="/keyboard/altgr/windows">Windows</a>',
+               'or <a href="/keyboard/altgr/macos">MacOS</a> maps.',
+       ),
+       description => [
+               "OLPC keyboard layout table",
+               "with the AltGr modifier key:",
+               "provides common western European letters and symbols,",
+               "and various combining accents above and below.",
+       ],
+       image => 'data/keyboard/thumb/olpc.jpg',
+       imagealt => 'AltGr on the OLPC XO Laptop',
+       moderows => '21-1',
+}
diff --git a/keyboard/altgr/spacecadet.eng.inc.pl b/keyboard/altgr/spacecadet.eng.inc.pl
new file mode 100644 (file)
index 0000000..740dd35
--- /dev/null
@@ -0,0 +1,117 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbchars';
+
+my %rows = (
+       'q' => '∧',
+       'w' => '∨',
+       'e' => '∩',
+       'r' => '∪',
+       't' => '⊂',
+       'y' => '⊃',
+       'u' => '∀',
+       'i' => '∞',
+       'o' => '∃',
+       'p' => '∂',
+
+       'a' => '⊥',
+       's' => '⊤',
+       'd' => '⊢',
+       'f' => '⊣',
+       'g' => '↑',
+       'h' => '↓',
+       'j' => '←',
+       'k' => '→',
+       'l' => '↔',
+
+       'z' => '⌊',
+       'x' => '⌈',
+       'c' => '≠',
+       'v' => '≃',
+       'b' => '≡',
+       'n' => '≤',
+       'm' => '≥',
+       #',' => '<', # just shifted
+       #'.' => '>',
+       #'/' => '?',
+
+       '+:' => '⋄', # positioned left of 1, also drawn as §-ish
+       '+1' => '†',
+       '+2' => '‡',
+       '+3' => '∇',
+       '+4' => '¢',
+       '+5' => '∘',
+       '+6' => '⎕', # or ⌷
+       '+7' => '÷',
+       '+8' => '×',
+       '+9' => '¶',
+       '+0' => '○',
+       '+-' => '¯',
+       '+=' => '≈',
+
+       '+q' => 'θ',
+       '+w' => 'ω',
+       '+e' => 'ε',
+       '+r' => 'ρ',
+       '+t' => 'τ',
+       '+y' => 'ψ',
+       '+u' => 'υ',
+       '+i' => 'ι',
+       '+o' => 'ο',
+       '+p' => 'π',
+       '+[' => '⟦', # separate keys ([ and {<
+       '+]' => '⟧', # )] and }> ⟨⟩
+       '+`' => '¬', # positioned between ] and \
+       '+\\'=> '∥',
+
+       '+a' => 'α',
+       '+s' => 'σ',
+       '+d' => 'δ',
+       '+f' => 'φ', # drawn like ø
+       '+g' => 'γ',
+       '+h' => 'η',
+       '+j' => 'ϑ', # probably
+       '+k' => 'κ',
+       '+l' => 'λ',
+       '+;' => '¨',
+       "+'" => '·', # ambiguous dot (visually raised •)
+
+       '+z' => 'ζ',
+       '+x' => 'ξ',
+       '+c' => 'χ',
+       '+v' => 'ς', # likely
+       '+b' => 'β',
+       '+n' => 'ν',
+       '+m' => 'μ',
+       '+,' => '≪',
+       '+.' => '≫',
+       '+/' => '∫',
+);
+
+my $groups = kbchars(\%rows);
+$rows{$_} =~ /\A\p{Greek}/ and $groups->{def}{''}{$_} =~ s/g6/g5/ for map {"+$_"} 'a'..'z';
+$groups->{flag}{g4} = ['similar', 'transliterates or transcribes an expected letter'];
+$groups->{flag}{g5} = ['greek', 'different Greek letters or symbols'];
+
++{
+       %{$groups},
+       version => '1.1',
+       title => 'Space Cadet',
+       category => 'legacy',
+       intro => join("\n",
+               'Apparent <a href="/charset">glyphs</a> available',
+               'on the 1978 (later Symbolics) Space Cadet keyboard',
+               'by pressing either the <em>Greek/Front</em> or <em>Top</em> key.',
+               'Distinct from the modern <a href="/keyboard/altgr/apl">IBM standard</a>',
+               'for <a href="/apl">APL</a> programming.',
+       ),
+       description => [
+               'A map of the legendary Space Cadet keyboard',
+               'with Unicode characters of all greek and APL options.',
+       ],
+       rows => [3, 0], # greek/front and top
+       moderows => '41-4',
+       image => 'data/keyboard/thumb/spacecadet.jpg',
+       imagealt => 'Many modifier keys on a traditional Symbolics Space Cadet keyboard',
+}
diff --git a/keyboard/altgr/symbolics.eng.inc.pl b/keyboard/altgr/symbolics.eng.inc.pl
new file mode 100644 (file)
index 0000000..b0c3a1e
--- /dev/null
@@ -0,0 +1,133 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbchars';
+
+my %rows = (
+  '!' => "\xAC",
+  '"' => "\x{2190}",
+  '#' => "\xA3",
+  '$' => "\x{20AC}",
+  '%' => "\x{2030}",
+  '&' => "\xA7",
+  "'" => "\x{2192}",
+  '(' => "\xB7",
+  ')' => "\xB0",
+  '*' => "\x{221E}",
+  '+' => "\xF7",
+  ',' => "\x{2264}",
+  '-' => "\x{2260}",
+  '.' => "\x{2265}",
+  '/' => "\x{203D}",
+  '0' => "\x{2070}",
+  '1' => "\xB9",
+  '2' => "\xB2",
+  '3' => "\xB3",
+  '4' => "\x{2074}",
+  '5' => "\x{2075}",
+  '6' => "\x{2076}",
+  '7' => "\x{2077}",
+  '8' => "\x{2078}",
+  '9' => "\x{2079}",
+  ':' => "\x{2191}",
+  ';' => "\x{2193}",
+  '<' => "\xAB",
+  '=' => "\xD7",
+  '>' => "\xBB",
+  '?' => "\x{2766}",
+  '@' => "\x{2234}",
+  '[' => "\x{222A}",
+  '\\'=> "`",
+  '|' => "~",
+  '`' => "\\",
+  '~' => "|",
+  ']' => "\x{2282}",
+  '^' => "\x{221A}",
+  '_' => "\xB1",
+  'A' => "\x{391}",
+  'a' => "\x{3B1}",
+  'B' => "\x{392}",
+  'b' => "\x{3B2}",
+  'c' => "\x{3C8}",
+  'C' => "\x{3A8}",
+  'D' => "\x{394}",
+  'd' => "\x{3B4}",
+  'E' => "\x{395}",
+  'e' => "\x{3B5}",
+  'F' => "\x{3A6}",
+  'f' => "\x{3C6}",
+  'g' => "\x{3B3}",
+  'G' => "\x{393}",
+  'H' => "\x{397}",
+  'h' => "\x{3B7}",
+  'i' => "\x{3B9}",
+  'I' => "\x{399}",
+  'j' => "\x{3BE}",
+  'J' => "\x{39E}",
+  'k' => "\x{3BA}",
+  'K' => "\x{39A}",
+  'l' => "\x{3BB}",
+  'L' => "\x{39B}",
+  'M' => "\x{39C}",
+  'm' => "\x{3BC}",
+  'n' => "\x{3BD}",
+  'N' => "\x{39D}",
+  'O' => "\x{39F}",
+  'o' => "\x{3BF}",
+  'p' => "\x{3C0}",
+  'P' => "\x{3A0}",
+  'Q' => "\x{2203}",
+  'q' => "\x{2200}",
+  'R' => "\x{3A1}",
+  'r' => "\x{3C1}",
+  'S' => "\x{3A3}",
+  's' => "\x{3C3}",
+  'T' => "\x{3A4}",
+  't' => "\x{3C4}",
+  'u' => "\x{3B8}",
+  'U' => "\x{398}",
+  'v' => "\x{3C9}",
+  'V' => "\x{3A9}",
+  'w' => "\x{2208}",
+  'W' => "\x{2209}",
+  'x' => "\x{3C7}",
+  'X' => "\x{3A7}",
+  'Y' => "\x{3A5}",
+  'y' => "\x{3C5}",
+  'z' => "\x{3B6}",
+  'Z' => "\x{396}",
+  '{' => "\x{2229}",
+  '}' => "\x{2283}",
+
+  # LSGT (iso key) brackets at TLDE/BKSL inversions
+  '`' => "\x{230A}",
+  '~' => "\x{230B}",
+  '\\'=> "\x{2308}",
+  '|' => "\x{2309}",
+);
+
+my $groups = kbchars(\%rows);
+$groups->{flag}->{g5} = [greek =>
+       "a different greek letter not corresponding with latin transcription"
+];
+while (my ($key, $chr) = each %rows) {
+       $groups->{def}->{''}->{$key} =~ s/g6/g5/ if $chr =~ /\A\p{Greek}/;
+}
+
++{
+       %{$groups},
+       version => '1.1',
+       title => 'US Symbolics',
+       category => 'specialised/greek/xorg',
+       intro => join("\n",
+               "A US English extension providing scientific",
+               '<a href="/unicode">Unicode</a> characters while pressing AltGr,',
+               'developed by Daniele Baisero',
+               'for <abbr title="distributed with X since 2020">Linux</abbr>.',
+       ),
+       description => [
+               "US Symbolics keyboard layout table",
+               "with the AltGr modifier key: provides Greek letters",
+               "and symbols for scientific English literature.",
+       ],
+}
diff --git a/keyboard/altgr/ukext.eng.inc.pl b/keyboard/altgr/ukext.eng.inc.pl
new file mode 100644 (file)
index 0000000..6a8e914
--- /dev/null
@@ -0,0 +1,120 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbchars';
+
+my %rows = (
+       '~' => '¦',
+       '!' => '¡',
+       '1' => '¹',
+       '@' => '½', # uk "
+       '2' => "\N{COMBINING DIAERESIS}",
+       '#' => '⅓', # uk £
+       '3' => '³',
+       '$' => '¼',
+       '4' => '€',
+       '%' => '⅜',
+       '5' => '½',
+       '^' => '⅝',
+       '6' => "\N{COMBINING CIRCUMFLEX ACCENT}",
+       '&' => '⅞',
+       '7' => '{',
+       '*' => '™',
+       '8' => '[',
+       '(' => '±',
+       '9' => ']',
+       ')' => '°',
+       '0' => '}',
+       '_' => '¿',
+       '-' => '\\',
+       '+' => "\N{COMBINING OGONEK}",
+       '=' => "\N{COMBINING CEDILLA}",
+       'Q' => 'Ω',
+       'q' => '@',
+       'W' => 'Ẃ',
+       'w' => 'ẃ',
+       'E' => 'É',
+       'e' => 'é',
+       'R' => '®',
+       'r' => '¶',
+       'T' => 'Ŧ',
+       't' => 'ŧ',
+       'Y' => 'Ý',
+       'y' => 'ý',
+       'U' => 'Ú',
+       'u' => 'ú',
+       'I' => 'Í',
+       'i' => 'í',
+       'O' => 'Ó',
+       'o' => 'ó',
+       'P' => 'Þ',
+       'p' => 'þ',
+       '{' => "\N{COMBINING RING ABOVE}",
+       '}' => "\N{COMBINING MACRON}",
+       'A' => 'Á',
+       'a' => 'á',
+       'S' => '§',
+       's' => 'ß',
+       'D' => 'Ð',
+       'd' => 'ð',
+       'F' => 'ª',
+       'f' => 'đ',
+       'G' => 'Ŋ',
+       'g' => 'ŋ',
+       'H' => 'Ħ',
+       'h' => 'ħ',
+       'J' => "\N{COMBINING HORN}",
+       'j' => "\N{COMBINING HOOK ABOVE}",
+       'K' => '&',
+       'k' => 'ĸ',
+       'L' => 'Ł',
+       'l' => 'ł',
+       ':' => "\N{COMBINING DOUBLE ACUTE ACCENT}",
+       ';' => "\N{COMBINING ACUTE ACCENT}",
+       '"' => "\N{COMBINING CARON}", # uk @
+       "'" => "\N{COMBINING ACUTE ACCENT}", # same as ;?
+       '|' => "\N{COMBINING TILDE}",
+       '\\'=> "\N{COMBINING BREVE}",
+       '~' => "\N{COMBINING GRAVE ACCENT}",
+       '`' => '¦',
+       'Z' => '<',
+       'z' => '«',
+       'X' => '>',
+       'x' => '»',
+       'C' => 'Ç',
+       'c' => 'ç',
+       'V' => '‘',
+       'v' => '“',
+       'B' => '’',
+       'b' => '”',
+       #'N'=> 'N',
+       #'n'=> 'n',
+       'M' => 'º',
+       'm' => 'µ',
+       '<' => '×',
+       ',' => '─',
+       '>' => '÷',
+       '.' => '·',
+       '?' => "\N{COMBINING DOT ABOVE}",
+       '/' => "\N{COMBINING DOT BELOW}",
+);
+$rows{'['} = $rows{'2'};
+$rows{']'} = $rows{'#'};
+
++{
+       %{ kbchars(\%rows) },
+       title => 'UK-extended',
+       category => 'latin/thirdparty',
+       version => '1.0',
+       intro => join("\n",
+               "A Chrome OS extension",
+               "expanding on Windows' UK Extended QWERTY keyboard.",
+               'Similar to the <a href="/keyboard/altgr">US international</a> variant.',
+       ),
+       description => [
+               "Google UK-Extended keyboard layout table for Chrome OS",
+               "with the AltGr modifier key.",
+       ],
+       image => 'data/keyboard/thumb/chromebook-hp11.jpg',
+       imagealt => 'AltGr key on a HP Chromebook 11 G2',
+}
diff --git a/keyboard/altgr/windows.eng.inc.pl b/keyboard/altgr/windows.eng.inc.pl
new file mode 100644 (file)
index 0000000..eb021b7
--- /dev/null
@@ -0,0 +1,98 @@
+use utf8;
+use strict;
+use warnings;
+use Shiar_Sheet::KeyboardChars 'kbchars';
+
+my @az = ('A'..'Z', 'a'..'z');
+my @letters = qw(
+       Á B ¢ Ð É F G H Í J Œ Ø µ Ñ Ó Ö Ä ® § Þ Ú V Å X Ü Æ
+       á b © ð é f g h í j œ ø µ ñ ó ö ä ® ß þ ú v å x ü æ
+);
+my %xkb = (
+       '@' => "\N{COMBINING DOUBLE ACUTE ACCENT}",
+       '#' => "\N{COMBINING MACRON}",
+       '&' => "\N{COMBINING HORN}",
+       '*' => "\N{COMBINING OGONEK}",
+       '(' => "\N{COMBINING BREVE}",
+       ')' => "\N{COMBINING RING ABOVE}",
+       '_' => "\N{COMBINING DOT BELOW}",
+       '>' => "\N{COMBINING CARON}",
+       '.' => "\N{COMBINING DOT ABOVE}",
+       '?' => "\N{COMBINING HOOK ABOVE}",
+       # 1.7~39 (2009-06-12)
+       '%' => "\N{COMBINING CEDILLA}",
+       # 2.38~16 (2023-01-13)
+       'R' => '™',
+       # 2.39~101 (2023-03-17)
+       'f' => 'ë',
+       'F' => 'Ë',
+       'j' => 'ï',
+       'J' => 'Ï',
+       # 2.39~96 (2023-03-21)
+       'M' => '±',
+       # 2.40~122 (2023-06-11)
+       'x' => '·',
+       'X' => "\N{COMBINING SHORT SOLIDUS OVERLAY}", # dead_stroke (slash ø, bar ʉ, stroke ł)
+);
+
+my %rows = (
+       '~' => "\N{COMBINING TILDE}",
+       '`' => "\N{COMBINING GRAVE ACCENT}",
+       '!' => '¹',
+       '1' => '¡',
+       '2' => '²',
+       '3' => '³',
+       '$' => '£',
+       '4' => '¤',
+       '5' => '€',
+       '^' => "\N{COMBINING CIRCUMFLEX ACCENT}",
+       '6' => '¼',
+       '7' => '½',
+       '8' => '¾',
+       '9' => '‘',
+       '0' => '’',
+       '-' => '¥',
+       '+' => '÷',
+       '=' => '×',
+       '{' => '“',
+       '}' => '”',
+       '[' => '«',
+       ']' => '»',
+       ':' => '°',
+       ';' => '¶',
+       "'" => "\N{COMBINING ACUTE ACCENT}",
+       '"' => "\N{COMBINING DIAERESIS}",
+       '<' => 'Ç',
+       ',' => 'ç',
+       '/' => '¿',
+       '|' => '¦',
+       '\\'=> '¬',
+       (map {
+               ($az[$_] eq $letters[$_]) ? () :
+               ($az[$_] => $letters[$_])
+       } 0 .. $#az),
+       %xkb,
+);
+
+my $groups = kbchars(\%rows);
+$groups->{def}{''}{$_} .= ' ext' for keys %xkb;
+$groups->{flag}{ext} = ['xkb', 'unofficial extensions added in Linux (Gnome, KDE)'];
+
++{
+       %{$groups},
+       title => 'Windows AltGr',
+       category => '1/latin/windows/xorg',
+       version => '1.4',
+       intro => join("\n",
+               'Resulting <a href="/unicode">selection</a> of <a href="/charset">Unicode</a> characters',
+               'while pressing the AltGr modifier',
+               'with the Windows US international layout.',
+               'Macs have <a href="/keyboard/altgr/macos">option</a> options instead.',
+       ),
+       description => [
+               "Windows US international keyboard layout table",
+               "with the AltGr modifier key.",
+       ],
+       image => 'data/keyboard/thumb/ku2971b-usint.jpg',
+       imagealt => 'AltGr on a KeyboardCompany KU2971B with USA International keycaps',
+}
similarity index 96%
rename from less.eng.inc.pl
rename to keyboard/less.eng.inc.pl
index fed0bd26174638a24d3c9aa88bd100e5c55e2ade..9000189d42a76152cb675cc93e155b9f068d55a8 100644 (file)
@@ -2,6 +2,14 @@ use utf8;
 
 {
 # less v418
+title => 'Less',
+version => '1.1',
+description => [
+       "Default bindings of the less pager.",
+       "Clearly shows how much it's more than more.",
+],
+keywords => [qw' less keys pager more options '],
+rows => [1, 0],
 
 key => {
        "\e"=> "alt<>/meta",
similarity index 79%
rename from mplayer.eng.inc.pl
rename to keyboard/mplayer.eng.inc.pl
index 1c2e170998a4ccac398c29a85044e76e85f82cec..9568ed8513adaeb09384c5e7a0752fada3b5ddd6 100644 (file)
@@ -1,6 +1,19 @@
 use utf8;
 
 {
+title => 'MPlayer',
+version => '1.3',
+intro => join("\n",
+       'Default interface controls for the original MPlayer v1.0 media player.',
+       'Mostly inherited by <a href="/mplayer/mpv">mpv</a>.',
+),
+description => [
+       "Keyboard cheat sheet for the $mode media player,",
+       "overviewing the default controls.",
+],
+keywords => [qw'mpv mplayer mplayer2 media player video audio'],
+rows => [1, 0],
+
 key => {
        '[' => "slow down 10%",
        ']' => "speed up 10%",
@@ -11,8 +24,8 @@ key => {
        'p' => "pause",
        '.' => "step forward",
        'q' => "stop and quit",
-       '+' => "audio delay +<>.1s",
-       '-' => "audio delay -<>.1s",
+       '+' => "audio delay +<>.1s", # +ctrl in mpv
+       '-' => "audio delay -<>.1s", # +ctrl in mpv
        '/' => "volume decrease",
        '*' => "volume increase",
        'm' => "mute sound",
@@ -39,8 +52,8 @@ key => {
        's' => "screen<>shot\n-vf screenshot",
        'S' => "record screen<>shot<>s\n-vf screenshot",
        'I' => "filename",
-       '!' => "chapter back",
-       '@' => "chapter forward",
+       '!' => "chapter back", # also pgdn in mpv
+       '@' => "chapter forward", # also pgup in mpv
        '1' => "contrast less",
        '2' => "contrast more",
        '3' => "brighter",
@@ -63,7 +76,7 @@ flag => {
        g7 => [playback  => "Playback control."],
        g9 => [general   => "Other MPlayer features."],
 
-       arg => ["key<arg>" => "Commands with a dot need an argument afterwards."],
+       arg => ["key<arg>" => "Commands with a dot read further input afterwards."],
        ext => ["optional" => "Some features depend on setup and/or parameters."],
 },
 
diff --git a/keyboard/mpv.eng.inc.pl b/keyboard/mpv.eng.inc.pl
new file mode 100644 (file)
index 0000000..11d4aff
--- /dev/null
@@ -0,0 +1,80 @@
+use utf8;
+
+my $legacy = do 'keyboard/mplayer.eng.inc.pl' or die $@;
+
+{
+title => 'mpv',
+version => '1.3',
+intro => join("\n",
+       'Default interface controls for version 0.35 of the mpv media player.',
+       'Differences from the original <a href="/mplayer">MPlayer</a> are indicated.',
+),
+keywords => $legacy->{keywords},
+rows => [1, 0],
+
+key => { %{ $legacy->{key} },
+       ',' => "step backward<>s",
+       'Q' => "save and quit",
+       '_' => "cycle video tr<>ack<>s",
+       '+' => "audio delay +<>.1s", # +ctrl in mpv
+       '-' => "audio delay -<>.1s", # +ctrl in mpv
+       'o' => "osd state switch",
+       'O' => "osd mode toggle",
+       'd' => "deint<>erlace",
+       'E' => "edition cycle",
+       'i' => "stats info",
+       'I' => "toggle stats info",
+       'j' => "next sub<>title",
+       'J' => "prev<>ious sub<>title",
+       'A' => "aspect override",
+       'u' => "subtitle style",
+       'V' => "subtitle aspect",
+       'l' => "A-B loop",
+       'L' => "infinite looping",
+       'S' => "record screen<>shot<>s\n-vf screenshot", # mpv?
+       '^s'=> "actual screen<>shot",
+       '5' => "gamma decrease",
+       '6' => "gamma increase",
+       'F' => 'enlarge sub font',
+       'G' => 'smaller sub font',
+       '`' => 'console',
+},
+
+mode => $legacy->{mode},
+
+flag => { %{ $legacy->{flag} },
+       new => ["mpv"      => "Introduced in <em>mpv</em>, not supported in original MPlayer"],
+},
+
+def => {
+       '' => { %{ $legacy->{def}->{''} },
+               '`' => 'g9 arg new',
+               'b' => undef,
+               'g' => undef,
+               'y' => undef,
+               'F' => 'g2 new',
+               'G' => 'g2 new',
+               'a' => undef,
+               'c' => undef,
+               'n' => undef,
+               'i' => 'g1 new',
+               'I' => 'g1 new',
+               ',' => 'g7 new',
+               '_' => 'g4 new',
+               'Q' => 'g9 new',
+               'O' => 'g1 new',
+               'P' => '=o new',
+               'J' => 'g2 new',
+               'A' => 'g4 new',
+               'u' => 'g2 new',
+               'V' => 'g2 new',
+               'l' => 'g7 new',
+               'L' => 'g7 new',
+               'E' => 'g4 new',
+               '^s'=> 'g9 ext new',
+               'R' => '=t new',
+               'Z' => '=x new',
+               'W' => '=e new',
+       },
+},
+}
similarity index 87%
rename from mutt.eng.inc.pl
rename to keyboard/mutt.eng.inc.pl
index 2c58283b654e100c4f0d416aacb742c1877e4e9a..4c94c09349a2af06d8afb51956217f1e09a003e3 100644 (file)
@@ -59,6 +59,14 @@ my %commondef = (
 );
 
 {
+title => 'Mutt',
+version => '1.3',
+description => [
+       "Cheat sheet for the Mutt v2.2 e-mail client,",
+       "showing the default binding for each key.",
+],
+keywords => [qw' mutt MUA email client '],
+
 key => {
        %commonkey,
 
@@ -69,6 +77,10 @@ key => {
        '#' => "split up thread",
        '&' => "thread tagged",
        '%' => "toggle reado<>nl<>y",
+       '-' => "collapse thread",
+       '_' => "collapse all",
+       "'" => "marked mes<>sag<>e",
+       '~' => "save mark",
 
        'a' => "create alias",
        'b' => "bounce",
@@ -91,6 +103,7 @@ key => {
        'g' => "reply to all\nGroup reply",
        'G' => "fetch POP\nGather new mail",
        'h' => "toggle headers",
+       'i' => "fetch IMAP",
        '^i'=> "unread msg <down>",
        '+^i'=> "unread msg <up>",
        'j' => "messag<>e <down>",
@@ -101,7 +114,8 @@ key => {
        '+k'=> "send public key",
        'l' => "limit pattern",
        'L' => "reply to list",
-       '+l'=> "show current limit",
+       '+l'=> "mlist actions",
+       #'+l'=> "show current limit",
        'm' => "compo<>s<>e mail",
        'N' => "toggle new",
        '^n'=> "thread <down>",
@@ -136,12 +150,14 @@ key => {
        'W' => "clear flag",
        'x' => "abort",
        'y' => "list incoming mailboxes",
+       'Y' => "edit label",
 
        (map { 'm'.$_ => $commonkey{$_} } keys %commonkey),
 
        'm|' => "pipe attach<>m<>ent",
        'ma' => "attach file",
        'mA' => "attach messag<>e",
+       'm^b'=> "url<>view",
        'mb' => "edit bcc",
        'mc' => "edit cc",
        'mC' => "copy file",
@@ -158,22 +174,30 @@ key => {
        'mG' => "get attach<>m<>ent",
        'mh' => "display message",
        'mi' => "run ispell",
+       'm+k'=> "attach PGP key",
        'ml' => "print attach<>m<>ent",
        'mm' => "edit attach<>m<>ent\nMime-appropriate open",
        'mM' => "edit mix",
        'm^m'=> "view attach<>m<>ent",
        'mn' => "new attach<>m<>ent",
+       'mo' => "autocrypt toggle", #TODO
+       'm^o'=> "rename attach<>m<>ent",
+       'mp' => "PGP menu",
        'mP' => "post<>pone",
        'mr' => "edit reply<>-to",
        'mR' => "rename attach<>m<>ent",
        'ms' => "edit subject",
        'mS' => "s/mime options",
        'mt' => "edit to",
+       'mT' => "enter tags", #TODO
        'm^t'=> "ctype attach<>m<>ent",
-       'mw' => "copy to folder",
        'mu' => "unlink toggle",
+       'mv' => "preview alt fil<>t<>er", # m+v mV
+       'mV' => "preview mailcap filter",
+       'mw' => "copy to folder",
        'mU' => "encode attach<>m<>ent",
        'my' => "send",
+       # ^xe
 
        'wD' => "deleted",
        'wN' => "new",
@@ -202,6 +226,7 @@ key => {
        '/~L' => "receiv<>d by",
        '/~l' => "mailing list",
        '/~m' => "num<>ber ran<>g<>e",
+       '/~M' => "mime type",
        '/~n' => "score range",
        '/~N' => "new",
        '/~O' => "old",
@@ -225,6 +250,8 @@ key => {
        '/~=' => "dupli<>cate",
        '/~$' => "unrefer<>enced",
        '/~(' => "in thread",
+       '/~<' => "parent match",
+       '/~>' => "child match",
 
        # globally label escape as meta key
          "\e"=> "+",
@@ -237,7 +264,7 @@ mode => {
        '' => 'index',
        'm' => 'compose (m)',
        w => 'message flags (w)',
-       '/~' => 'search flags (/~)',
+       '/~' => 'search patterns (/~)',
 },
 
 flag => {
@@ -264,6 +291,10 @@ def => {
                '#' => 'g4', # break-thread
                '&' => 'g4', # link-threads
                '%' => 'g4', # toggle-write
+               '-' => 'g9',
+               '_' => 'g9',
+               '~' => 'g4 arg', # mark-message #TODO
+               "'" => 'g3 arg',
                ' ' => '=^m',
 
                'a' => 'g6', # create-alias
@@ -275,7 +306,7 @@ def => {
                '+c'=> 'g8', # change-folder-readonly
                '+C'=> undef, # decode-copy
                'd' => 'g4', # delete-message
-               'D' => 'g4 arg', # delete-pattern
+               'D' => 'g4 arg mode/~', # delete-pattern
                '^d'=> 'g4', # delete-thread
                '+d'=> 'g4', # delete-subthread
                'e' => 'g4 linkvi', # edit
@@ -287,6 +318,7 @@ def => {
                'g' => 'g7 modem', # group-reply
                'G' => 'g6', # fetch-mail
                'h' => 'g9', # display-toggle-weed
+               'i' => 'g6', # imap-fetch-mail
                '^i'=> 'g3', # next-new-then-unread
                '+^i'=> undef, # previous-new-then-unread
                'j' => 'g2', # next-undeleted
@@ -295,9 +327,9 @@ def => {
                'K' => 'g3', # previous-entry
                '^k'=> 'g1', # extract-keys
                '+k'=> 'g7 modem', # mail-key
-               'l' => 'g9', # limit
+               'l' => 'g9 arg mode/~', # limit
                'L' => 'g7 modem', # list-reply
-               '+l'=> 'g1', # show-limit
+               '+l'=> 'g1', # list-actions/show-limit
                'm' => 'g7 modem', # mail
                'N' => 'g4', # toggle-new
                '^n'=> 'g3', # next-thread
@@ -317,11 +349,11 @@ def => {
                '+r'=> 'g4', # read-subthread
                's' => 'g4', # save-message
                '+s'=> 'g4', # decode-save
-               'T' => 'g4 arg', # tag-pattern
-               '^t'=> 'g4', # untag-pattern
+               'T' => 'g4 arg mode/~', # tag-pattern
+               '^t'=> 'g4 arg mode/~', # untag-pattern
                '+t'=> 'g4', # tag-thread
                'u' => 'g4', # undelete-message
-               'U' => 'g4 arg', # undelete-pattern
+               'U' => 'g4 arg mode/~', # undelete-pattern
                '^u'=> 'g4', # undelete-thread
                '+u'=> 'g4', # undelete-subthread
                'v' => 'g1', # view-attachments
@@ -332,15 +364,17 @@ def => {
                'W' => 'g4 arg modew', # clear-flag
                'x' => 'g8', # exit
                'y' => 'g8', # M <change-folder>?<toggle-mailboxes>
+               'Y' => 'g4 arg', # edit-label
        }, # index
 
        'm' => {
                %commondef,
 
                '|' => 'g4',
-               'a' => 'g6',
-               'A' => 'g6',
+               'a' => 'g4',
+               'A' => 'g4',
                'b' => 'g4',
+               '^b'=> 'g1',
                'c' => 'g4',
                'C' => 'g6',
                'd' => 'g4',
@@ -356,20 +390,28 @@ def => {
                'G' => 'g1',
                'h' => 'g1',
                'i' => 'g6',
+               '+k'=> 'g4',
                'l' => 'g1',
                'm' => 'g4',
                'M' => 'g4',
                '^m'=> 'g1',
                'n' => 'g6',
+               'o' => 'g4 ext',
+               '^o'=> 'g4',
+               'p' => 'g1', #TODO
                'P' => 'g8 mode',
                'r' => 'g4',
                'R' => 'g4',
                's' => 'g4',
                'S' => 'g4 menumS',
                't' => 'g4',
+               'T' => 'g4',
                '^t'=> 'g4',
                'w' => 'g6',
                'u' => 'g6',
+               'v' => 'g1',
+               '+v'=> 'g1',
+               'V' => 'g1',
                'U' => 'g4',
                'y' => 'g7',
        }, # compose
@@ -410,6 +452,7 @@ def => {
                L => 'g3 arg',
                l => 'g3',
                m => 'g3 arg',
+               M => 'g3 arg',
                n => 'g3 arg',
                N => 'g3',
                O => 'g3',
@@ -433,6 +476,8 @@ def => {
                '=' => 'g3',
                '$' => 'g3',
                '(' => 'g3 arg',
+               '<' => 'g3 arg',
+               '>' => 'g3 arg',
        }, # search option
 },
 }
similarity index 85%
rename from nethack.eng.inc.pl
rename to keyboard/nethack.eng.inc.pl
index e54a6390a097ab0f7420c28e8bb030fe25c77d08..19aa7a3110579eebc69232af87efef3e8e6dc28c 100644 (file)
@@ -1,6 +1,17 @@
 use utf8;
 
 {
+title => 'NetHack',
+version => '1.2',
+description => [
+       "Keyboard overview sheet for the NetHack console roguelike game,",
+       "describing the default controls.",
+],
+intro => "Command bindings for version 3.6.1 of the vanilla NetHack game.",
+keywords => [qw' nethack rogue game controls '],
+rows => [3, 2, 1, 0],
+moderows => '4321-421',
+
 key => {
        'b' =>   "step left down\nnumpad 1",
        'j' =>        "step down\nnumpad 2",
@@ -31,6 +42,7 @@ key => {
 
        'g' => "run until interest",
        'm' => "m<>ove blind\nno pickup",
+       'M' => "go far",
        'G' => "g any<>where\nfollow branches",
 
        '?' => "help menu",
@@ -49,6 +61,7 @@ key => {
        '+a'=> "adjust inv<>ent<>ory",
        'c' => "close door",
        'C' => "call monster",
+       '^c'=> "panic quit",
        '+c'=> "chat",
        'd' => "drop item",
        'D' => "drop items",
@@ -69,6 +82,7 @@ key => {
        '+n'=> "name item<>(s)",
        'o' => "open door",
        'O' => "options",
+       '^o'=> "overview levels",
        '+o'=> "offer sacrifice",
        'p' => "pay bill",
        'P' => "put on",
@@ -116,6 +130,7 @@ key => {
        '$' => "count gold",
        '+' => "list spells",
        '\\'=> "discover<>ed obj<>ects",
+       '`' => "types ident<>if<>ied",
        '!' => "shell escape",
        '#' => "more comm<>ands",
 
@@ -133,10 +148,16 @@ key => {
        'Iu' => 'list unpaid',
        'Ix' => 'list billed',
        'I$' => 'count money',
+       'IB' => 'list blessed',
+       'IU' => 'list uncurs<>e<>d',
+       'IC' => 'list cursed',
+       'IX' => 'list un<>know<>n',
 },
 
 mode => {
        '' => 'normal gameplay',
+       'I' => 'inventory type',
+       'D' => 'drop item type',
 },
 
 flag => {
@@ -183,6 +204,7 @@ def => {
 
                'g' => 'g3 argm',
                'm' => 'g3 argm',
+               'M' => 'g3 argm',
                'G' => 'g3 argm',
 
                '?' => 'g8 mode?', #help
@@ -201,6 +223,7 @@ def => {
                '+a'=> 'g6', #adjust
                'c' => 'g4 argm', #close
                'C' => 'g4 arg', #call
+               '^c'=> 'g6',
                '+c'=> 'g4 arg', #chat
                'd' => 'g4 argi', #drop
                'D' => 'g4 arg modeD', #Drop
@@ -221,7 +244,9 @@ def => {
                '+n'=> 'g7 arg', #name
                'o' => 'g4 argm', #open
                'O' => 'g8 modeO', #options
+               '^o'=> 'g6',
                '+o'=> 'g4 argi', #offer
+               '+O'=> "=^o", #overview
                'p' => 'g4', #pay
                'P' => 'g4 argi', #puton
                '^p'=> 'g6', #prevmsg
@@ -259,20 +284,23 @@ def => {
                ',' => 'g4', #pickup
                '@' => 'g6',
                '^' => 'g6 argm', #trap_id
-               ')' => 'g7',
-               '[' => 'g7',
-               '=' => 'g7',
-               '"' => 'g7',
-               '(' => 'g7',
-               '*' => 'g7',
-               '$' => 'g6', #gold
-               '+' => 'g7', #spells
+               ')' => 'g7', #seeweapon
+               '[' => 'g7', #seearmor
+               '=' => 'g7', #seerings
+               '"' => 'g7', #seeamulet
+               '(' => 'g7', #seetools
+               '*' => 'g7', #seeall
+               '$' => 'g6', #seegold
+               '+' => 'g7', #seespells
                '\\'=> 'g7', #known
+               '`' => 'g7', #knownclass
                '!' => 'g6', #sh
                '#' => 'g8',
        },
 
        'D' => {
+               "\e" => 'g8 mode', # static reset button, even though it's not (officially) in the game
+
                'B' => 'g4',
                'U' => 'g4',
                'C' => 'g4',
@@ -285,10 +313,16 @@ def => {
        },
 
        'I' => {
+               "\e" => 'g8 mode',
+
                '*' => 'g6',
                'u' => 'g6',
                'x' => 'g6',
                '$' => 'g6',
+               'B' => 'g6',
+               'U' => 'g6',
+               'C' => 'g6',
+               'X' => 'g6',
        },
 },
 }
similarity index 78%
rename from readline.eng.inc.pl
rename to keyboard/readline.eng.inc.pl
index 4b2888505a1d8bff9fd7f6c677cdb0cb52427189..5e5b0cd18f32b98086ed5d55f0ba35bfd3afd68b 100644 (file)
@@ -1,6 +1,16 @@
 use utf8;
 
 {
+title => 'readline',
+version => 1.2,
+description => [
+       "Reference sheet of default key bindings for GNU readline,",
+       "used for line-editing in most Unix software, notably Emacs and Bash.",
+],
+keywords => [qw( readline gnu bash emacs editing curses )],
+rows => [4, 3, 2],
+moderows => '^x=213',
+
 key => {
        '+<' => "history start",
        '+>' => "history end",
@@ -94,8 +104,8 @@ flag => {
        g9 => [mode    => "Additional key functionality (click to view)."],
 
        arg => ["key<arg>" => "Commands with a dot need a char argument afterwards."],
-       new => ["&gt;v2.0" => "Unavailable before readline version 2.1 (1997)."],
-       ext => ["bash" => "Default assignment in Bash shells, but not common readline."],
+       'v21 new' => ["&gt;v2.0" => "Unavailable before readline version 2.1 (1997)."],
+       'xbash ext' => ["bash" => "Default assignment in Bash shells, but not common readline."],
 },
 
 def => {
@@ -103,37 +113,37 @@ def => {
                "\e" => 'g8',
 
                '+<' => 'g4',
-               '+=' => '=+?', #TODO: new # emacs, not in v2.0
+               '+=' => '=+?', #TODO: v21 # emacs, not in v2.0
                '+>' => 'g4',
                '^?' => '=^h',
                '+?' => 'g1',
-               '^@' => 'g8 new', # not in v2.0
+               '^@' => 'g8 v21', # not in v2.0
 
-               '+#' => 'g6 new', # not in v2.0
+               '+#' => 'g6 v21', # not in v2.0
                '+&' => 'g6',
-               '+*' => 'g6 new', # not in v2.0
+               '+*' => 'g6 v21', # not in v2.0
                '+.' => 'g4',
                '+~' => '=+&amp;', # emacs
 
                '^[' => 'g8',
                '^+['=> '=^i',
                '+\\'=> 'g7',
-               '^]' => 'g2 arg new', # not in v2.0
-               '^+]'=> 'g2 arg new', # not in v2.0
+               '^]' => 'g2 arg v21', # not in v2.0
+               '^+]'=> 'g2 arg v21', # not in v2.0
                '^_' => 'g4',
                '+_' => '=+.',
 
-               '+~' => 'g6 ext', # common emacs => '=+&'
-               '+!' => 'g6 ext',
-               '+@' => 'g6 ext',
-               '+$' => 'g6 ext',
-               '+^' => 'g4 ext',
-               '+/' => 'g1 ext',
+               '+~' => 'g6 xbash', # common emacs => '=+&'
+               '+!' => 'g6 xbash',
+               '+@' => 'g6 xbash',
+               '+$' => 'g6 xbash',
+               '+^' => 'g4 xbash',
+               '+/' => 'g1 xbash',
 
                '^a' => 'g2',
                '^b' => 'g2',
                '+b' => 'g2',
-               '^c' => 'g8 ext',
+               '^c' => 'g8 xbash',
                '+c' => 'g6',
                '^d' => 'g7',
                '+d' => 'g7 ring',
@@ -156,7 +166,7 @@ def => {
                '^+m'=> '=^+j',
                '^n' => 'g4',
                '+n' => 'g4',
-               '^o' => 'g4 ext',
+               '^o' => 'g4 xbash',
                '^p' => 'g4',
                '+p' => 'g4',
                '^q' => '=^v',
@@ -174,7 +184,7 @@ def => {
                '^y' => 'g4',
                '+y' => 'g4 ring',
                '^+y'=> "yank arg",
-               '^z' => 'g8 ext',
+               '^z' => 'g8 xbash',
        },
 
        '^x' => {
@@ -185,13 +195,13 @@ def => {
                '('  => 'g8',
                ')'  => 'g8',
                'e'  => 'g8',
-               '^e' => 'g6 ext linkvi',
+               '^e' => 'g6 xbash linkvi',
                '^g' => '=^g',
                '^h' => '=^u',
                '^r' => 'g8',
                '^u' => '=^_',
-               '^x' => 'g2 new', # not in v2.0
-               '^v' => 'g1 ext',
+               '^x' => 'g2 v21', # not in v2.0
+               '^v' => 'g1 xbash',
                '^?' => '=^x^h',
        },
 },
similarity index 91%
rename from screen.eng.inc.pl
rename to keyboard/screen.eng.inc.pl
index 7198b250014f67165a652c4101bbccac48b8530d..893adf319164a58f492eaf674bd31986e4bdb9d6 100644 (file)
@@ -2,6 +2,13 @@ use utf8;
 
 {
 # screen version 4.00.03jw4
+title => 'Screen',
+version => '1.1',
+description => [
+       "Interactive cheat sheet for the Screen terminal manager,",
+       "describing the function of each key.",
+],
+keywords => [qw' screen terminal window manager '],
 
 key => {
        'a' => "literal a",
@@ -16,7 +23,7 @@ key => {
        'f' => "flow", # flow
        'F' => "fit",
        '^g'=> "vbell", # vbell
-       'h' => "hardcopy", # hardcopy
+       'h' => "hard<>copy", # hardcopy
        'H' => "log", # log
        'i' => "prop<>erties", # info
        'I' => "login on", #XXX
@@ -53,7 +60,7 @@ key => {
        '|' => "split vert", # split -v
        '{' => "history", # history
        '}' => "history", # history
-       '=' => "remove buf", # removebuf
+       '=' => "remove buf<>fer", # removebuf
        '*' => "displays", # displays
        '.' => "dump<>termcap", # dumptermcap
        ',' => "license", # license
@@ -73,7 +80,7 @@ flag => {
        g3 => ["select", "Switch between existing windows."],
        g4 => ["config", "Toggle configuration flags to change behaviour of the current window."],
        g5 => ["window", "Operate on or in current window with lasting results."],
-       g6 => ["buffer", "..."],
+       g6 => ["buffer", "Related to the paste buffer or an exchange file"],
        g7 => ["contents", "Insert or read character contents on a screen."],
        g9 => ["screen", "Manipulate global and inter-screen commands."],
 
similarity index 68%
rename from vi.eng.inc.pl
rename to keyboard/vi.eng.inc.pl
index 48a3832cc2046d2c615ffe1154e2d6bd29c62413..58ac2f39d37e0b7a458c4aac0d7ceeec170a453f 100644 (file)
@@ -1,6 +1,20 @@
 use utf8;
 
+my @motions = qw(
+       g z [ ]
+       b B e E f F G ^h h H j ^j k l L M ^m n N ^n ^p t T w W
+       0 ` ' # $ % ^ * ( ) { } ; / ? + - _ | ,
+);
+
 {
+title => 'vi/vim',
+version => 1.5,
+description => [
+       "Interactive cheat sheet for vi text editors, notably Vim,",
+       "describing each key in various modes.",
+],
+keywords => [qw' vi vim nvi '],
+
 key => {
 
        "\e"=> "normal mode",
@@ -242,7 +256,6 @@ key => {
        '[^i'=> "to first occur<>renc<>e",
        '[m' => "start of funct<>ion",
        '[p' => "P reind<>ent<>ed",
-       '[P' => "[p",
        '[s' => "last missp<>ell<>ing",
        '[S' => "last bad word",
        '[z' => "start of open fold",
@@ -339,7 +352,6 @@ key => {
        'vD' => "delete lines",
        'vg' => "extra cmds",
        'v^g'=> "select mode",
-       'v^h'=> "back<>space", # delete in select mode, left otherwise
        'vi' => "extend inner a<>r<>ea",
        'vI' => "insert to block", # block
        'vJ' => "join lines",
@@ -448,6 +460,98 @@ key => {
        'i^x^u' => "cus<>tom comp<>l<>et<>e",
        'i^x^v' => "ex cmd compl<>et<>e",
        'i^x^y' => "window down",
+
+       # plugins
+         'gc' => "(un)<>com<>ment\ncommentary or tComment plugin",
+        'vgc' => "(un)<>com<>ment\ncommentary or tComment plugin",
+         'gl' => "align to left\nlion plugin, also good for easy-align",
+        'vgl' => "align to left\nlion plugin, also good for easy-align",
+         'gL' => "align to right\nlion plugin",
+        'vgL' => "align to right\nlion plugin",
+       'i^gs' => "sur<>round\nsurround plugin",
+
+       # unimpaired
+       '[a' => "prev<>ious file\nunimpaired map for :prev",
+       ']a' => "next file\nunimpaired map for :n",
+       '[A' => "first file\nunimpaired map for :rew",
+       ']A' => "last file\nunimpaired map for :la",
+       '[b' => "prev<>ious buffer\nunimpaired map for :bp",
+       ']b' => "next buffer\nunimpaired map for :bn",
+       '[B' => "first buffer\nunimpaired map for :br",
+       ']B' => "last buffer\nunimpaired map for :bl",
+       '[e' => "exchange line above\nunimpaired plugin",
+       ']e' => "exchange line belo<>w\nunimpaired plugin",
+       '[f' => "preced<>ing file alph<>abet<>ic<>al<>ly\nunimpaired plugin",
+       ']f' => "next file alph<>abet<>ic<>al<>ly\nunimpaired plugin",
+       '[l' => "previous loc<>at<>ion\nunimpaired map for :lp",
+       ']l' => "next loc<>ation\nunimpaired map for :lne",
+       '[L' => "first loc<>ation\nunimpaired map for :lr",
+       ']L' => "last loc<>ation\nunimpaired map for :lla",
+       '[^l'=> "next file in loc<>at<>ions\nunimpaired map for :lpf",
+       ']^l'=> "file back in loc<>at<>ions\nunimpaired map for :lnf",
+       '[n' => "previous conflict<>/hunk\nunimpaired plugin",
+       ']n' => "next confl<>ict<>/hunk\nunimpaired plugin",
+       '[o' => "enable option\nunimpaired plugin",
+       ']o' => "disable option\nunimpaired plugin",
+       'yo' => "toggle option\nunimpaired plugin",
+       '[q' => "previous quickfix\nunimpaired map for :cp",
+       ']q' => "next quickfix error\nunimpaired map for :cn",
+       '[Q' => "first quickfix\nunimpaired map for :cr",
+       ']Q' => "last quickfix error\nunimpaired map for :cla",
+       '[^q'=> "quickfix file b<>ack\nunimpaired map for :cpf",
+       ']^q'=> "next file in q<>uick<>f<>ix\nunimpaired map for :cnf",
+       '[t' => "previous tag\nunimpaired map for :tp",
+       ']t' => "next tag\nunimpaired map for :tn",
+       '[T' => "first tag\nunimpaired map for :tr",
+       ']T' => "last tag\nunimpaired map for :tl",
+       '[u' => "url encode\nunimpaired plugin",
+       ']u' => "url decode\nunimpaired plugin",
+       '[x' => "xml encode\nunimpaired plugin",
+       ']x' => "xml decode\nunimpaired plugin",
+       '[y' => "escape c str<>ing\nunimpaired plugin",
+       ']y' => "unescap<>e c str<>ing\nunimpaired plugin",
+
+       (map { ("d$_" => "delete to <alias>$_") } qw( g z [ ] )),
+       'dW' => 'delete <span style="font-variant:small-caps">word</span>',
+       'db' => 'delete <left> word',
+       'dB' => 'delete <left> <span style="font-variant:small-caps">word</span>',
+       'de' => 'delete word e<>nd',
+       'dE' => 'delete <span style="font-variant:small-caps">word</span> e<>nd',
+       'df' => 'delete to char<>acter',
+       'dF' => 'delete <left> to char<>acter',
+       'dG' => 'delete to line<>/eof',
+       'dH' => 'delete to top',
+       'dj' => 'delete <down> line',
+       'dk' => 'delete <up> line',
+       'dL' => 'delete to bottom',
+       'dM' => 'delete to middle',
+       'dn' => 'delete to next res<>ult',
+       'dN' => 'delete to prev res<>ult',
+       'dt' => 'delete upto ch<>ar<>acter',
+       'dT' => 'delete <left> upto ch<>ar<>acter',
+       'dw' => 'delete word',
+       'd,' => 'delete to prev ch<>ar<>acter',
+       'd;' => 'delete to next ch<>ar<>acter',
+       'd/' => 'delete to res<>ult',
+       'd?' => 'delete <left> to res<>ult',
+       'd`' => 'delete to mark',
+       "d'" => 'delete lines to m<>ark',
+       'd*' => 'delete to find',
+       'd#' => 'delete <left> to find',
+       'd%' => 'delete to line pct',
+       'd^' => 'delete to soft bol',
+       'd0' => 'delete to bol',
+       'd|' => 'delete to col<>umn',
+       'd{' => 'delete <left> par<>agr<>aph',
+       'd}' => 'delete rest of p<>ar<>agr<>aph',
+       'd(' => 'delete <left> senten<>ce',
+       'd)' => 'delete rest of s<>en<>ten<>ce',
+       'da' => 'delete area',
+       'di' => 'delete inner',
+       'dd' => 'delete line',
+       'do' => 'diff obtain',
+       'dp' => 'diff put',
+       'ds' => 'delete surr<>oun<>d<>ing',
 },
 
 mode => {
@@ -466,6 +570,7 @@ mode => {
         i    => "insert mode",
        'i^g' => "extended insert commands (i ctrl-g)",
        'i^x' => "insert completion commands (i ctrl-x)",
+        d    => 'delete motions',
 },
 
 flag => {
@@ -480,8 +585,8 @@ flag => {
 
        arg => ["key<arg>", "Commands with a dot need a char argument afterwards."],
        motion => ["key<motion>", "Requires a motion afterwards, operates between cursor and destination."],
-       'vim6 ext' => ["vim", "Not in original Vi (assessment incomplete)."],
-       'vim7 ext new' => ["vim7", "New in vim version 7.x."],
+       'v6 new' => ["vim", "Not in original Vi (assessment incomplete)."],
+       'xcommentary xlion xsurround xunimpaired ext' => ["plugin", "Optional features provided by common plugins."],
 },
 
 def => {
@@ -489,12 +594,12 @@ def => {
                '~' => "g4 undo",
                '!' => "g4 argm undo modec",
                '@' => "g4 arg undo",
-               '#' => "g2 ext vim6",
+               '#' => "g2 v6",
                '$' => "g2",
                '%' => "g2",
                '^' => "g2",
                '&' => "g4 undo",
-               '*' => "g2 ext vim6",
+               '*' => "g2 v6",
                '(' => 'g2',
                ')' => 'g2',
                '_' => "g2",
@@ -528,11 +633,11 @@ def => {
                '^a'=> "g4 undo",
                'b' => "g2",
                'B' => "g2",
-               '^b'=> "g2",
+               '^b'=> "g3",
                'c' => "g6 argm undo modei",
                'C' => "g6 undo modei",
                '^c'=> "g4",
-               'd' => "g4 argm undo",
+               'd' => "g4 argm undo moded",
                'D' => "g4 undo",
                '^d'=> "g3",
                'e' => "g2",
@@ -540,7 +645,7 @@ def => {
                '^e'=> "g3",
                'f' => "g2 arg",
                'F' => "g2 arg",
-               '^f'=> "g2",
+               '^f'=> "g3",
                'g' => "g9 arg modeg",
                'G' => "g2",
                '^g'=> "g1",
@@ -549,7 +654,7 @@ def => {
                '^h'=> "=h",
                'i' => "g6 undo modei",
                'I' => "g6 undo modei",
-               '^i'=> "g2 ext vim6",
+               '^i'=> "g2 v6",
                'j' => "g2",
                'J' => "g4 undo",
                '^j'=> "=j",
@@ -566,7 +671,7 @@ def => {
                '^n'=> "=j",
                'o' => "g6 undo modei",
                'O' => "g6 undo modei",
-               '^o'=> "g2 ext vim6",
+               '^o'=> "g2 v6",
                'p' => "g4 undo",
                'P' => "g4 undo",
                '^p'=> "=k",
@@ -575,7 +680,7 @@ def => {
                '^q'=> "g1", # or ^v
                'r' => "g4 arg undo",
                'R' => "g6 undo modei",
-               '^r'=> "g4 undo ext vim6",
+               '^r'=> "g4 undo v6",
                's' => "g6 undo modei",
                'S' => "g6 undo modei",
                '^s'=> "g1",
@@ -587,7 +692,7 @@ def => {
                '^u'=> "g3",
                'v' => "g8 modev",
                'V' => "g8 modev",
-               '^v'=> "g8 modev ext vim6",
+               '^v'=> "g8 modev v6",
                'w' => "g2",
                'W' => "g2",
                '^w'=> "g9 arg mode^w",
@@ -607,65 +712,68 @@ def => {
 
                "\e"=> "g7 mode",
 
-               '~' => "g4 argm ext vim6",
-               '@' => "g4 ext vim7 new",
-               '#' => "g2 ext vim6",
+               '~' => "g4 argm v6",
+               '@' => "g4 v7",
+               '#' => "g2 v6",
                '$' => "g2",
-               '^' => "g2 ext vim6",
+               '^' => "g2 v6",
                '&' => "g4",
-               '*' => "g2 ext vim6",
-               '_' => "g2 ext vim6",
-               '+' => "g4 ext vim7 new",
-               '`' => "g2 ext vim6 arg",
+               '*' => "g2 v6",
+               '_' => "g2 v6",
+               '+' => "g4 v7",
+               '`' => "g2 v6 arg",
                '8' => "g1",
-               '0' => "g2 ext vim6",
-               '-' => "g4 ext vim7 new",
+               '0' => "g2 v6",
+               '-' => "g4 v7",
                '^['=> "g7 mode",
                ']' => "g3",
                '^]'=> "g3",
                ';' => "g2",
-               "'" => "g2 ext vim6 arg",
-               '<' => "g4 ext vim7 new",
-               '?' => "g4 argm ext vim6",
+               "'" => "g2 v6 arg",
+               '<' => "g4 v7",
+               '?' => "g4 argm v6",
                ',' => "g2",
 
                'a' => "g1",
                '^a'=> "g1",
-               'd' => "g2 ext vim6",
-               'D' => "g2 ext vim6",
+               'c' => 'g4 argm xcommentary',
+               'd' => "g2 v6",
+               'D' => "g2 v6",
                'e' => "g2",
                'E' => "g2",
-               'f' => "g4 ext vim6",
-               'F' => "g4 ext vim7 new",
+               'f' => "g4 v6",
+               'F' => "g4 v7",
                'g' => "g2",
                '^g'=> "g1 vim6",
                'h' => "g8 modev",
                'H' => "g8 modev",
-               '^h'=> "g8 modev ext vim6",
+               '^h'=> "g8 modev v6",
                'i' => "g6 undo modei",
                'I' => "g6 undo modei",
-               'j' => "g2 ext vim6",
+               'j' => "g2 v6",
                'J' => "g4",
-               'k' => "g2 ext vim6",
-               'm' => "g2 ext vim6",
-               'n' => "g8 vim7 new modev",
-               'N' => "g8 vim7 new modev",
+               'k' => "g2 v6",
+               'l' => 'g3 argm arg xlion',
+               'L' => 'g3 argm arg xlion',
+               'm' => "g2 v6",
+               'n' => "g8 v7 modev",
+               'N' => "g8 v7 modev",
                'o' => "g2",
                'p' => "g4 undo",
                'P' => "g4",
                'q' => "g4 argm",
-               'Q' => "g7 ext vim6",
+               'Q' => "g7 v6",
                'r' => "g4 arg",
                'R' => "g6 undo modei",
-               's' => "g1 ext vim6",
-               't' => "g3 ext vim7 new",
-               'T' => "g3 ext vim7 new",
-               'u' => "g4 argm ext vim6", # XXX undo?
-               'U' => "g4 undo argm ext vim6",
+               's' => "g1 v6",
+               't' => "g3 v7",
+               'T' => "g3 v7",
+               'u' => "g4 argm v6", # XXX undo?
+               'U' => "g4 undo argm v6",
                'v' => 'g8 modev',
                'V' => "g4",
-               'w' => "g4 argm ext vim7 new",
-               'x' => "g4 ext vim7 new",
+               'w' => "g4 argm v7",
+               'x' => "g4 v7 ext",
        }, # mode g
 
        Z => {
@@ -674,7 +782,7 @@ def => {
                "\e"=> "g7 mode",
                '^['=> "=\e",
 
-               'Q' => "g4 ext vim6",
+               'Q' => "g4 v6",
                'Z' => "g4",
        }, # mode Z
 
@@ -686,47 +794,47 @@ def => {
                '^' => "g3",
                '+' => "g3",
                '-' => "g3",
-               '=' => "g4 ext vim7 new",
+               '=' => "g4 v7",
                '.' => "g3",
                '^['=> "=\e",
 
-               'a' => "g4 ext vim6 folding",
-               'A' => "g4 ext vim6 folding",
+               'a' => "g4 v6 folding",
+               'A' => "g4 v6 folding",
                'b' => "g3",
-               'c' => "g4 ext vim6 folding",
-               'C' => "g4 ext vim6 folding",
-               'd' => "g4 ext vim6 folding",
-               'D' => "g4 ext vim6 folding",
+               'c' => "g4 v6 folding",
+               'C' => "g4 v6 folding",
+               'd' => "g4 v6 folding",
+               'D' => "g4 v6 folding",
                'e' => "g3",
-               'E' => "g4 ext vim6 folding",
-               'f' => "g4 argm ext vim6 folding",
-               'F' => "g4 ext vim6 folding",
-               'g' => "g4 ext vim7 new",
-               'G' => "g4 ext vim7 new",
+               'E' => "g4 v6 folding",
+               'f' => "g4 argm v6 folding",
+               'F' => "g4 v6 folding",
+               'g' => "g4 v7",
+               'G' => "g4 v7",
                'h' => "g3",
                'H' => "g3",
-               'i' => "g4 ext vim6 folding",
-               'j' => "g2 ext vim6 folding",
-               'k' => "g2 ext vim6 folding",
+               'i' => "g4 v6 folding",
+               'j' => "g2 v6 folding",
+               'k' => "g2 v6 folding",
                'l' => "g3",
                'L' => "g3",
                '^m'=> "g3",
-               'm' => "g4 ext vim6 folding",
-               'M' => "g4 ext vim6 folding",
-               'n' => "g4 ext vim6 folding",
-               'N' => "g4 ext vim6 folding",
-               'o' => "g4 ext vim6 folding",
-               'O' => "g4 ext vim6 folding",
-               'r' => "g4 ext vim6 folding",
-               'R' => "g4 ext vim6 folding",
+               'm' => "g4 v6 folding",
+               'M' => "g4 v6 folding",
+               'n' => "g4 v6 folding",
+               'N' => "g4 v6 folding",
+               'o' => "g4 v6 folding",
+               'O' => "g4 v6 folding",
+               'r' => "g4 v6 folding",
+               'R' => "g4 v6 folding",
                's' => "g3",
                't' => "g3",
-               'u' => "g4 arg ext vim7 new",
-               'v' => "g4 ext vim6 folding",
-               'w' => "g4 ext vim7 new",
-               'W' => "g4 ext vim7 new",
-               'x' => "g4 ext vim6 folding",
-               'X' => "g4 ext vim6 folding",
+               'u' => "g4 arg v7",
+               'v' => "g4 v6 folding",
+               'w' => "g4 v7",
+               'W' => "g4 v7",
+               'x' => "g4 v6 folding",
+               'X' => "g4 v6 folding",
                'z' => "g3",
        }, # mode z
 
@@ -735,12 +843,12 @@ def => {
 
                "\e"=> "g7 mode",
 
-               "`" => "g2 ext vim6",
+               "`" => "g2 v6",
                '#' => "g2",
                '(' => "g2",
                '*' => "=[/",
                '/' => "g2",
-               "'" => "=[` ^", # ext vim6
+               "'" => "=0[`^ g2 v6",
                '{' => "g2",
                '[' => "g2",
                '^['=> "=\e",
@@ -756,10 +864,32 @@ def => {
                '^i'=> 'g3',
                'm' => "g2",
                'p' => "g4",
-               'P' => "g4",
-               's' => "g3 ext vim7 new",
-               'S' => "g3 ext vim7 new",
-               'z' => "g2 ext vim6 folding",
+               'P' => "=[p",
+               's' => "g3 v7",
+               'S' => "g3 v7",
+               'z' => "g2 v6 folding",
+
+               'a' => "g3 xunimpaired",
+               'A' => "g3 xunimpaired",
+               'b' => "g3 xunimpaired",
+               'B' => "g3 xunimpaired",
+               'e' => "g4 xunimpaired",
+               'l' => "g3 xunimpaired",
+               'L' => "g3 xunimpaired",
+               '^l'=> "g3 xunimpaired",
+               '^l'=> "g3 xunimpaired",
+               'f' => "g3 xunimpaired", # overrides native alias
+               'n' => "g3 xunimpaired",
+               'o' => "g9 arg xunimpaired",
+               'q' => "g3 xunimpaired",
+               'Q' => "g3 xunimpaired",
+               '^q'=> "g3 xunimpaired",
+               't' => "g3 xunimpaired",
+               'T' => "g3 xunimpaired",
+               'u' => "g4 argm xunimpaired",
+               'x' => "g4 argm xunimpaired",
+               'y' => "g4 argm xunimpaired",
+               ' ' => "g4 xunimpaired",
        }, # mode [
 
        ']' => {
@@ -767,11 +897,11 @@ def => {
 
                "\e"=> "g7 mode",
 
-               "`" => "g2 ext vim6",
+               "`" => "g2 v6",
                '#' => "g2",
                ')' => "g2",
                '*' => "=]/",
-               "'" => "=]` ^", # ext vim6
+               "'" => '=$]`^ g2 v6',
                '/' => "g2",
                '[' => "g2",
                '^['=> "=\e",
@@ -789,9 +919,31 @@ def => {
                'm' => "g2",
                'p' => "g4",
                'P' => "=[p",
-               's' => "g3 ext vim7 new",
-               'S' => "g3 ext vim7 new",
-               'z' => "g2 ext vim6 folding",
+               's' => "g3 v7",
+               'S' => "g3 v7",
+               'z' => "g2 v6 folding",
+
+               'a' => "g3 xunimpaired",
+               'A' => "g3 xunimpaired",
+               'b' => "g3 xunimpaired",
+               'B' => "g3 xunimpaired",
+               'e' => "g4 xunimpaired",
+               'l' => "g3 xunimpaired",
+               'L' => "g3 xunimpaired",
+               '^l'=> "g3 xunimpaired",
+               '^l'=> "g3 xunimpaired",
+               'f' => "g3 xunimpaired", # overrides native alias
+               'n' => "g3 xunimpaired",
+               'o' => "g9 arg xunimpaired",
+               'q' => "g3 xunimpaired",
+               'Q' => "g3 xunimpaired",
+               '^q'=> "g3 xunimpaired",
+               't' => "g3 xunimpaired",
+               'T' => "g3 xunimpaired",
+               'u' => "g4 argm xunimpaired",
+               'x' => "g4 argm xunimpaired",
+               'y' => "g4 argm xunimpaired",
+               ' ' => "g4 xunimpaired",
        }, # mode ]
 
        '^w' => {
@@ -816,7 +968,7 @@ def => {
                '^c'=> "g7 mode",
                'd' => "g3",
                'f' => "g4",
-               'F' => "g4 ext vim7 new",
+               'F' => "g4 v7",
                'g' => "g9 arg mode^wg",
                'h' => "g3",
                'H' => "g3",
@@ -837,7 +989,7 @@ def => {
                's' => "g3",
                'S' => "=^ws",
                't' => "g3",
-               'T' => "g3 vim7 new",
+               'T' => "g3 v7",
                'v' => "g3",
                'w' => "g3",
                'W' => "g3",
@@ -853,8 +1005,8 @@ def => {
                ']' => "g3",
                '}' => "g3",
                '^]'=> "g3",
-               'f' => "g4 ext vim7 new",
-               'F' => "g4 ext vim7 new",
+               'f' => "g4 v7",
+               'F' => "g4 v7",
        }, # mode ^w g
 
        v => {
@@ -862,30 +1014,26 @@ def => {
 
                '!' => "g4",
                ':' => "g7 modec",
-               ',' => "=,",
-               "'" => "='",
-               '"' => '="',
                '<' => "g4",
                '=' => 'g4 undo',
                '>' => 'g4',
-               '~' => "g4 ext vim6",
+               '~' => "g4 v6",
                '^['=> "=\e",
                '^]'=> "g3",
                '^\\'=>'^\\',
 
-               'a' => 'g9 modeva arg ext vim6',
-               'A' => 'g6 modei ext vim6',
-               '^a'=> 'g4 undo vim8 ext new',
-               'c' => 'g6 modei ext vim6',
-               'C' => 'g6 modei ext vim6',
+               'a' => 'g9 modeva arg v6',
+               'A' => 'g6 modei v6',
+               '^a'=> 'g4 undo v8',
+               'c' => 'g6 modei v6',
+               'C' => 'g6 modei v6',
                '^c'=> "g7 mode",
-               'd' => "g4 ext vim6",
-               'D' => "g4 ext vim6",
+               'd' => "g4 v6",
+               'D' => "g4 v6",
                'g' => "g9 arg modevg",
                '^g'=> "g8",
-               '^h'=> 'g4',
-               'i' => "g9 modeva arg ext vim6",
-               'I' => "g6 modei ext vim6", # block
+               'i' => "g9 modeva arg v6",
+               'I' => "g6 modei v6", # block
                'J' => "g4",
                'K' => 'g4',
                'o' => "g2",
@@ -893,28 +1041,24 @@ def => {
                '^o'=> "g8",
                'p' => 'g4',
                'P' => 'g4',
-               '^q'=> "=^q",
                'r' => 'g4 arg',
                'R' => "=vS",  # "might change in future"
-               '^s'=> "=^s",
                's' => "=vc",
-               'S' => 'g6 modei ext vim6',
-               'u' => "g4 ext vim6",
-               'U' => "g4 ext vim6",
+               'S' => 'g6 modei v6',
+               'u' => "g4 v6",
+               'U' => "g4 v6",
                'v' => "g8",
                'V' => "g8",
-               '^v'=> "g8 ext vim6",
+               '^v'=> "g8 v6",
                'x' => "=vd",
                'X' => "=vD",
-               '^x'=> 'g4 undo vim8 ext new',
+               '^x'=> 'g4 undo v8',
                'y' => "g4",
                'Y' => "g4",
 
-               map { $_ => "=$_" } qw(
-                       b B ^b ^d e E ^e f F ^f G h H ^i j ^j k l L m M ^m
-                       n N ^n ^p q ^s t T ^u w W ^w ^y z ^z
-                       ` # $ % ^ * ( 0 ) [ { ] } ; / ? + - _ |
-               ) # a lot like normal mode
+               (map { $_ => "=$_" } @motions, qw(
+                       ^b ^d ^e ^f ^i m q ^q ^s ^u ^w ^y z ^z "
+               )), # a lot like normal mode
        }, # mode v
 
        vg => {
@@ -923,14 +1067,17 @@ def => {
                "\e"=> "g8 modev",
 
                '^['=> "=\e",
-               '?' => "g4 ext vim6",
-               '^a'=> 'g4 undo vim8 ext new',
+               '?' => "g4 v6",
+               '^a'=> 'g4 undo v8',
                '^g' => "g1 vim6",
-               'J' => 'g4 ext vim6',
-               'q' => "g4 ext vim6",
+               'c' => 'g4 xcommentary',
+               'J' => 'g4 v6',
+               'l' => 'g3 arg xlion',
+               'L' => 'g3 arg xlion',
+               'q' => "g4 v6",
                'v' => "=gv",
-               'w' => 'g4 ext vim7 new',
-               '^x'=> 'g4 undo vim8 ext new',
+               'w' => 'g4 v7',
+               '^x'=> 'g4 undo v8',
        }, # mode v g
 
        va => {
@@ -939,23 +1086,23 @@ def => {
                '(' => "=vab",
                ')' => "=vab",
                '`' => "=va'",
-               "'" => 'g2 ext vim7 new',
+               "'" => 'g2 v7',
                '"' => "=va'",
-               '<' => 'g2 ext vim6',
+               '<' => 'g2 v6',
                '>' => "=va&lt;",
-               '[' => 'g2 ext vim6',
+               '[' => 'g2 v6',
                '{' => "=vaB",
                '^['=> "=\e",
                ']' => "=va[",
                '}' => "=vaB",
 
-               'b' => 'g2 ext vim6',
-               'B' => 'g2 ext vim6',
-               'p' => 'g2 ext vim6',
-               's' => 'g2 ext vim6',
-               't' => 'g2 ext vim7 new',
-               'w' => 'g2 ext vim6',
-               'W' => 'g2 ext vim6',
+               'b' => 'g2 v6',
+               'B' => 'g2 v6',
+               'p' => 'g2 v6',
+               's' => 'g2 v6',
+               't' => 'g2 v7',
+               'w' => 'g2 v6',
+               'W' => 'g2 v6',
        }, # mode v a
 
        #c => {
@@ -976,7 +1123,7 @@ def => {
        #       '^n' => "", #todo
        #       '^p' => "", #todo
        #       '^r' => "=i^r", # and then some...
-       #            ## ["g4 arg ext vim6"],
+       #            ## ["g4 arg v6"],
        #       '^q' => "=^q",
        #       '^u' => "=i^u",
        #       '^v' => "=i^v",
@@ -988,38 +1135,38 @@ def => {
                "\e" => "g7 mode",
 
                '^@' => "g4",
-               '^^' => "g4 ext vim6",
+               '^^' => "g4 v6",
                '^['=> "=\e",
-               '^]' => "g4 ext vim6",
-               '^_' => "g4 ext vim6",
+               '^]' => "g4 v6",
+               '^_' => "g4 v6",
                '^\\'=>'^\\',
 
-               '^a' => "g4 ext vim6",
-               '^b' => "no ext vim6",
+               '^a' => "g4 v6",
+               '^b' => "no v6",
                '^c' => "g7 mode",
                '^d' => 'g4',
-               '^e' => "g4 ext vim6",
+               '^e' => "g4 v6",
                '^f' => 'g4',
                '^g' => "g9 modei^g arg",
                '^h' => "g4",
                '^i' => "g4",
                '^j' => "g4",
-               '^k' => 'g4 arg arg ext vim6 linkdigraphs',
-               '^l' => "g7 mode ext vim6",  # insertmode only
+               '^k' => 'g4 arg arg v6 linkdigraphs',
+               '^l' => "g7 mode v6",  # insertmode only
                '^m' => "g4",
-               '^n' => "g2 ext vim6",
+               '^n' => "g2 v6",
                '^o' => 'g4',
-               '^p' => "g2 ext vim6",
+               '^p' => "g2 v6",
                '^q' => "=^q",  # or i^v
-               '^r' => 'g4 arg ext vim6',
+               '^r' => 'g4 arg v6',
                '^s' => "=^s",
                '^t' => "g4",
                '^u' => "g4",
                '^v' => 'g4 linkcharset',
                '^w' => "g4",
-               '^x' => 'g9 arg modei^x ext vim6',
-               '^y' => "g4 ext vim6",
-               '^z' => "g1 ext vim6",  # insertmode only
+               '^x' => 'g9 arg modei^x v6',
+               '^y' => "g4 v6",
+               '^z' => "g1 v6",  # insertmode only
        }, # modei
 
        'i^g' => {
@@ -1029,8 +1176,9 @@ def => {
 
                'k' => 'g2',
                'j' => 'g2',
+               's' => 'g4 arg xsurround',
                'u' => 'g4',
-               'U' => 'g4 vim8 ext new',
+               'U' => 'g4 v8',
                # other keys (even esc) are not recognized
        }, # mode i ^g
 
@@ -1048,16 +1196,45 @@ def => {
                '^k' => 'g4',
                '^l' => 'g4',
                '^n' => 'g4',
-               '^o' => 'g4 ext vim7 new',
+               '^o' => 'g4 v7',
                '^p' => 'g4',
                '^s' => 'g4',
                 's' => "=i^x^s",
                '^t' => 'g4',
-               '^u' => 'g4 ext vim7 new',
+               '^u' => 'g4 v7',
                '^v' => 'g4',
                '^y' => "g3",
        }, # mode i ^x
 
+       d => {
+               lead => "d",
+
+               "\e"=> "g7 mode",
+               '^['=> "=\e",
+
+               (map { $_ => 'g4' } @motions),
+               (map { $_ => 'g4 arg' } qw( f F t T ` ' / ? )), # @motions with option
+               (map { $_ => "=v$_" } qw( a i )), # motions from virtual
+               (map { $_ => "=$_" } qw( g z [ ]  \\ ^\\ ^q ^s ^z )),
+
+               'd'  => 'g4',
+               'o'  => 'g5',
+               'p'  => 'g5',
+               's'  => 'g4 arg xsurround',
+
+               'l'  => '=x',
+               'h'  => '=X',
+               '^h' => '=X',
+               '^j' => '=dj',
+               '^m' => '=dj',
+               '^n' => '=dj',
+               '+'  => '=dj',
+               '^p' => '=dk',
+               '-'  => '=dk',
+               '$'  => '=D',
+               '_'  => '=dd',
+       }, # mode d
+
        # TODO: mode/ (command-line)
        # XXX ex mode if you want to go completely wild
 },
similarity index 96%
rename from vimperator.eng.inc.pl
rename to keyboard/vimperator.eng.inc.pl
index 4fc3771a3c30ff49769bb27742e8ede75121fcf7..5338ecd98eae620376ced63e96605da3cbe4763e 100644 (file)
@@ -1,6 +1,14 @@
 use utf8;
 # vimperator v3.16.0
 {
+title => 'Vimperator',
+version => '1.3',
+description => [
+       "Interactive cheat sheet for the Vimperator (or Pentadactyl)",
+       "Firefox extension, describing the function of each key.",
+],
+keywords => [qw'vimperator firefox pentadactyl vim browser vimfx vimium cvim'],
+
 key => {
        '~' => "open home<>dir",
        '@' => "play macro",
index 56f9d03d8a206769aeb12918ab2edff127e9e65c..578d858f73c2b14890ce2a720763a18e9654f8b5 100644 (file)
--- a/latin.plp
+++ b/latin.plp
@@ -2,7 +2,7 @@
 
 Html({
        title => 'latin alphabet cheat sheet',
-       version => '1.4',
+       version => '1.7',
        description => [
        ],
        keywords => [qw'
@@ -47,33 +47,36 @@ or <span title="fuck yeah!">'mercan</span>) letters A–Z.
 Also see <a href="/writing">related alphabets</a>
 and <a href="/chars/abc">font comparison</a>.</p>
 
-<div>
-
 <:
-use List::Util qw( pairs );
-
-my @table = do 'writing-latn.inc.pl';
-if ($! or $@) {
-       Alert("Table data not found", $@ || $!);
-}
-else {
+my $table = Data('writing-latn');
+{
+       say '<div>';
        say '<style>';
-       for my $row (pairs @table) {
-               my ($id, $info) = @{$row};
+       while (my ($id, $info) = each %$table) {
+               ref $info eq 'HASH' or next;
                my $style = $info->{style} or next;
                ref $style or $style = [$style];
                say "\t", !/^@/ && "#$id ", $_ for @{$style};
        }
        say "</style>\n";
+}
 
-       my %VOWELCOLS = (map { ($_ => 1) } 0, 4, 8, 14, 20, 24);
-       say '<table class="glyphs">';
-       say '<thead><tr><th># <small>ASCII − 64</small>';
-       print '<td>', $_ for 1 .. 26;
-       say '</thead>';
-
-       for my $row (pairs @table) {
-               my ($id, $info) = @{$row};
+my %VOWELCOLS = (map { ($_ => 1) } 0, 4, 8, 14, 20, 24);
+say '<table class="glyphs">';
+say '<thead>';
+printtr('order');
+say '</thead>';
+printtr('default');
+say "</table></div>";
+
+sub printtr {
+       for my $id (@_) {
+               my $info = $table->{$id};
+
+               if (ref $info eq 'ARRAY') {
+                       printtr(@{$info});
+                       next;
+               }
 
                printf '<tr id="%s">', $id;
                my $th = 'th';
@@ -109,11 +112,9 @@ else {
                        say;
                }
        }
-       say "</table>\n";
 }
 
-:></div>
-
+:>
 <script type="text/javascript" src="/latinsample.js"></script>
 <script type="text/javascript"><!--
        prependinput(document.getElementById('intro'));
index 406cac12cdb355f3dd38070c9ccf7172ead633ec..846e5004fa97a9b8529c9251c929dcef7006521e 100644 (file)
@@ -44,7 +44,9 @@ function appendsample() {
                                output += cols[col].split('&nbsp;')[final ? 1 : 0];
                        }
                        else if (col < cols.length) {
-                               output += '<span>' + (cols[col] || ' ') + '</span>';
+                               var final = cols[col] || ' ';
+                               if (!/^<svg/.test(cols[col])) final = `<span>${final}</span>`;
+                               output += final;
                        }
                        else {
                                output += '<b> </b>';
index 206de5e85a18e649d1d3554f54daab9b2a24df1f..ef99eb35db9a8038e16e08107b344b9f61d182ee 100644 (file)
--- a/less.plp
+++ b/less.plp
@@ -1,30 +1,3 @@
-<(common.inc.plp)><:
-
-Html({
-       title => 'less cheat sheet',
-       version => '1.1',
-       description => [
-               "Default bindings of the less pager.",
-               "Clearly shows how much it's more than more.",
-       ],
-       keywords => [qw'
-               less sheet cheat keys pager more
-               shortkey reference keyboard commands options overview
-       '],
-       stylesheet => [qw( light dark circus mono red )],
-       keys => 1,
-});
-
-:>
-<h1>Less cheat sheet</h1>
-
-<h2>normal pager (default)</h2>
-
 <:
-use Shiar_Sheet::Keyboard 2.07;
-my $info = do 'less.eng.inc.pl' or die $@;
-my $keys = Shiar_Sheet::Keyboard->new($info);
-$keys->map($get{map}) or undef $get{map};
-$keys->print_rows($get{rows}, [1,0]);
-$keys->print_legends(\%get);
-
+$Request = 'less';
+Include 'keyboard.plp';
index 269b0d623746ecf243d357533f05a504bd0d7f8b..d1b394f1449f0aef5c925c87bf700063175a53ab 100644 (file)
--- a/lite.css
+++ b/lite.css
@@ -1,4 +1,4 @@
-@import url(light.css?1.10);
+@import url(light.css?1.11);
 
 .pm, td.c-na       {background: #DFD}
 .po                {background: #EFC}
diff --git a/map-numbers.nld.tsv b/map-numbers.nld.tsv
new file mode 100644 (file)
index 0000000..8fce01a
--- /dev/null
@@ -0,0 +1,17 @@
+getalvisualisaties     Koppeling van getalparen 00 t/m 99 aan uiteenlopende begrippen zodat ze makkelijker onthouden kunnen worden.
+       0 zwart 1 rood  2 oranje        3 geel  4 groen 5 cyaan 6 blauw 7 paars 8 wit   9 grijs
+0 (ruimte)     ☄ eros/eris   ♂ mars        ♃ jupiter     ♀ venus       ♁ aarde       ♄ saturnus    ♆ neptunus    ☿ mercurius   ☼ zon ☾ maan
+1 (dier)       🕷 spin       🐈 kat        🦔 egel       🐤 eend       🐛 rups       🐬 dolfijn    🐟 vis        🐙 oktopus    🐑 schaap     🐘 olifant
+2 (gerecht)    🍞 boterham   🍕 pizza      🍝 pasta      🍟 patat      🥗 sla        🍲 stamppot   🥣 soep       🎂 taart      🍚 rijst      🍮 pudding
+3 (fruit)      dadel   🍓 aardbei    🍊 sinaasappel        🍌 banaan     🍋 limoen     🍐 peer/kiwi  🫐 bes/druif  🍒 kers       🥥 kokosnoot  🍈 meloen
+4 (groente)    🌶 peper      🍅 tomaat     🥕 wortel     🌽 mais       🥒 augurk     spinazie        rodekool        🍆 aubergine  🥦 bloemkool  🍄 champignon
+5 (transport)  🛸 duikboot   🎈 ballon     🚲 fiets      🚂 trein      🚶 lopen      🚗 auto       ⛵ boot        🚇 buis/bubbel        ✈ vliegtuig   🏍 motor
+6 (drinken)    🥤 cola       🍹 limo       🫖 thee       🍺 bier       🧃 appelsap   7up     🚰 water      🍷 wijn       🥛 melk       ☕ koffie
+7 (instrument) 𝄢 bas        🎸 gitaar     🎻 viool      🎷 sax        doedelzak       fluit   🥁 drum       harmonica       🎹 piano      clarinet
+8 (bioom)      💀 moeras     🔥 berg       🏙 stad ◇   🏜 woestijn   🌳 regenwoud  🌲 taiga      💧 eiland     ☀ heide       ❄ ijs/toendra savanne
+9 (spel)       diablo  mario   chell   pikachu link    lara croft      sonic   zergling        larry   doomguy
+
+0 (lichaamsdeel)       🎩 haar       👄 mond       👂 oor        👃 neus       👁 oog        💪 arm        🦵 been       🫀 ingewand   🦷 tand       💅 nagel
+9 (weer)       storm   herfst  vakantie        zomer/zon       lente   winter  regen   tornado sneeuw  bewolkt/mist
+
+# vim:ts=17
diff --git a/map.plp b/map.plp
new file mode 100644 (file)
index 0000000..aa74c1c
--- /dev/null
+++ b/map.plp
@@ -0,0 +1,29 @@
+<(common.inc.plp)><:
+my $mapfile = sprintf 'map-%s.nld.tsv', $Request || 'numbers';
+open my $table, '<', $mapfile;
+my ($title, $description) = split /\t/, readline $table;
+
+Html({
+       title => $title,
+       version => '1.0',
+       data => [$mapfile],
+       description => $description,
+       raw => '<style>td,th{text-align:left}</style>',
+});
+
+say "<h1>\u$title</h1>";
+say "<p>$description</p>";
+
+say '<table><thead>';
+while (my $row = readline $table) {
+       my @cols = split /\t/, $row;
+       @cols > 1 or last;
+       say '<tbody>' if $. == 3;
+       print '<tr>';
+       for (@cols) {
+               print /^\d / ? '<th>' : '<td>';
+               s/^.\K /&nbsp;/g; # icon glyph
+               print;
+       }
+}
+say '</table>';
index 5879a4ca6fd4f0154c54609f3db61c9348779926..fbf3b40a7804f6d5f6cceaf59dcf841bf53f9b0c 100644 (file)
--- a/mono.css
+++ b/mono.css
@@ -1,4 +1,4 @@
-@import url(light.css?1.10);
+@import url(light.css?1.11);
 
 a:active, a:visited:active,
 a:hover,  a:visited:hover {color: inherit}
index 5620792af02b449b4b5713bfeab6890df0939d04..013f7dfa3fa90c2e752dc940b3ee17cfb0d21b42 100644 (file)
@@ -1,29 +1,3 @@
-<(common.inc.plp)><:
-
-Html({
-       title => 'mplayer cheat sheet',
-       version => '1.1',
-       description => [
-               "Keyboard cheat sheet for the MPlayer media player,",
-               "overviewing the default controls."
-       ],
-       keywords => [qw'
-               mplayer video media sheet cheat reference overview control shortkey keyboard
-       '],
-       stylesheet => [qw( light dark circus mono red )],
-       keys => 1,
-});
-
-:>
-<h1>MPlayer cheat sheet</h1>
-
-<h2>index (default)</h2>
-
 <:
-use Shiar_Sheet::Keyboard 2;
-my $info = do 'mplayer.eng.inc.pl' or die $@;
-my $keys = Shiar_Sheet::Keyboard->new($info);
-$keys->map($get{map}) or undef $get{map};
-$keys->print_rows($get{rows}, [1,0]);
-$keys->print_legends(\%get);
-
+$Request = $ENV{PATH_INFO} eq '/mpv' ? 'mpv' : 'mplayer';
+Include 'keyboard.plp';
index bac469f9fd7b42c578794a0b4f35db450beea9ca..57f1f13f60214a4f4d06541631e63d877e1a1f41 100644 (file)
--- a/mutt.plp
+++ b/mutt.plp
@@ -1,29 +1,3 @@
-<(common.inc.plp)><:
-
-Html({
-       title => 'mutt cheat sheet',
-       version => '1.2',
-       description => [
-               "Cheat sheet for the Mutt e-mail client,",
-               "showing the default binding for each key.",
-       ],
-       keywords => [qw'
-               mutt MUA email client sheet cheat reference overview commands keyboard
-       '],
-       stylesheet => [qw( light dark circus mono red )],
-       keys => 1,
-});
-
-:>
-<h1>Mutt cheat sheet</h1>
-
-<h2>index (default)</h2>
-
 <:
-use Shiar_Sheet::Keyboard 2;
-my $info = do 'mutt.eng.inc.pl' or die $@;
-my $keys = Shiar_Sheet::Keyboard->new($info);
-$keys->map($get{map}) or undef $get{map};
-$keys->print_rows($get{rows});
-$keys->print_legends(\%get);
-
+$Request = 'mutt';
+Include 'keyboard.plp';
index 8a49a3d78e30e23748ead2c1835be8cfc84b0d2d..81d9ee3f6f16f15261cd4dadb16ce51fed82834f 100644 (file)
@@ -1,31 +1,3 @@
-<(common.inc.plp)><:
-
-Html({
-       title => 'nethack cheat sheet',
-       version => '1.1',
-       description => [
-               "Keyboard overview sheet for the Nethack console RPG game,",
-               "describing the default controls.",
-       ],
-       keywords => [qw'
-               nethack rogue game control controls sheet reference overview keyboard
-       '],
-       stylesheet => [qw( light dark circus mono red )],
-       keys => 1,
-});
-
-:>
-<h1>NetHack cheat sheet</h1>
-
-<h2>normal gameplay</h2>
-
 <:
-use Shiar_Sheet::Keyboard 2;
-my $info = do 'nethack.eng.inc.pl' or die $@;
-my $keys = Shiar_Sheet::Keyboard->new($info);
-$_->{"\e"} = ['me mode'] for values %{ $info->{def} };
-       # static reset button, even though it's not (officially) in the game
-$keys->map($get{map}) or undef $get{map};
-$keys->print_rows($get{rows} || '4321-421', [3,2,1,0]);
-$keys->print_legends(\%get);
-
+$Request = 'nethack';
+Include 'keyboard.plp';
diff --git a/osicon.ttf b/osicon.ttf
new file mode 100644 (file)
index 0000000..25eb79a
Binary files /dev/null and b/osicon.ttf differ
index d81743d99577237edbb526fc0e62b82e93ea2c05..e26cbcd4603722a25fbc935e5469eee3ca367ec4 100644 (file)
@@ -1,9 +1,19 @@
 use utf8;
+use strict;
 
+my $wbr = "\N{ZERO WIDTH SPACE}";
 +{
+       v5.004 => {
+               release => '1997-05-15',
+               distro => {
+                       debian => '2.0', # hamm 1998-07 to potato 2000-08 eol 2003-06
+               },
+               unstable => 0,
+       },
+
        v5.005 => {
                new => [
-                       ['threads' => '', {experimental => 0, stable => 0}],
+                       ['<code>lock</code>', 'obsolete <code>Thread</code> implementation, including a keyword to place advisory locks on shared variables', {experimental => 0, stable => 0, dropped => v5.10}],
                        ['<code>B::…</code>', 'backend hooks'],
                        ['<code>qr//</code>' => 'overhauled regular expression engine: precompile operator, lookahead/behind, code, conditions, localised flags'],
                        ['<code>… foreach</code>' => '<code>for(each)</code> as statement modifier, with large ranges optimised as counting loops'],
@@ -15,19 +25,26 @@ use utf8;
                        ['<code>splice …,…,-$length</code>', 'negative length indicates elements to keep at the end of an array'],
                        ['<code>$/</code>', 'integer or scalar separator makes <code>&lt;&gt;</code> read records instead of lines'],
                ],
+               modules => [
+                       ['Data::Dumper' => 'stringify data structures'],
+                       ['Errno' => 'system <code>errno.h</code> constants'],
+                       ['File::Spec'],
+                       ['Fatal'],
+                       ['Test'],
+               ],
                release => '1998-07-22',
                unstable => 0,
        },
 
        v5.6 => {
                new => [
-                       ['<code>use warnings</code>', 'pragma to enable warnings in lexical scope'],
-                       ['<code>use utf8</code>', 'experimental unicode semantics <small>(completed in v5.8)</small>', {experimental => 0, stable => v5.8}],
+                       ['<code>use warnings</code>', 'pragma to enable warnings in lexical scope', {name => 'warnings'}],
+                       ['<code>use utf8</code>', 'experimental unicode semantics <small>(completed in <a href="#utf8_data">v5.8</a>)</small>', {name => 'utf8', experimental => 0, stable => v5.8}],
                        ['<code>use charnames</code>', 'string escape <code>\N{}</code> to insert named character'],
                        ['<code>our</code>', 'declare global variables'],
                        ['<code>v1.2.3</code>', q"represent strings as vector of ordinals, useful in version numbers (<code>printf '%vd'</code> to display)"],
                        ['<code>0b0</code>', q"binary numbers in literals, <code>printf '%b'</code>, and <code>oct</code>"],
-                       ['<code>sub :lvalue</code>', 'subroutine attribute to return a modifiable value', {experimental => 0, stable => v5.20}],
+                       ['<code>sub :lvalue</code>', 'subroutine attribute to return a modifiable value', {name => 'sub_lvalue', experimental => 0, stable => v5.20}],
 #                      ['<code>sub :locked :method</code>', 'syntax to declare subroutine attributes'], # can be inferred from :lvalue support
                        ['<code>open my $fh, $mode, $expr</code>', 'file handles in scoped scalars, third argument for unambiguous file name'],
                        [q"<code>pack 'q'</code>", '64-bit integer support (also large files &gt;2GiB)', {experimental => 0, stable => v5.8.1}],
@@ -38,33 +55,53 @@ use utf8;
                ],
                release => '2000-03-23',
                distro => {
-                       debian => 'woody',
+                       debian => '3.0', # woody 2002-07 eol 2006-06
                        rhel => '2', # v5.6.0; also in red hat 7.0
                        solaris => '9', # v5.6.1; 2002-05 eol 2014-10
                        aix => '5.1', # 2001-05 eol 2006-04
+                       opensuse => '7.1', # 2001-01
                },
+               versum => 'start of modern compatibility',
                unicode => '3.0.1',
        },
 
        v5.8 => {
                new => [
-                       [q"<code>no utf8</code>", 'full unicode support, <code>utf8</code> pragma only for script encoding'],
-                       [q"<code>binmode $fh, ':perlio'</code>", 'file handle behaviour altered by PerlIO layers'],
+                       [q"<code>no utf8</code>", 'full unicode support, <code>utf8</code> pragma only for script encoding', {name => 'utf8_data'}],
+                       [q"<code>use open</code>", 'file handle behaviour altered by PerlIO layers', {name => 'perlio', eg => 'binmode $fh, ":bytes"'}],
                        [q"<code>open $fh, '-|', @cmd</code>", 'open list to fork a command without spawning a shell'],
                        [q"<code>open $fh, '>', \$var</code>", 'perl scalars as virtual files'],
                        [q"<code>printf '%1$s', @args</code>", 'syntax to use parameters out of order'],
                        [q"<code>1_2_3 == 123</code>", 'underscores between digits allowed in numeric constants'],
-#                      [q"<code>use if</code>", 'conditional module inclusion'], # also installable in earlier versions
+               ],
+               modules => [
+                       [bignum => 'transparent big number support', 'length 1e100 == 101'],
+                       [if => 'conditional module inclusion', 'no if $] >= 5.022, "warnings", "redundant"'],
+                       [sort => 'override sort() algorithm', {dropped => v5.28, eg => 'sort::current eq "stable"'}],
+                       [Digest => 'calculate various message digests (data hashes)', '$hash = sha256_hex($data)'],
+                       [Encode => 'character set conversion', 'encode("utf8", decode("iso-8859-1", $octets))'],
+                       ['File::Temp' => 'create a temporary file or directory safely', '$fh = tempfile();'],
+                       ['List::Util' => 'general-utility list subroutines', '@cards = shuffle 0..51'],
+                       ['Locale::Maketext' => 'various localization and internationalization in <code>Locale::*</code> and <code>L18N::*</code>'],
+                       ['Memoize' => 'remember function results, trading space for time', 'memoize "stat"'],
+                       ['MIME::Base64' => 'base64 encoded strings as in email attachments'],
+                       ['Test::More' => 'modern framework for unit testing', 'is $got, $expected'],
+                       ['Time::HiRes' => 'high resolution timers', '$μs = [gettimeofday]; sleep .1;'.$wbr.' $elapsed = tv_interval $μs'],
                ],
                release => '2002-07-18',
                distro => {
-                       debian => 'sarge',
+                       debian => '3.1', # sarge 2005-06 eol 2008-03, v5.8.8 in etch 2007-04 eol 2010-02
                        rhel => '3', # v5.8.0; v5.8.8 in RHEL6 (2007-2014)
                        solaris => '10', # v5.8.4; 2005-01 eol 2021-01
                        centos => '3-5', # v5.8.0 in v3 (2004-03); v5.8.8 in v5 (eol 2017-03)
-                       ubuntu => '4.10',
+                       ubuntu => '4.10', # v5.8.4 (2004-10); v5.8.7 in 6.06 LTS (2006-06); v5.8.8 in 8.04 LTS (2008-04)
                        aix => '5.2', # v5.8.0; v5.8.2 in 5.3 and 6.1 (eol 2017-04-30)
+                       freebsd => '4-6',
+                       opensuse => '8.1', # 2002-09 eol (SLES8 2002-10 eol 2007-12 ltss 2009-12)
                },
+               distrosum => "RHEL 3, SLES 8, AIX 5/6 until 2017, Solaris 10 until 2021",
+               versum => 'stable minimum during 20[01]\d',
+               support => '2021-01', # solaris
                unicode => '3.2.0',
        },
 
@@ -73,23 +110,36 @@ use utf8;
                        ['<code>//</code>', 'defined-or operator'],
                        ['<code>~~</code>', 'smart-match operator to compare different data types <small>(updated in v5.10.1)</small>', {experimental => 'smartmatch'}],
                        ['<code>say</code>', 'print with newline, equivalent to <code>print @_, "\n"</code>', {feature => 'say'}],
-                       ['<code>given</code>', 'switch statement to smart-match with <code>when</code>/<code>default</code>', {feature => 'switch', experimental => 'smartmatch'}],
+                       ['<code>given</code>', 'switch statement to smart-match with <code>when</code>/<code>default</code>', {name => 'switch', feature => 'switch', experimental => 'smartmatch'}],
                        ['<code>/(?&lt;name>)/</code>', 'named capture buffers into <code>%+</code>'],
                        ['<code>/(?1)/</code>', 'recursive regular expression patterns'],
+                       ['<code>/(?|)/</code>', 'resets capture numbering for each contained branch'],
                        ['<code>/.++/</code>', 'possessive quantifiers <code>?+</code>, <code>*+</code>, <code>++</code> to match greedily'],
                        ['<code>s/keep\K//</code>', 'floating positive lookbehind, efficient alternative for <code>s/(keep)/$1/</code>'],
-                       ['<code>/\v/, /\h/</code>', 'vertical and horizontal whitespace escapes'],
+                       ['<code>/p</code>', 'optionally preserve <code>${^MATCH}</code> variables (avoiding <code>$&</code> penalty until COW in v5.20)'],
+                       ['<code>/\v/, /\h/</code>', 'vertical and horizontal whitespace escapes (<code>\V</code> <code>\H</code> to invert); also <code>/\R/</code> for newlines'],
                        ['<code>my $_</code>', 'lexically scoped version of the default variable', {experimental => 'lexical_topic', dropped => v5.23.4}],
-                       ['<code>state</code>', 'persistent <code>my</code> variables', {feature => 'state'}],
+                       ['<code>state</code>', 'persistent <code>my</code> variables (scalars only until <a href="#state_ext">5.28</a>)', {feature => 'state'}],
+               ],
+               modules => [
+                       [autodie => 'replace builtin functions to throw exceptions instead of returning failure', 'eval {open ...} or $@->matches("open") || die'],
+                       # overloading
+                       ['IO::Compress::Zip' => 'various file compression standards', 'zip IO::Uncompress::Gunzip->new("test.gz")'.$wbr.' => "recompressed.zip"'],
+                       ['Time::Piece' => 'timestamps as objects', 'localtime->year > 1900'],
+                       ['File::Fetch' => 'generic data retrieval/download', 'File::Fetch->new(uri => "http://localhost/")'.$wbr.'->fetch(to => \$slurp)'],
                ],
                release => '2007-12-18',
                distro => {
-                       debian => 'lenny',
-                       rhel => '6', # v5.10.1
-                       centos => '6', # v5.10.1 (2011-07 eol 2020-11)
-                       ubuntu => '8.10', # v5.10.1 in 10.04 LTS
-                       aix => '7.1', # v5.10.1 (2010-09 eol 2020?)
+                       debian => '5.0', # lenny 2009-02 eol 2012-02
+                       rhel => '6', # v5.10.1 (-6.9 2017-03 eol 2020-11 TuxCare els 2024-12)
+                       centos => '6', # v5.10.1 (2011-07 eol 2020-11 TuxCare els 2024-11)
+                       ubuntu => '8.10', # v5.10.1 in 10.04 LTS 2010-04 eol 2013-05
+                       aix => '7.1', # v5.10.1 (2010-09 eol 2023-04)
+                       opensuse => '11.0', # 2008-06 (SLES11 2009-03 eol 2019-03 ltss 2022-03)
                },
+               distrosum => "SLES 11 until 2022, RHEL 6 until 2020 or commercially 2024, AIX 7.1 until 2023",
+               versum => "supported commercially until 2024",
+               support => '2024-11', # aix
                unicode => '5.0.0',
        },
 
@@ -101,29 +151,42 @@ use utf8;
                        ['<code>… when</code>', '<code>when</code> is now allowed to be used as a statement modifier'],
                        [q"<code>use overload 'qr'</code>", 'customisable conversion to regular expressions'],
                        ['<code>/\N/</code>', 'inverse \n to match any character except newline regardless of <code>/s</code>', {experimental => 0, stable => v5.18}],
+                       ['<code>each $ref</code> e.a.', 'array and hash container functions accept references', {experimental => 'autoderef', dropped => v5.23.1}],
                ],
                release => '2010-04-12',
                unicode => '5.2',
                distro => {
-                       solaris => '11', # also v5.8.4; 2010-11 eol 2024-11
+                       solaris => '11', # also v5.8.4; 2010-11; v11.3 eol 2024-01
                        ubuntu => '11.10',
+                       freebsd => '7',
+                       opensuse => '11.3', # 2010-07
                },
+               support => '2024-01', # solaris
        },
 
        v5.14 => {
                new => [
                        ['<code>s///r</code>', 'non-destructive substitution'],
-                       ['<code>/(?^)/</code>', 'construct to reset to default modifiers'],
                        ['<code>/(?{ m() })/</code>', 'regular expressions can be nested in <code>/(?{})/</code> and <code>/(??{})/</code>', {experimental => 0, stable => v5.20}],
+                       ['<code>/dalu</code>', 'regexp modifiers to restrict character classes: either <strong>d</strong>efault, <strong>a</strong>scii, <strong>l</strong>ocale, or <strong>u</strong>nicode semantics.'],
                        [q"<code>use re '/flags'</code>", 'customise default modifiers'],
-                       ['<code>each $ref</code> e.a.', 'array and hash container functions accept references', {experimental => 'postderef', dropped => v5.23.1}],
+                       ['<code>/(?^)/</code>', 'construct to reset to default modifiers'],
                        ['<code>FH->method</code>', 'filehandle method calls load IO::File on demand (eg. <code>STDOUT->flush</code>)'],
+                       ['<code>\o{}</code>', 'escape sequence for octal values beyond \777'],
+               ],
+               modules => [
+                       [JSON => 'interface with data in JavaScript Object Notation', 'decode_json <>'],
+                       ['HTTP::Tiny' => 'minimal HTTP/1.1 client without <code>LWP::UserAgent</code> overhead', {dropped => 0}],
+                       # Unicode::Collate::Locale
                ],
                release => '2011-05-14',
                distro => {
-                       debian => 'wheezy',
+                       debian => '7', # wheezy 2013-05 eol 2018-05 elts 2020-06
                        ubuntu => '12.04',
+                       opensuse => '12.1', # 2011-11 (SLES12 2014-10 eol 2024-10 ltss 2027-10)
                },
+               distrosum => "Debian 7 until 2020, Ubuntu 12.04, SLES 12 until 2027",
+               support => '2027-10', # suse
                unicode => '6.0+#8',
        },
 
@@ -135,8 +198,10 @@ use utf8;
                ],
                release => '2012-05-20',
                distro => {
-                       rhel => '7', # v5.16.3
-                       centos => '7', # v5.16.3 (2014-07 eol 2024-06)
+                       rhel => '7', # v5.16.3 (-7.9 2020-09 eol 2024-06)
+                       centos => '7', # v5.16.3 (2014-07 eol 2024-06 TuxCare els 2028-06)
+                       freebsd => '9',
+                       opensuse => '12.2', # 2012-09
                },
                unicode => '6.1',
        },
@@ -144,21 +209,23 @@ use utf8;
        v5.18 => {
                new => [
                        ['<code>${^LAST_FH}</code>', 'last read filehandle (used by <code>$.</code>)'],
-                       ['<code>/(?[ a + b ])/</code>', 'regex set operations (character substraction <code>-</code>, unions <code>&amp;</code>)', {experimental => 'regex_sets'}],
+                       ['<code>/(?[ a + b ])/</code>', 'regex set operations (character subtraction <code>-</code>, union <code>+</code>, intersection <code>&amp;</code>, xor <code>^</code>)', {experimental => 'regex_sets', stable => v5.36}],
                        ['<code>my sub</code>', 'lexical subroutines (also <code>state</code>, <code>our</code>); buggy before v5.22', {experimental => 'lexical_subs', stable => v5.26}],
                        ['<code>next $expression</code>', 'loop controls allow runtime expressions'],
                        [q"<code>no warnings 'experimental::…'</code>", 'mechanism for experimental features, as of now required for <em>smartmatch</em>'],
                ],
                release => '2013-05-18',
                distro => {
-                       ubuntu => '14.04',
+                       ubuntu => '14.04 LTS', # trusty
+                       opensuse => '13.1', # 2013-11 eol 2016-01
+                       macos => '10.15', # pre-installed catalina 2019-10
                },
                unicode => '6.2',
        },
 
        v5.20 => {
                new => [
-                       ['<code>sub ($var)</code>', 'subroutine signatures', {feature => 'signatures', experimental => 'signatures'}],
+                       ['<code>sub ($var)</code>', 'subroutine signatures', {feature => 'signatures', experimental => 'signatures', stable => v5.36}],
                        ['<code>%hash{…}</code>', 'hash slices return key+value pairs'],
                        ['<code>[]->@*</code>', 'postfix dereferencing (also e.g. <code>$scalar->$*</code> for <code>$$scalar</code>)', {feature => 'postderef, postderef_qq', experimental => 'postderef', stable => v5.23.1}],
                        [q"<code>use warnings 'once'; $a</code>", 'variables $a and $b are exempt from <em>used once</em> warnings'],
@@ -166,10 +233,13 @@ use utf8;
                unicode => '6.3',
                release => '2014-05-27',
                distro => {
-                       debian => 'jessie',
-                       ubuntu => '14.10',
-                       aix => '7.2',
+                       debian => '8', # jessie 2015-04 eol 2018-06 lts 2020-06 elts 2025-06
+                       ubuntu => '14.10', # utopic
+                       aix => '7.2', # 2015-12 eol 2028?
+                       opensuse => '13.2', # 2014-11 eol 2017-01
                },
+               distrosum => "Debian 8 until 2025, Ubuntu 14.10, openSUSE 13.2, AIX 7.2",
+               versum => "extended vendor support 202X",
        },
 
        v5.22 => {
@@ -178,14 +248,15 @@ use utf8;
                        ['<code>&lt;&lt;>></code>', 'safe <code>readline</code> ignoring open flags in arguments'],
                        ['<code>/()/n</code>', 'flag to disable numbered capturing, turning <code>()</code> into <code>(?:)</code>'],
                        ['<code>/\b{}/</code>', 'boundary types: <em>gcb</em> (grapheme cluster), <em>sb</em> (sentence), <em>wb</em> (word)'],
-                       ['<code>&.</code>', '<code>& | ^ ~</code> consistently numeric, dotted operators for strings', {experimental => 'bitwise'}],
+                       ['<code>&.</code>', '<code>& | ^ ~</code> consistently numeric, dotted operators for strings', {feature => 'bitwise', experimental => 'bitwise', stable => v5.28}],
                        [q"<code>use re 'strict'</code>", 'apply stricter syntax rules to regular expression patterns', {experimental => 're_strict'}],
                        ['<code>0x.beep+0</code>', q"hexadecimal floating point notation with binary power; <code>printf '%a'</code> to display"],
+                       ['<code><s>??</s></code>', 'single match shorthand (deprecated since v5.14) requires the operator <code><em>m</em>?PATTERN?</code>'],
                ],
                unicode => '7.0',
                release => '2015-06-01',
                distro => {
-                       ubuntu => '16.04',
+                       ubuntu => '16.04 LTS', # xenial 2016-04 eol 2021-04 TuxCare els 2025-04
                },
        },
 
@@ -197,6 +268,12 @@ use utf8;
                ],
                unicode => '8.0',
                release => '2016-05-09',
+               distro => {
+                       debian => '9', # stretch 2017-06 eol 2020-07 lts 2022-06 elts 2027-06
+                       ubuntu => '17.04', # zesty 2017-04 eol 2018-01
+                       freebsd => '10',
+               },
+               support => '2027-06',
        },
 
        v5.26 => {
@@ -205,7 +282,127 @@ use utf8;
                        ['<code>@{^CAPTURE}</code>', q"array of last match's captures, so <code>${^CAPTURE}[0]</code> is <code>$1</code>"],
                        ['<code>//xx</code>', 'extended modifier to also ignore whitespace in bracketed character classes'],
                ],
+               modules => [
+                       ['Test2::V0' => 'generic testing framework to replace <code>Test::*</code> and <code>TAP::*</code>'],
+               ],
                unicode => '9.0', # also Script_Extensions/scx in "\p{script}"
                release => '2017-05-30',
+               distro => {
+                       ubuntu => '17.10', # artful 2017-10; 18.04 LTS 2018-04 eol 2023-04
+                       opensuse => '15.0', # 2018-05 eol 2019-11; same in 15.4 2022-06
+                       centos => '8', # 2019-09 eol 2021-12 TuxCare els 2026-01
+               },
+               distrosum => "stable servers such as Ubuntu 17.10+ (Debian &gt;9), CentOS 8, openSUSE 15.0",
+               support => '2023-04',
+       },
+
+       v5.28 => {
+               new => [
+                       ['<code>delete %hash{…}</code>', 'hash slices can be deleted with key+value pairs'],
+                       ['<code>/(*…)/</code>', 'alphabetic synonyms for assertions, e.g. <code>(*atomic:…)</code> for <code>(?&gt;…)</code> and <code>(*nlb:…)</code> for <code>(?&lt;!…)</code>', {experimental => 'alpha_assertions', stable => v5.31.6}],
+                       ['<code>/(*script_run:)/</code>', 'enforces all characters to be from the same script', {experimental => 'script_run', stable => v5.31.6}],
+                       ['<code>state @a</code>', 'persistent lexical array or hash variables (in addition to <a href="#state">scalars</a>)', {name => 'state_ext'}],
+                       ['perl<code> -i -pe die</code>', 'safe in-place editing: files are replaced only after successful completion'],
+                       ['<code>${^SAFE_LOCALES}</code>', 'locales are thread-safe on supported systems, indicated by this variable'],
+               ],
+               unicode => '10.0',
+               release => '2018-06-22',
+               distro => {
+                       debian => '10', # buster 2019-07
+                       ubuntu => '19.04', # disco 2019-04 eol 2020-01
+                       freebsd => '11', # eol 2021-09
+                       macos => '11', # big sur 2020-11
+               },
+               distrosum => "stable systems such as Debian 10, Ubuntu 19.04, FreeBSD 11",
+       },
+
+       v5.30 => {
+               new => [
+                       ['<code>/(?<=var+)</code>', 'variable length lookbehind assertions', {experimental => 'vlb', stable => v5.36}],
+                       ['<code>m(\p{nv=/.*/})</code>', 'match unicode properties by regular expressions', {experimental => 'uniprop_wildcards'}],
+                       ['<code><s>my $state if 0</s></code>', 'workaround for <code><a href="#state">state</a></code> (deprecated since v5.10!) is now prohibited'],
+                       [q"<code>qr'\N'</code>", 'Delimiters must be graphemes; unescaped <code>{</code> illegal; <code>\N</code> in single quotes'],
+               ],
+               unicode => '12.1',
+               release => '2019-05-22',
+               distro => {
+                       ubuntu => '20.04', # focal LTS 2020-04 eol 2025-04
+                       macos => '11.3', # big sur 2021-04 and monterrey 2021-10, alongside compatibility v5.18
+               },
+       },
+
+       v5.32 => {
+               new => [
+                       ['<code>isa</code>', 'infix operator to check class instance', {feature => 'isa', experimental => 'isa', stable => v5.36}],
+                       ['<code>$min &lt; $_ &lt;= $max</code>', 'chained comparison repeats inner part as <code>$min &lt; $_ and $_ &lt;= $max</code>'],
+                       ['<code>/\p{Name=$var}/</code>', 'match Unicode Name property like <code>\N{}</code> but with interpolation and subpatterns'],
+                       [q"<code>open F, '+&gt;&gt;', undef</code>", 'respect append mode on temporary files with mixed access'],
+                       ["<code>no feature 'indirect'</code>", 'disable indirect object notation such as <code>new Class</code> instead of <code>Class-&gt;new</code>'],
+                       ['streamzip', 'program distributed with core IO::Compress::Base to compress stdin into a zip container'],
+               ],
+               unicode => '13.0',
+               release => '2020-06-20',
+               details => 'https://www.effectiveperlprogramming.com/2020/01/perl-v5-32-new-features/',
+               distro => {
+                       debian => '11', # bullseye 2021-08
+                       ubuntu => '21.04', # hirsute 2021-04 eol 2022-01
+                       rhel => '8', # -8.7 and -9.1 2022-11
+                       solaris => '11.4', # 2018-08 eol 2034-11 (SRU 38 removes 5.22, 5.26)
+               },
+               distrosum => "stable systems such as Debian 11, Ubuntu 21.04, RHEL 8, Solaris 11.4, AIX 7.3",
+               support => '2034-11', # solaris
+       },
+
+       v5.34 => {
+               new => [
+                       ['<code>try {} catch</code>', 'exception handling similar to eval blocks', {feature => 'try', experimental => 'try'}],
+                       ['<code>/{,<i>n</i>}/</code>', 'empty lower bound quantifier is accepted as shorthand for 0'],
+                       ['<code>\x{ … }</code>', 'insignificant space within curly braces, also for <code>\b{}</code>, <code>\g{}</code>, <code>\k{}</code>, <code>\N{}</code>, <code>\o{}</code> as well as <code>/{m,n}/</code> quantifiers'],
+                       ['<code>0o0</code>', 'octal prefix <code>0o</code> alternative to <code>0…</code> and <code>oct</code>'],
+                       ['<code>re::optimization(qr//)</code>', 'debug regular expression optimization information discovered at compile time'],
+                       ['<code>no feature …</code>', 'disable discouraged practices of <code>bareword_filehandles</code> and <code>multidimensional</code> array emulation'],
+               ],
+               release => '2021-05-20',
+               distro => {
+                       ubuntu => '22.04', # jammy LTS 2022-04 eol 2027-04
+                       aix => '7.3', # 2021-12 eol 2035?
+               },
+       },
+
+       v5.36 => {
+               new => [
+                       ['<code>use v5.36</code>', "use <code>warnings</code>; use feature qw'<code>signatures isa</code>'; no feature qw'<code>indirect multidimensional switch</code>'"],
+                       ['<code>use builtin</code>', 'namespace for interpreter functions, such as <code>weaken</code> and <code>blessed</code> from <code>Scalar::Util</code>, <code>ceil</code>/<code>floor</code> from <code>POSIX</code>, and <code>trim</code> like <code>String::Util</code>', {experimental => 'builtin'}],
+                       ['<code>is_bool(!0)</code>', 'distinguish scalar variable types (by <code>builtin</code> functions) for data interoperability', {name => 'is_bool'}],
+                       ['<code>for my ($k, $v) (%hash)</code>', 'iterate over multiple values at a time (including <code>builtin::indexed</code> for arrays)', {experimental => 'for_list', feature => 'for_list'}],
+                       ['<code>defer {}</code>', 'queue code to be executed when going out of scope', {feature => 'defer', experimental => 'defer'}],
+                       ['<code>try {} finally {}</code>', 'run code at the end of a <code><a href="#try">try</a></code> construct regardless of failure', {name => 'finally', feature => 'try', experimental => 'try'}],
+                       ['<code>q«…»</code>', 'unicode delimiters for quoting operators', {experimental => 'extra_paired_delimiters'}],
+                       ['<code>sub ($var) {!<s>pop</s>}</code>', '<a href="#signatures">signature</a>d subs are stable, but mixing with the arguments array <code>@_</code> remains experimental', {feature => 'signatures', experimental => 'args_array_with_signatures'}],
+                       ['<code>$SIG{FPE}</code>', 'floating-point exceptions no longer deferred but delivered immediately like other signals', {name => 'sig_fpe'}],
+                       ['perl<code> -g</code>', 'disable input record separator (slurp mode), alias for <code>-0777</code>'],
+               ],
+               unicode => '14.0',
+               release => '2022-05-28',
+               distro => {
+                       debian => '12', # bookworm 2023-06
+                       ubuntu => '23.04', # lunar upcoming
+               },
+       },
+
+       v5.38 => {
+               new => [
+                       ['<code>use feature "module_true"</code>', 'default in use 5.37 and up, also <code>no feature "bareword_filehandles"</code>'],
+                       ['<code>sub ($var ||=</code> default<code>)</code>', 'assign values when false (or undefined on <code>//=</code>) instead of omitted'],
+                       ['<code>/(*{ … })/</code>', 'optimistic eval: <code>(?{ … })</code> with regex optimisations enabled'],
+                       ['<code>class</code>', "define object classes: packages with <code>field</code> variables and <code>method</code> subroutines", {feature => 'class', experimental => 'class'}],
+                       ['<code>${^LAST_<wbr>SUCCESSFUL_<wbr>PATTERN}</code>', 'explicit variable to access the previous match as in <code>s//…/</code>'],
+                       ['<code>%{^HOOK}</code>', "perform tasks <code>require__before</code> and <code>require__after</code> when calling <code>require</code>"], # also @INC hook enhancements
+                       ['<code>PERL_RAND_<wbr>SEED</code>', 'environment variable to set <code>srand</code> random number seed', {dropped => 0}],
+                       ['<code>is_tainted</code>', '<code>builtin</code> function to check variable tainting like <code>Scalar::Util::tainted</code>', {dropped => 0, experimental => 'builtin', eg => 'builtin::is_tainted($ENV{PWD}) and say "running perl -T"'}],
+                       ['<code>export_lexically</code>', '<code>builtin</code> function to export named functions into the calling scope', {dropped => 0, experimental => 'builtin', eg => 'method import {builtin::export_lexically {map {$_ => $self->can($_)} @_}}'}],
+               ],
+               unicode => '15.0',
+               release => '2023-07-02',
        },
 }
index a5ef991145750b9fbcb617837a58b5e481059a3a..596e4d84d80afeafe4dece3facdbe662e04d326c 100644 (file)
--- a/perl.plp
+++ b/perl.plp
@@ -2,7 +2,7 @@
 
 Html({
        title => 'perl version cheat sheet',
-       version => '1.2',
+       version => '1.6',
        keywords => [qw'
                perl version feature features comparison
                sheet cheat overview summary
@@ -11,51 +11,132 @@ Html({
        data => ['perl.inc.pl'],
 });
 
+use experimental 'signatures';
 :>
 <h1>Perl release summary</h1>
 
-<p>The most significant features introduced for recent versions of the Perl scripting language.
-Depending on desired compatibility you'll want to support a minimum of
-<span title="on dinosaur platforms such as Solaris 10, AIX 5.2, RHEL 3, SLES 8">v5.8</span> or
-<span title="on stable servers such as Debian wheezy, Ubuntu 12.04, CentOS 7">v5.14</span>.
-</p>
-
+<p>The most significant features introduced for recent versions of the Perl
+scripting language.
 <:
-my $info = do 'perl.inc.pl' or die $@ // $!;
+my $info = Data('perl');
+
+use feature 'signatures';
+sub vname ($v) {
+       return sprintf 'v%d%03d', unpack 'C*', $v;
+}
+sub linkversion ($v) {
+       return showlink(sprintf('%vd', $v), '#'.vname($v));
+}
+
+eval {
+       use List::Util 'first';
+       use Time::Piece;
+       use Time::Seconds;
+
+       my $now = Time::Piece->new;
+       if (my $ts = $get{at}) {
+               $now = $now->strptime($ts, '%Y-%m-%d');
+               say "Compatibility details emulated for <em>$ts</em>.";
+       }
+       my $ts = $now->strftime('%F');
+       my @versions = sort grep { $info->{$_}{release} le $ts } keys %{$info};
+
+       # perlpolicy: «We "officially" support the two most recent stable release
+       # series. [...] we will attempt to fix critical issues»
+       $info->{ $versions[-2] }{versum} //= "active core support";
+       $info->{ $versions[-1] }{versum} //= "latest stable release";
+
+       # perlpolicy: «we will attempt to fix critical issues in the two most
+       # recent stable 5.x release series»
+       my $coreeol = ($now - ONE_YEAR * 3)->strftime('%F');
+       my $vcore = first { $info->{$_}{release} ge $coreeol } @versions;
+       print "<p>Core security support is provided for 3 years";
+       print ", so typical users should run at least ", linkversion($_)
+               for $vcore // ();
+       say '.';
+       $info->{$vcore}{versum} //= "official security patches";
+
+       # «We encourage vendors to ship the most recent supported release of Perl
+       # at the time of their code freeze»
+       # assume debian ships after 1 year, and expires after 5 years LTS
+       my $vendoreol = ($now - ONE_YEAR * 6)->strftime('%F');
+       my $vdebian = first {
+               $info->{$_}{release} ge $vendoreol && $info->{$_}{distro}{debian}
+       } @versions;
+       say sprintf "Stable distributions such as Debian %s maintain %s+.",
+               $info->{$_}{distro}{debian}, linkversion($_) for $vdebian // ();
+       $info->{$vdebian}{versum} //= "still maintained by common vendors";
+
+       # extended support given at random
+       my $nowcmp = $now->strftime('%F');
+       my $vdino = first { $info->{$_}{support} ge $nowcmp } @versions;
+       say "Enterprise platforms retain versions up to $_."
+               for map { linkversion($_) } $vdino // ();
+       return 1;
+} or Alert('Missing version recommendations', $@);
+say '</p>';
+
 for my $vernum (reverse sort keys %{$info}) {
        my $verrow = $info->{$vernum};
        defined $verrow->{unstable} and next unless exists $get{v};
 
-       say '<div class="section">';
-       say sprintf '<h2>%vd <small>%s</small></h2><dl>', $vernum, $verrow->{release};
+       say sprintf '<div class="section" id="%s">', vname($vernum);
+       my $title = $verrow->{release} // '?';
+       $title .= ": $_" for $verrow->{versum} // ();
+       say sprintf '<h2>%vd <small>%s</small></h2>', $vernum, $title;
+       say '<dl>';
        for (@{ $verrow->{new} }) {
                my ($topic, $desc, $attr) = @{$_};
-               if ($attr) {
-                       my $title;
-                       if (defined $attr->{experimental}) {
-                               $title = 'experimental';
-                       }
-                       if ($attr->{dropped}) {
-                               next unless exists $get{v};
-                               $title = sprintf 'removed in %vd', $attr->{dropped};
-                       }
-                       elsif ($attr->{stable}) {
-                               $title .= sprintf ' until %vd', $attr->{stable};
-                       }
-                       if ($attr->{experimental}) {
-                               $title = sprintf '<span title="experimental::%s">%s</span>',
-                                       $attr->{experimental}, $title;
-                       }
-                       if ($attr->{feature}) {
-                               $title = sprintf('<span title="%s">feature</span>', $attr->{feature})
-                                       . (defined $title && ", $title");
-                       }
-                       $desc .= sprintf ' <em class="ex">(%s)</em>', $title;
+               $desc .= featattrs($attr);
+               my $ref = defined $attr->{name} && sprintf ' id="%s"', $attr->{name};
+               say sprintf '<dt%s>%s<dd>%s', $ref, $topic, $desc || '<br/>';
+       }
+       if (my $mods = $verrow->{modules}) {
+               for (@{$mods}) {
+                       my ($name, $desc, $attr) = @{$_};
+                       my $ref = lc $name =~ s/::/_/gr;
+                       $desc .= featattrs($attr);
+                       printf '<dt id="%s"><code>use %s</code>', $ref, $name;
+                       say '<dd>', $desc;
                }
-               say sprintf '<dt>%s<dd>%s', $topic, $desc || '<br/>';
        }
        say sprintf '<dt>Unicode</dt><dd>v%s', $_ for $verrow->{unicode} || ();
        say '</dl>';
        say "</div>\n";
 }
 
+sub featattrs ($attr) {
+       $attr or return '';
+       ref $attr or $attr = {eg => $attr};
+       my $title;
+       if (defined $attr->{experimental}) {
+               $title = 'experimental';
+       }
+       if (defined $attr->{dropped}) {
+               return '' unless exists $get{v};
+               $title = sprintf 'removed in %vd', $_ for $attr->{dropped} || ();
+       }
+       elsif ($attr->{stable}) {
+               $title .= sprintf ' until %vd', $attr->{stable};
+       }
+       if ($attr->{experimental}) {
+               $title = sprintf '<span title="experimental::%s">%s</span>',
+                       $attr->{experimental}, $title;
+               $attr->{name} //= $attr->{experimental};
+       }
+       if ($attr->{feature}) {
+               my $prefix = sprintf '<span title="%s">feature</span>',
+                       $attr->{feature};
+               $title = join ', ', $prefix, $title // ();
+               $attr->{name} //= $attr->{feature};
+       }
+       $title = $title ? sprintf ' <em class="ex">(%s)</em>', $title : '';
+
+       if (my $eg = $attr->{eg}) {
+               my $pre = Entity($eg);
+               $pre =~ s<\N{ZERO WIDTH SPACE}>{</code><wbr/><code>}g;
+               $pre = " <small>{<code>$pre</code>}</small>";
+               $title = $pre . $title;
+       }
+       return $title;
+}
index 1c5350a7317923b563e3c4c997473f0f701067b1..62dd7fc6cc38ce80a9122c7d5738000526ae7379 100644 (file)
@@ -1,29 +1,3 @@
-<(common.inc.plp)><:
-
-Html({
-       title => 'readline cheat sheet',
-       version => '1.1',
-       description => [
-               "Reference sheet of default key bindings for GNU readline,",
-               "used for line-editing in most Unix software, notably Emacs and Bash.",
-       ],
-       keywords => [qw'
-               readline gnu bash emacs sheet cheat reference overview keyboard editing curses
-       '],
-       stylesheet => [qw( light dark circus mono red )],
-       keys => 1,
-});
-
-:>
-<h1>readline cheat sheet</h1>
-
-<h2>default emacs mode</h2>
-
 <:
-use Shiar_Sheet::Keyboard 2;
-my $info = do 'readline.eng.inc.pl' or die $@;
-my $keys = Shiar_Sheet::Keyboard->new($info);
-$keys->map($get{map}) or undef $get{map};
-$keys->print_rows($get{rows} || '^x=213', [4,3,2]);
-$keys->print_legends(\%get);
-
+$Request = 'readline';
+Include 'keyboard.plp';
diff --git a/red.css b/red.css
index f5763a1a065db0caa69b70d7cee74a12d84f7385..bb2ee71c27ce3db7caacfb6ca61d321d32177153 100644 (file)
--- a/red.css
+++ b/red.css
@@ -1,4 +1,4 @@
-@import url(light.css?1.10);
+@import url(light.css?1.11);
 
 body {
        background: #000;
diff --git a/robots.txt b/robots.txt
deleted file mode 100644 (file)
index 15b6ecb..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-User-agent: *
-Disallow: /source/*::*
-
-Sitemap: http://sheet.shiar.nl/sitemap.xml
-Host: sheet.shiar.nl
diff --git a/robots.txt.plp b/robots.txt.plp
new file mode 100644 (file)
index 0000000..9b91707
--- /dev/null
@@ -0,0 +1,10 @@
+<(common.inc.plp)><:
+$header{content_type} = 'text/plain; charset=us-ascii';
+checkmodified($ENV{SCRIPT_FILENAME});
+
+say 'User-agent: *';
+say 'Disallow: ', $Dev ? '/' : '/source/*::*';
+#say 'Disallow: /font/*?q=*';
+:>
+Sitemap: https://sheet.shiar.nl/sitemap.xml
+Host: sheet.shiar.nl
index 33d06e909d878f45197a09bd8b139e6d5c5e64f3..36ad2b424e995697c25e7a6602e94b8c18cb0158 100644 (file)
@@ -1,28 +1,40 @@
 <(common.inc.plp)><:
 
-my $textinc = 'data/unicode-sampler/unicode.txt';
+my $textinc = 'sample.txt';
 
 Html({
        title => "unicode sampler",
-       version => '2.0',
+       version => '2.1',
        stylesheet => [qw'light dark mono red'],
        data => [$textinc],
+       image => 'sample.png',
 });
 
 open my $source, '<', $textinc
-       or die "Could not open text at $textinc: $!\n";
+       or Abort("Could not open text at $textinc", 501, $!);
 local $/ = "\n\n";
 
 my $top = readline $source;
-my ($title, $hr, $intro) = split /\n(\H)$1+\n/, $top, 2;
+my ($title, $hr, $intro) = split /\n(\pP)\1+\n/, $top, 2;
 say "<h1>$title</h1>";
+say <<".";
+<p>HTML display of <a href="/$textinc">plain text</a>
+intended for monospaced (terminal) output.
+Compare an expected rendering of the <a href="/sample.png">overview</a>.
+</p>
+.
 
 say '<pre>';
 print $intro;
 
 while (my $p = readline $source) {
        EscapeHTML($p);
-       $p =~ s{ \A (\N+:) \n\Z }{<h2>$1</h2>}x;
+       $p =~ s{ \A ((\pL+) \N*:) \n }{<h2 id="\L$2\E">$1</h2>}x;
+       if ($2 eq 'Unicode') {
+               # table without proper direction control
+               $p =~ s/^(?= )/\x{202d}/gm; # ltr override every line
+       }
+       $p =~ s{(?<=^  )([\p{Latin} ]+:)}{<em>$1</em>}gm;
        print $p;
 }
 
diff --git a/sample.png b/sample.png
new file mode 100644 (file)
index 0000000..8895b26
Binary files /dev/null and b/sample.png differ
diff --git a/sample.txt b/sample.txt
new file mode 120000 (symlink)
index 0000000..9ce7fc7
--- /dev/null
@@ -0,0 +1 @@
+data/unicode-sampler/unicode.txt
\ No newline at end of file
index f4c1f7c9c9e1b0a1e11a73c5988cbe4e5cc682a8..8d490d4b24e59fbf6a7fa68dfb5ecba07f001717 100644 (file)
@@ -43,6 +43,13 @@ attack => [
 sight => 8,
 speed => 4.92 * $SM,
 counter => ['vulture', 'dark templar'],
+special => [
+       {
+               name => 'worker',
+               abbr => '⚒',
+               desc => "warp-in buildings and gather minerals (65/minute) or gas (103/minute)",
+       },
+],
 },
 
 {
@@ -842,6 +849,13 @@ attack => [
 ],
 sight => 7,
 speed => 4.92 * $SM,
+special => [
+       {
+               name => 'worker',
+               abbr => '⚒',
+               desc => "construct or repair buildings and gather minerals (68/minute) or gas (103/minute)",
+       },
+],
 },
 
 {
@@ -1626,6 +1640,13 @@ attack => [
 ],
 sight => 7,
 speed => 4.92 * $SM,
+special => [
+       {
+               name => 'worker',
+               abbr => '⚒',
+               desc => "morph into buildings and gather minerals (67/minute) or gas (103/minute)",
+       },
+],
 },
 
 {
@@ -1881,7 +1902,7 @@ cat => 'lair',
 name => 'Lurker',
 min => 125,
 gas => 125,
-base => 'hydralisk',
+base => ['hydralisk'],
 build => 40,
 suit => 2,
 size => 32 / $PPT,
@@ -2104,7 +2125,7 @@ name => 'Guardian',
 min => 150,
 gas => 200,
 build => 40,
-base => 'mutalisk',
+base => ['mutalisk'],
 suit => 3,
 size => 44 / $PPT,
 pop => 2,
@@ -2136,7 +2157,7 @@ name => 'Devourer',
 min => 250,
 gas => 150,
 build => 40,
-base => 'mutalisk',
+base => ['mutalisk'],
 suit => 3,
 size => 44 / $PPT,
 pop => 2,
index 524ed128fc03496ee32506d306903c8664e75913..9b49af2d5460315abd9f883b9123dc19f1ba3ec2 100644 (file)
@@ -1,8 +1,11 @@
 use utf8;
 use strict;
 
+my $V = v5.0.2; # some patch data for invisible attributes
+my $GATHER = "gather 5 minerals (7 gold) after 4s (upto 60/minute)\n  or 4 gas (8 rich) after 3s (53/min)"; # lotv time scale
+
 [
-'patch 2.1.10',
+'patch 2.1.9+',
 # http://wiki.teamliquid.net/starcraft2/Unit_Statistics
 # http://wiki.teamliquid.net/starcraft2/User:Roemy/Unit_Statistics_(detailed)
 # http://starcraft.wikia.com/wiki/List_of_StarCraft_II_units
@@ -35,6 +38,13 @@ use strict;
        ],
        speed => 2.8125,
        sight => 8,
+       special => [
+               {
+                       name => 'worker',
+                       abbr => '⚒',
+                       desc => "warp-in buildings\n- $GATHER",
+               },
+       ],
 },
 
 {
@@ -147,6 +157,7 @@ use strict;
                psionic => 1,
                massive => 1,
                flying => 1,
+               heroic => 1,
        },
        attack => [
                {
@@ -195,6 +206,7 @@ use strict;
        race => 'protoss',
        cat => 'base',
        name => 'Nexus',
+       pop => -10,
        min => 400,
        gas => 0,
        build => 100,
@@ -271,12 +283,17 @@ use strict;
                        min => 200,
                        gas => 200,
                        build => 140,
-                       speed => .5,
                        range => 4,
                        duration => 3.5,
                        cooldown => 10,
                },
        ],
+       upgrade => [
+               {
+                       name => 'Charge',
+                       speed => .5,
+               },
+       ],
 },
 
 {
@@ -514,6 +531,85 @@ use strict;
        sight => 9,
 },
 
+{
+       race => 'protoss',
+       cat => 'robotic',
+       name => 'Observer',
+       pop => 1,
+       min => 25,
+       gas => 75,
+       build => 30,
+       size => 1,
+       cargo => 0,
+       armor => 0,
+       hp => 40,
+       shield => 20,
+       attr => {
+               light => 1,
+               mech => 1,
+               flying => 1,
+       },
+       speed => 1.875,
+       sight => 11,
+       detect => 1,
+       special => [
+               {
+                       name => 'Permanent Cloak',
+                       abbr => 'cl',
+                       desc => 'cloaked at all times',
+                       duration => -1,
+               },
+       ],
+       upgrade => [
+               {
+                       name => 'Gravitic Boosters',
+                       min => 100,
+                       gas => 100,
+                       build => 80,
+                       speed => $V ge v5.0.11 ? 1 : 0.9375, # 50% increase
+               },
+       ],
+},
+
+{
+       race => 'protoss',
+       cat => 'robotic',
+       name => 'Warp Prism',
+       pop => 2,
+       min => 200,
+       gas => 0,
+       build => 50,
+       size => 1.75,
+       cargo => -8,
+       armor => 0,
+       hp => 100,
+       shield => 100,
+       attr => {
+               armored => 1,
+               mech => 1,
+               psionic => 1,
+               flying => 1,
+       },
+       speed => 2.9531,
+       sight => 10,
+       special => [
+               {
+                       name => 'Phasing Mode',
+                       abbr => 'pm',
+                       desc => 'basically transforms into a hovering pylon',
+               },
+       ],
+       upgrade => [
+               {
+                       name => 'Gravitic Drive',
+                       min => 100,
+                       gas => 100,
+                       build => 80,
+                       speed => 0.422,
+               },
+       ],
+},
+
 {
        race => 'protoss',
        cat => 'robotic',
@@ -605,85 +701,6 @@ use strict;
        ],
 },
 
-{
-       race => 'protoss',
-       cat => 'robotic',
-       name => 'Observer',
-       pop => 1,
-       min => 25,
-       gas => 75,
-       build => 30,
-       size => 1,
-       cargo => 0,
-       armor => 0,
-       hp => 40,
-       shield => 20,
-       attr => {
-               light => 1,
-               mech => 1,
-               flying => 1,
-       },
-       speed => 1.875,
-       sight => 11,
-       detect => 1,
-       special => [
-               {
-                       name => 'Permanent Cloak',
-                       abbr => 'cl',
-                       desc => 'cloaked at all times',
-                       duration => -1,
-               },
-       ],
-       upgrade => [
-               {
-                       name => 'Gravitic Boosters',
-                       min => 100,
-                       gas => 100,
-                       build => 80,
-                       speed => 0.9375,
-               },
-       ],
-},
-
-{
-       race => 'protoss',
-       cat => 'robotic',
-       name => 'Warp Prism',
-       pop => 2,
-       min => 200,
-       gas => 0,
-       build => 50,
-       size => 1.75,
-       cargo => -8,
-       armor => 0,
-       hp => 100,
-       shield => 100,
-       attr => {
-               armored => 1,
-               mech => 1,
-               psionic => 1,
-               flying => 1,
-       },
-       speed => 2.9531,
-       sight => 10,
-       special => [
-               {
-                       name => 'Phasing Mode',
-                       abbr => 'pm',
-                       desc => 'basically transforms into a hovering pylon',
-               },
-       ],
-       upgrade => [
-               {
-                       name => 'Gravitic Drive',
-                       min => 100,
-                       gas => 100,
-                       build => 80,
-                       speed => 0.422,
-               },
-       ],
-},
-
 {
        race => 'protoss',
        cat => 'stargate',
@@ -1013,6 +1030,17 @@ use strict;
        ],
        speed => 2.8125,
        sight => 8,
+       special => [
+               {
+                       name => 'worker',
+                       abbr => '⚒',
+                       desc => join("\n- ",
+                               'construct buildings',
+                               'repair mechanical units and buildings (speed as build time but 25% cost)',
+                               $GATHER,
+                       ),
+               },
+       ],
 },
 
 {
@@ -1032,6 +1060,18 @@ use strict;
        },
        speed => 2.8,
        sight => 8,
+       special => [
+               {
+                       name => 'limited worker',
+                       abbr => '⛏',
+                       desc => join("\n- ",
+                               'repair like an SCV (but cannot build)',
+                               'gather 25 minerals after 6s: 200-225 over its 64s lifetime (equilavent to 3½ SCVs)',
+                       ),
+                       duration => 64,
+                       energy => 50,
+               },
+       ],
 },
 
 {
@@ -1261,8 +1301,6 @@ use strict;
                        ],
                        duration => 15,
                },
-       ],
-       upgrade => [
                {
                        name => 'Concussive Shells',
                        abbr => 'cs',
@@ -1349,7 +1387,16 @@ use strict;
        ],
        speed => 2.25,
        sight => 11,
-       energy => 75,
+       energy => $V ge v4.1.4 || $V lt v4.0.0 ? 75 : 50,
+       upgrade => [
+               $V ge v4.1.4 || $V lt v4.0.0 ? () : {
+                       name => 'Moebius Reactor',
+                       min => 100,
+                       gas => 100,
+                       build => 80,
+                       energy => 25,
+               },
+       ],
        capacity => 200,
        special => [
                {
@@ -1365,7 +1412,8 @@ use strict;
                        desc => 'reveals cloaked units and removes up to 100 shields and energy',
                        cost => 75,
                        range => 10,
-                       radius => 1.5,
+                       radius => 1.5, # 2 after upgrade
+                       detect => 1,
                },
                {
                        name => 'Personal Cloaking',
@@ -1380,8 +1428,9 @@ use strict;
                {
                        name => 'Nuclear Strike',
                        abbr => 'ns',
-                       desc => 'guides a nuclear missile which will do 300 damage plus 200 to buildings',
+                       desc => 'guides a missile which will do 300 damage plus 200 to buildings',
                        duration => 20,
+                       cooldown => 20,
                        range => 12,
                },
        ],
@@ -1431,8 +1480,8 @@ use strict;
        upgrade => [
                {
                        name => 'Infernal Pre-Igniter',
-                       min => 150,
-                       gas => 150,
+                       min => $V lt v4.11.0 ? 150 : 100,
+                       gas => $V lt v4.11.0 ? 150 : 100,
                        build => 110,
                        attack => [
                                {
@@ -1471,6 +1520,9 @@ use strict;
                        name => 'Napalm Spray',
                        damage => 18,
                        upgrade => 2,
+                       bonus => {
+                               light => 0, # visibility for upgrade
+                       },
                        splash => 1,
                        cooldown => 2,
                        range => 2,
@@ -1483,14 +1535,14 @@ use strict;
                        name => 'Hellion Mode',
                        abbr => 'hm',
                        desc => 'transform to Hellion',
-                       duration => 4,
+                       transform => 4,
                },
        ],
        upgrade => [
                {
                        name => 'Infernal Pre-Igniter',
-                       min => 150,
-                       gas => 150,
+                       min => $V lt v4.11.0 ? 150 : 100,
+                       gas => $V lt v4.11.0 ? 150 : 100,
                        build => 110,
                        attack => [
                                {
@@ -1531,6 +1583,7 @@ use strict;
                        splash => 1,
                        cooldown => 40,
                        range => 5,
+                       transform => 1.0, # time to burrow
                },
        ],
        speed => 2.8125,
@@ -1579,8 +1632,8 @@ use strict;
        ],
        special => [
                {
-                       name => 'siege mode',
-                       abbr => 'sg',
+                       name => 'Siege Mode',
+                       abbr => 'sm',
                        alt => 'Sieged Tank',
                        cargo => 0,
                        attack => [
@@ -1599,7 +1652,7 @@ use strict;
                                },
                        ],
                        speed => 0,
-                       duration => 4,
+                       transform => 4,
                },
        ],
        speed => 2.25,
@@ -1655,6 +1708,7 @@ use strict;
 #                      name => 'High Impact Payload',
                        damage => 24,
                        upgrade => 2,
+                       type => 'projectile',
                        cooldown => 2,
                        range => 10,
                },
@@ -1699,7 +1753,7 @@ use strict;
                {
                        name => 'Assault Mode',
                        abbr => 'am',
-                       build => 3, # transformation time
+                       transform => 3,
                        alt => 'Landed Viking',
                        cargo => 2,
                        attack => [
@@ -1839,7 +1893,7 @@ use strict;
        special => [
                {
                        alt => 'Auto-Turret',
-#                      abbr => 'at',
+                       abbr => 'at',
                        cost => 50,
                        size => 2,
                        cargo => 0,
@@ -1880,7 +1934,15 @@ use strict;
                                        build => 140,
                                        armor => 2,
                                },
+                               {
+                                       name => 'Durable Materials',
+                                       min => 150,
+                                       gas => 150,
+                                       build => 110,
+                                       duration => 60,
+                               },
                        ],
+                       duration => 180,
                },
                {
                        alt => 'Point Defense Drone',
@@ -1923,6 +1985,13 @@ use strict;
                                        build => 140,
                                        armor => 2,
                                },
+                               {
+                                       name => 'Durable Materials',
+                                       min => 150,
+                                       gas => 150,
+                                       build => 110,
+                                       duration => 10,
+                               },
                        ],
                        duration => 20,
                },
@@ -1942,20 +2011,6 @@ use strict;
                },
        ],
        upgrade => [
-               {
-                       name => 'Durable Materials',
-                       min => 150,
-                       gas => 150,
-                       build => 110,
-#                      special => {
-#                              at => {
-#                                      duration => 240,
-#                              },
-#                              pd => {
-#                                      duration => 30,
-#                              },
-#                      },
-               },
                {
                        name => 'Corvid Reactor',
                        min => 150,
@@ -1996,7 +2051,7 @@ use strict;
                },
                {
                        anti => 2,
-                       name => 'ATS Laser Batteries',
+                       name => 'ATA Laser Batteries',
                        damage => 6,
                        upgrade => 1,
                        cooldown => 0.225,
@@ -2067,6 +2122,13 @@ use strict;
        speed => 2.8125,
        creep => 1.0,
        sight => 8,
+       special => [
+               {
+                       name => 'worker',
+                       abbr => '⚒',
+                       desc => "morph into buildings\n- $GATHER",
+               },
+       ],
 },
 
 {
@@ -2172,8 +2234,8 @@ use strict;
        upgrade => [
                {
                        name => 'Pneumatized Carapace',
-                       min => 100,
-                       gas => 100,
+                       min => $V lt v4.10.1 || $V ge v4.11.0 ? 100 : 75,
+                       gas => $V lt v4.10.1 || $V ge v4.11.0 ? 100 : 75,
                        build => 60,
                        speed => 1.294,
                },
@@ -2214,6 +2276,7 @@ use strict;
        special => [
                {
                        name => 'Spawn Changeling',
+                       abbr => 'sc',
                        alt => 'Changeling',
                        cost => 50,
                        duration => 150,
@@ -2229,6 +2292,7 @@ use strict;
                        speed => 2.25,
                        creep => 1.0,
                        sight => 8,
+                       range => 0,
                },
                {
                        name => 'Contaminate',
@@ -2242,8 +2306,8 @@ use strict;
        upgrade => [
                {
                        name => 'Pneumatized Carapace',
-                       min => 100,
-                       gas => 100,
+                       min => $V lt v4.10.1 || $V ge v4.11.0 ? 100 : 75,
+                       gas => $V lt v4.10.1 || $V ge v4.11.0 ? 100 : 75,
                        build => 60,
                        speed => 3.375 - 1.875,
                },
@@ -2473,6 +2537,7 @@ use strict;
                        damage => 16,
                        upgrade => 2,
                        cooldown => 2.0,
+                       range => 4,
                },
        ],
        speed => 2.25,
@@ -2558,6 +2623,7 @@ use strict;
                        build => 100,
                        speed => 0.5625,
                        creep => -0.302,
+                       speed => $V ge v5.0.11 ? .98 : .79, #TODO
                },
        ],
 },
@@ -2604,13 +2670,14 @@ use strict;
                        range => 10,
                        duration => 4,
                        radius => 2.0,
+                       detect => 1,
                },
                {
                        alt => 'Infested Terran',
                        cost => 25,
                        range => 9,
                        duration => 30,
-                       build => 5,
+                       build => 4.8, # 5 normal but 3 faster
                        size => 0.75,
                        cargo => 0,
                        armor => 0,
@@ -2621,6 +2688,7 @@ use strict;
                        },
                        attack => [
                                {
+                                       name => 'Infested Rockets',
                                        anti => 3,
                                        damage => 8,
                                        cooldown => 0.8608,
@@ -2647,7 +2715,7 @@ use strict;
        race => 'zerg',
        cat => 'lair',
        name => 'Nydus Worm',
-       min => 100,
+       min => 100, # Nydus Network costs 150/200
        gas => 100,
        build => 20,
        size => 3,
@@ -2696,7 +2764,7 @@ use strict;
        special => [
                {
                        alt => 'Locust',
-                       build => 5,
+                       build => 4.8, # 5 normal but 3 faster
                        size => 0.75,
                        cargo => 0,
                        armor => 0,
@@ -2722,6 +2790,14 @@ use strict;
                        duration => 25,
                        cooldown => 60,
                        count => 2,
+                       upgrade => [
+                               {
+                                       # Flying Locusts
+                                       attr => {
+                                               flying => 1,
+                                       },
+                               },
+                       ],
                },
        ],
        upgrade => [
@@ -2730,9 +2806,6 @@ use strict;
                        min => 200,
                        gas => 200,
                        build => 160,
-                       attr => {
-                               flying => 1,
-                       },
                },
        ],
 },
@@ -2857,9 +2930,8 @@ use strict;
        special => [
                {
                        name => 'Swarm Seeds',
-                       abbr => 'ss',
                        desc => 'Broodlings are spawned upon each attack',
-                       duration => -1,
+                       duration => $V ge v5.0.11 ? 2.55 : 4,
                        alt => 'Broodling',
                        pop => 0,
 #                      build => 1,
diff --git a/sc-units-lotv.inc.pl b/sc-units-lotv.inc.pl
new file mode 100644 (file)
index 0000000..9a2433d
--- /dev/null
@@ -0,0 +1,1169 @@
+use utf8;
+use strict;
+
+my $V = v5.0.11;
+my $RT = 1.4;  # real-time speed factor to faster
+
+my $hots = do 'sc-units-hots.inc.pl' or die $!;
+my %unit;
+for my $row (@{$hots}) {
+       ref $row eq 'HASH' or next;
+       for ($row, @{ $row->{special} }, @{ $row->{upgrade} }) {
+               $_ *= $RT for $_->{speed} // ();
+               $_ /= $RT for $_->{build} // (), $_->{transform} // (), $_->{warp} // (), $_->{cooldown} // ();
+               $_->{cooldown} and $_->{cooldown} /= $RT for @{ $_->{attack} // [] };
+       }
+       $unit{ lc $row->{name} =~ tr/ /_/r } = $row;
+}
+
+[
+sprintf('patch %vd', $V),
+
+# protoss
+
+$unit{probe},
+
+$unit{photon_cannon},
+
+{
+       %{ $unit{mothership} },
+       speed => 2.62,
+       special => [
+               $unit{mothership}->{special}->[0], # cf
+               $unit{mothership}->{special}->[1], # mr
+               {
+                       %{ $unit{mothership}->{special}->[2] }, # tw
+                       desc => $V lt v4.11.0 ? 'create a temporal field which slows ground units by 50%'
+                                             : 'create a temporal field which slows ground and air units by 50%',
+               },
+       ],
+},
+
+{
+       %{ $unit{nexus} },
+       attack => [],
+       energy => 50,
+       capacity => 200,
+       special => [
+               $unit{nexus}->{special}->[0], # chrono boost
+               {
+                       name => $V lt v4.7.1 ? 'Mass Recall' : 'Strategic Recall',
+                       abbr => 'sr',
+                       desc => 'recalls units owned by the player in the target area to the Nexus',
+                       cost => 50,
+                       cooldown => $V lt v4.7.1 || $V ge v4.10.1 ? 130 : 85,
+                       radius => $V lt v4.7.1 ? 6.5 : 2.5,
+                       duration => 3.6, # +.7 warp in
+               },
+               $V lt v4.12.0 ? () : {
+                       name => 'Battery Overcharge',
+                       abbr => 'bo',
+                       desc => sprintf(
+                               'increase target Shield Battery restoration rate by %d%% and function without consuming energy for 14s',
+                               $V ge v5.0.11 ? 50 : 100,
+                       ),
+                       cost => 50,
+                       duration => 14,
+                       cooldown => 60, # shared by all nexuses
+                       range => 8, # within any friendly nexus
+               },
+       ],
+},
+
+{
+       %{ $unit{zealot} },
+       special => [
+               {
+                       name => 'Charge',
+                       abbr => 'ch',
+                       desc => 'on attack, increases movement speed to 8.47 for 2.5s and deals 8 damage on hit',
+                       min => 100, # changed in 3.14.0
+                       gas => 100,
+                       build => 100,
+                       speed => .5,
+                       range => 4,
+                       duration => 2.5,
+                       cooldown => 7,
+               },
+       ],
+       upgrade => [
+               {
+                       name => 'Charge',
+                       speed => $V lt v4.11.0 ? .98 : 1.57,
+               },
+       ],
+},
+
+{
+       %{ $unit{sentry} },
+       build => $V ge v5.0.11 ? 22.9 : 26.4,
+       speed => $V ge v5.0.11 ? 3.5 : 3.15,
+       special => [
+               $unit{sentry}->{special}->[0], # ff
+               {
+                       %{ $unit{sentry}->{special}->[1] }, # gs
+                       radius => $V lt v4.7.1 ? 4 : 4.5,
+               },
+               {
+                       %{ $unit{sentry}->{special}->[2] }, # hl
+                       cost => $V lt v4.7.1 ? 100 : 75,
+               },
+       ],
+},
+
+{
+       %{ $unit{stalker} },
+       attack => [
+               {
+                       %{ $unit{stalker}->{attack}->[0] }, # particle disruptors
+                       damage => 13,
+                       bonus => {
+                               armored => 5,
+                               -armored => 1,
+                       },
+                       cooldown => 1.34,
+               },
+       ],
+},
+
+{
+       race => 'protoss',
+       cat => 'gateway',
+       name => 'Adept',
+       pop => 2,
+       min => 100,
+       gas => 25,
+       build => $V lt v4.8.2 ? 27 : 30,
+       warp => 20,
+       size => 1,
+       cargo => 2,
+       armor => 1,
+       hp => 70,
+       shield => 70,
+       attr => {
+               light => 1,
+               organic => 1,
+       },
+       attack => [
+               {
+                       anti => 1,
+                       name => undef, #TODO
+                       damage => 10,
+                       upgrade => 1,
+                       bonus => {
+                               light => 12,
+                               -light => 1,
+                       },
+                       type => 'projectile', #XXX: assumption
+                       cooldown => 1.61,
+                       range => 4,
+               },
+       ],
+       speed => 3.5,
+       sight => 9,
+       special => [
+               {
+                       name => 'Psionic Transfer',
+                       abbr => 'pt',
+                       desc => 'project shade, teleport after 7s',
+                       cooldown => 11,
+
+                       alt => 'Shade',
+                       build => 0,
+                       size => 0,
+                       cargo => 0,
+                       hp => -1,
+                       attack => [], #TODO: indicate diff from parent
+                       speed => 5, #XXX: faster than 3.5
+                       range => 7, #XXX: calculate from speed
+                       sight => 4,
+               },
+       ],
+       upgrade => [
+               {
+                       name => 'Resonating Glaives',
+                       min => 100,
+                       gas => 100,
+                       build => 100,
+                       attack => [
+                               {
+                                       cooldown => $V lt v4.11.0 || $V ge v4.11.3 ? -.5 : -.6, # +45%/60%
+                               },
+                       ],
+               },
+       ],
+},
+
+{
+       %{ $unit{high_templar} },
+       speed => $V ge v5.0.11 ? 2.82 : 2.63,
+       attack => [
+               {
+                       anti => 1,
+                       name => '?', #XXX
+                       damage => 4,
+                       upgrade => 1,
+                       cooldown => 1.25,
+                       range => 6,
+               },
+       ],
+       special => [
+               {
+                       $unit{high_templar}->{special}->[0], # fb
+                       range => $V lt v4.12.0 ? 9 : 10,
+               },
+               $unit{high_templar}->{special}->[1], # ps
+       ],
+},
+
+{
+       %{ $unit{dark_templar} },
+       special => [
+               $unit{dark_templar}->{special}->[0], # cl
+               {
+                       name => 'Shadow Stride',
+                       abbr => 'bl',
+                       desc => sprintf('teleport to visible location, %.2fs attack delay afterwards', $V ge v5.0.11 ? .71 : .75),
+                       min => 100,
+                       gas => 100,
+                       build => $V lt v4.7.1 ? 121 : 100,
+                       range => 8,
+                       cooldown => $V lt v4.7.1 ? 21 : 14,
+               },
+       ],
+},
+
+$unit{archon},
+
+{
+       %{ $unit{observer} },
+       speed => $V ge v5.0.11 ? 2.82 : $V lt v4.8.2 || $V ge v4.11.0 ? 2.63 : 3.01,
+       size => $V ge v5.0.11 ? 1.1 : 1,
+       special => [
+               $unit{observer}->{special}->[0], # cloak
+               {
+                       name => 'Surveillance Mode',
+                       abbr => 'sm',
+                       desc => 'gain 25% vision while immobilized',
+                       speed => 0,
+                       sight => 2.75, # +25%
+                       transform => .54,
+               },
+       ],
+},
+
+{
+       %{ $unit{warp_prism} },
+       hp => 80,
+       min => $V lt v4.10.1 ? 200 : 250,
+       range => $V lt v4.10.1 ? 6 : 5, # pickup
+       upgrade => [
+               {
+                       %{ $unit{warp_prism}->{special}->[0] }, # gravitic drive
+                       speed => 1.23,
+               },
+       ],
+},
+
+$unit{immortal},
+
+{
+       %{ $unit{colossus} },
+       attack => [
+               {
+                       %{ $unit{colossus}->{attack}->[0] }, # thermal lances
+                       damage => 10,
+                       upgrade => 1,
+                       bonus => {
+                               light => 5,
+                               -light => 1,
+                       },
+#                      cooldown => 1.18, #XXX
+               },
+       ],
+},
+
+{
+       race => 'protoss',
+       cat => 'robotic',
+       name => 'Disruptor',
+       pop => 3,
+       min => 150,
+       gas => 150,
+       build => 36,
+       size => 1,
+       cargo => 4,
+       armor => 1,
+       hp => 100,
+       shield => 100,
+       attr => {
+               armored => 1,
+               mech => 1,
+       },
+       attack => [
+               {
+                       anti => 1,
+                       name => 'Purification Nova',
+                       damage => 145,
+                       bonus => {
+                               shields => 55,
+                       },
+                       type => 'trans', #TODO: indicate
+                       splash => $V ge v5.0.11 ? 1.375 : 1.5,
+                       cooldown => 14.3,
+                       range => 13, # 2s
+               },
+       ],
+       speed => 3.15,
+       sight => 9,
+},
+
+$unit{phoenix},
+
+{
+       %{ $unit{void_ray} },
+       min => $V lt v5.0.9 && $V ge v5.0.2 ? 200 : 250,
+       speed => $V lt v5.0.2 ? 3.5 : 3.85,
+       build => $V lt v5.0.9 && $V ge v5.0.2 ? 37 : 43,
+       upgrade => [
+               {
+                       name => 'Flux Vanes',
+                       min => 100,
+                       gas => 100,
+                       build => 57,
+                       speed => $V lt v5.0.2 ? 1.15 : .798,
+               },
+       ],
+},
+
+{
+       %{ $unit{oracle} },
+       build => 37,
+       attr => {
+               $V lt v4.8.2 ? 'light' : 'armored' => 1,
+               mech => 1,
+               psionic => 1,
+               flying => 1,
+       },
+       attack => [
+               {
+                       %{ $unit{oracle}->{attack}->[0] }, # pulsar beam
+                       bonus => {
+                               light => 7,
+                       },
+               },
+       ],
+       special => [
+               {
+                       name => 'Revelation',
+                       abbr => 'rv',
+                       desc => 'hit enemy units and buildings are revealed for ½ minute',
+                       cost => $V lt v4.12.0 ? 50 : 25,
+                       range => 9,
+                       cooldown => $V lt v4.12.0 ? 2 : 10,
+                       duration => $V lt v4.12.0 ? 30 : $V lt v5.0.2 ? 15 : 20,
+                       radius => 6,
+                       detect => 1,
+               },
+               {
+                       name => 'Stasis Ward',
+                       abbr => 'sw',
+                       desc => 'places ward for 170s',
+                       cost => 50,
+                       range => 6,
+                       build => 3.58,
+                       duration => 170,
+                       alt => 'Stasis Ward',
+                       hp => 30,
+                       shield => 30,
+                       armor => 0,
+                       attr => {
+                               light => 1,
+                               structure => 1,
+                       },
+                       sight => 0,
+                       speed => 0,
+                       special => [
+                               {
+                                       name => 'Permanent Cloak',
+                                       abbr => 'cl',
+                                       desc => 'cloaked at all times',
+                                       duration => -1,
+                               },
+                               {
+                                       name => 'Stasis Trap',
+                                       abbr => 'st',
+                                       desc => 'triggered by nearby ground units, trapping them for 21½s',
+                               },
+                       ],
+               },
+       ],
+},
+
+{
+       %{ $unit{tempest} },
+       hp => $V lt v4.7.1 ? 300 : $V lt v4.11.0 ? 150 : 200,
+       shield => $V lt v4.7.1 ? 150 : $V lt v4.11.0 ? 125 : 100,
+       min => $V lt v4.7.1 ? 300 : 250,
+       gas => $V lt v4.7.1 ? 200 : 175,
+       pop => $V lt v4.7.1 ? 6 : 5,
+       speed => $V lt v4.7.1 ? 2.63 : $V lt v4.8.2 ? 3.5 : 3.15,
+       attack => [
+               {
+                       %{ $unit{tempest}->{attack}->[0] }, # kinetic overload
+                       bonus => {
+                               massive => 22,
+                               -massive => 2,
+                       },
+                       range => $V lt v4.11.0 ? 15 : 14,
+               },
+               {
+                       %{ $unit{tempest}->{attack}->[1] }, # resonance coil
+                       bonus => {
+                               structure => 0,
+                       },
+                       damage => 40,
+                       upgrade => 4,
+               },
+       ],
+       upgrade => [
+               $V lt v5.0.2 ? () : {
+                       name => 'Tectonic Destabilizers',
+                       attack => [
+                               {},
+                               {
+                                       bonus => {
+                                               structure => 40,
+                                       },
+                               },
+                       ],
+                       min => 150,
+                       gas => 150,
+                       build => 100,
+               },
+       ],
+},
+
+{
+       %{ $unit{carrier} },
+       hp => $V lt v4.7.1 ? 250 : 300,
+       build => $V lt v4.7.1 ? 86 : 64,
+       special => [
+               {
+                       %{ $unit{carrier}->{special}->[0] }, # interceptor
+                       min => 5,
+                       build => $V lt v4.7.1 ? 6 : $V lt v4.10.1 ? 11 : 9,
+               },
+       ],
+       $V lt v4.7.1 ? () : (upgrade => []), # remove Graviton Catapult
+},
+
+# terran
+
+$unit{scv},
+$unit{mule},
+$unit{missile_turret},
+$unit{planetary_fortress},
+
+$unit{marine},
+
+{
+       %{ $unit{marauder} },
+       attack => [
+               {
+                       %{ $unit{marauder}->{attack}->[0] }, # punisher grenades
+                       count => $V lt v4.3.0 ? 2 : 1,
+                       damage => $V lt v4.3.0 ? 5 : 10,
+                       upgrade => 1,
+                       bonus => {
+                               armored => $V lt v4.3.0 ? 5 : 10,
+                               -armored => $V lt v4.3.0 ? 0 : 1,
+                       },
+               },
+       ],
+},
+
+{
+       %{ $unit{reaper} },
+       build => 32,
+},
+
+{
+       %{ $unit{ghost} },
+       min => 150,
+       gas => 125,
+       speed => 3.94,
+       special => [
+               {
+                       # replaces Sniper Round
+                       name => 'Steady Targeting',
+                       abbr => 'st',
+                       desc => '170 damage ignoring armor to a biological unit after 1.43s without damage',
+                       cost => 50,
+                       range => 10, # kept until 14
+                       duration => 1.43,
+               },
+               {
+                       %{ $unit{ghost}->{special}->[1] }, # emp round
+                       radius => $V ge v5.0.11 ? 1.75 : $V lt v4.10.1 ? 1.5 : 2,
+               },
+               $unit{ghost}->{special}->[2], # cloak
+               $unit{ghost}->{special}->[3], # tac nuke strike
+       ],
+       upgrade => [
+               $V ge v5.0.11 || $V lt v4.10.1 ? () : {
+                       name => 'Enhanced Shockwaves',
+                       min => 150,
+                       gas => 150,
+                       build => 79,
+                       special => [
+                               {},
+                               { radius => .5 }, # emp
+                               {},
+                               {},
+                       ],
+               },
+       ],
+},
+
+{
+       %{ $unit{hellion} },
+       attack => [
+               {
+                       %{ $unit{hellion}->{attack}->[0] }, # infernal flamethrower
+                       bonus => {
+                               light => 5,
+                               -light => 0,
+                       },
+               },
+       ],
+       #TODO smart servos
+},
+
+{
+       %{ $unit{hellbat} },
+       special => [
+               $unit{hellbat}->{special}->[0], # Hellion Mode
+               {
+                       name => 'Smart Servos',
+                       min => 100,
+                       gas => 100,
+                       build => 79,
+                       transform => -1.43, # halve #TODO: alter special duration?
+               },
+       ],
+},
+
+{
+       %{ $unit{widow_mine} },
+       build => 21,
+       attack => [
+               {
+                       %{ $unit{widow_mine}->{attack}->[0] }, # Sentinel Missiles
+                       bonus => {
+                               shields => 25,
+                       },
+                       transform => $V lt v5.0.9 ? .71 : 1.07,
+               },
+       ],
+},
+
+{
+       %{ $unit{siege_tank} },
+       hp => 175,
+       special => [
+               {
+                       %{ $unit{siege_tank}->{special}->[0] }, # siege mode
+                       attack => [
+                               {
+                                       %{ $unit{siege_tank}->{special}->[0]->{attack}->[0] }, # shock cannon
+                                       damage => 40,
+                                       upgrade => 4,
+                                       bonus => {
+                                               armored => 30,
+                                               -armored => 1,
+                                       },
+                                       cooldown => 2.14,
+                               },
+                       ],
+               },
+       ],
+},
+
+{
+       race => 'terran',
+       cat => 'factory',
+       name => 'Cyclone',
+       pop => 3,
+       min => 150,
+       gas => 100,
+       build => 32,
+       size => 1.5,
+       cargo => 3,
+       armor => 1,
+       hp => $V lt v4.7.1 ? 180 : 120,
+       attr => {
+               armored => 1,
+               mech => 1,
+       },
+       attack => [
+               {
+                       anti => 1,
+                       name => $V lt v4.7.1 ? 'Tornado Blaster' : 'Typhoon Missile Pod',
+                       damage => $V lt v4.7.1 ? 3 : 18,
+                       upgrade => $V lt v4.7.1 ? 1 : 2,
+                       bonus => $V ge v4.7.1 ? {} : {
+                               armored => 2,
+                               -armored => 0,
+                       },
+                       cooldown => $V lt v4.7.1 ? .1 : .71,
+                       range => $V lt v4.7.1 ? 6 : 5,
+               },
+       ],
+       speed => $V lt v4.7.1 ? 4.13 : 4.73,
+       sight => 11,
+       special => [
+               {
+                       name => 'Lock On',
+                       abbr => 'lo',
+                       desc => (
+                               $V ge v5.0.11 ? 'deal 400 damage (600 after upgrade) over 14 seconds' :
+                               $V ge v4.7.1 ? 'deal 400 damage (double to armored after upgrade) over 14 seconds' :
+                               'target air for 160 damage ignoring armor while visible and within 15 range'
+                       ),
+                       range => 7,
+                       duration => 14.3,
+                       cooldown => 4,
+               },
+               $V ge v4.7.1 ? () : {
+                       name => 'Rapid Fire Launchers',
+                       abbr => 'rf',
+                       desc => 'rapid first 12 Lock On shots',
+                       min => 75,
+                       gas => 75,
+                       build => 79,
+               },
+       ],
+       upgrade => [
+               $V lt v4.7.1 ? () : {
+                       name => 'Mag-Field Accelerator',
+                       min => 100,
+                       gas => 100,
+                       build => $V lt v4.8.2 ? 79 : 100,
+                       desc => 'increases lock-on damage by '.($V ge v5.0.11 ? '50%' : '100% vs armored'),
+               },
+       ],
+},
+
+{
+       %{ $unit{thor} },
+       armor => $V ge v3.14.0 && $V lt v4.7.1 ? 2 : 1,
+       attack => [
+               $unit{thor}->{attack}->[0], # thor's hammer
+               $unit{thor}->{attack}->[1], # javelin missiles
+               {
+                       %{ $unit{thor}->{attack}->[2] }, # punisher cannons
+                       name => 'High Impact Payload',
+                       damage => $V lt v4.7.1 ? 35 : $V lt v4.11.0 ? 40 : 25,
+                       upgrade => 3,
+                       bonus => {
+                               $V lt v4.7.1 ? 'armored' : 'massive' => $V lt v4.11.0 ? 15 : 10,
+                               $V lt v4.7.1 ? '-armored' : '-massive' => 2,
+                       },
+                       cooldown => $V lt v4.7.1 ? 2.14 : $V lt v4.11.0 ? 1.71 : .9,
+                       range => $V lt v4.8.2 ? 10 : 11,
+               },
+       ],
+       #TODO smart servos
+},
+
+{
+       %{ $unit{viking} },
+       hp => $V lt v4.3.0 ? 125 : 135,
+},
+
+{
+       %{ $unit{medivac} },
+       special => [
+               $unit{medivac}->{special}->[0], # heal
+               {
+                       %{ $unit{medivac}->{special}->[1] }, # ignite afterburners
+                       desc => 'boost speed and accelleration to 4.25 for 8s',
+                       speed => 5.94,
+                       duration => $V lt v4.7.1 ? 6.43 : 4.29,
+                       cooldown => $V lt v4.11.0 ? 20 : 14,
+                       -cooldown => 9,
+               },
+       ],
+       upgrade => [
+               {
+                       name => 'Rapid Reignition System',
+                       min => 100,
+                       gas => 100,
+                       build => 57,
+                       speed => .63,
+               },
+       ],
+},
+
+{
+       race => 'terran',
+       cat => 'starport',
+       name => 'Liberator',
+
+       pop => 3,
+       min => 150,
+       gas => $V ge v5.0.11 ? 125 : 150,
+       build => 43,
+       size => 1.5,
+       armor => 0,
+       hp => 180,
+       attr => {
+               armored => 1,
+               mech => 1,
+               flying => 1,
+       },
+       attack => [
+               {
+                       anti => 2,
+                       name => 'Lexington Rockets',
+                       damage => 5,
+                       upgrade => 1,
+                       type => 'projectile',
+                       cooldown => 1.29,
+                       count => 2,
+                       range => 5,
+               },
+       ],
+       special => [
+               {
+                       name => 'Defender Mode',
+                       abbr => 'dm',
+                       transform => 2.88, # 1.46s to revert
+                       alt => 'Defender Liberator',
+                       attack => [
+                               {
+                                       anti => 1,
+                                       name => 'Concord Cannon',
+                                       desc => 'within 5 diameter circle',
+                                       damage => 75,
+                                       upgrade => 5,
+                                       cooldown => 1.14,
+                                       range => 10,
+                               },
+                       ],
+                       speed => 0,
+                       sight => 15, # only targeted area
+                       upgrade => [
+                               {
+                                       name => 'Advanced Ballistics',
+                                       min => 150,
+                                       gas => 150,
+                                       build => 79,
+                                       attack => [
+                                               {
+                                                       range => 4,
+                                               },
+                                       ],
+                                       sight => 4,
+                               },
+                       ],
+               },
+       ],
+       speed => 4.72,
+       sight => 10,
+},
+
+{
+       %{ $unit{banshee} },
+       speed => 3.85,
+       upgrade => [
+               {
+                       name => 'Hyperflight Rotors',
+                       speed => 1.4,
+                       min => $V ge v5.0.11 ? 125 : $V ge v4.7.1 ? 150 : 200,
+                       gas => $V ge v5.0.11 ? 125 : $V ge v4.7.1 ? 150 : 200,
+                       build => $V ge v5.0.11 ? 100 : 121.4,
+               },
+       ],
+},
+
+{
+       %{ $unit{raven} },
+       gas => $V ge v5.0.11 ? 150 : 200,
+       build => $V ge v5.0.11 ? 34.3 : 42.9,
+       speed => $V lt v4.11.0 ? 3.85 : 4.13,
+       upgrade => $V ge v5.0.11 ? [] : $unit{raven}->{upgrade}, # corvid reactor
+       special => [
+               {
+                       %{ $unit{raven}->{special}->[0] }, # auto-turret
+                       attack => [
+                               {
+                                       anti => 3,
+                                       name => '12 mm Gauss Cannon',
+                                       damage => 18,
+                                       cooldown => .57,
+                                       range => 6,
+                               },
+                       ],
+                       hp => $V ge v5.0.11 ? 100 : 150,
+                       armor => $V ge v5.0.11 ? 0 : 1,
+                       range => $V lt v4.3.0 ? 1 : 2,
+                       duration => $V ge v5.0.11 ? 7.9 : 10,
+                       upgrade => [
+                               $unit{raven}->{special}->[0]->{upgrade}->[0], # hi-sec auto tracking
+                               $unit{raven}->{special}->[0]->{upgrade}->[1], # structure armor
+                               # no more durable materials
+                       ],
+               },
+               {
+                       name => 'Interference Matrix',
+                       abbr => 'im',
+                       desc => 'disable target mech or psionic unit rendering it unable to attack or cast',
+                       cost => $V lt v4.11.0 ? 50 : 75,
+                       range => 9,
+                       duration => $V lt v4.11.0 ? 7.9 : 11,
+               },
+               {
+                       name => 'Anti-Armor Missile',
+                       abbr => 'aa',
+                       desc => sprintf('launches missile %s reduce armor by %d',
+                               $V lt v4.7.1 ? 'to do splash damage and' : 'to',
+                               $V ge v5.0.11 ? 2 : 3,
+                       ),
+                       range => 10,
+                       size => 2.88,
+                       attack => [
+                               {
+                                       damage => $V lt v4.3.0 ? 30 : $V lt v4.7.1 ? 15 : 0,
+                                       splash => 1,
+                               },
+                       ],
+                       cost => 75,
+                       duration => 21,
+               },
+       ],
+},
+
+{
+       %{ $unit{battlecruiser} },
+       attack => [
+               $unit{battlecruiser}->{attack}->[0], # ats laser
+               {
+                       %{ $unit{battlecruiser}->{attack}->[1] }, # ata laser
+                       damage => $V lt v4.7.1 ? 6 : 5,
+               },
+       ],
+       special => [
+               {
+                       name => 'Tactical Jump',
+                       abbr => 'tj',
+                       desc => 'warps to the target location after 5s (invulnerable after 1s)',
+                       duration => 4,
+                       cooldown => 71,
+               },
+               {
+                       %{ $unit{battlecruiser}->{special}->[0] }, # yc
+                       desc => '240 damage to a single target',
+                       attack => [
+                               {
+                                       damage => 240,
+                               },
+                       ],
+                       cost => undef,
+                       cooldown => 71,
+               },
+       ],
+       upgrade => [],
+},
+
+# zerg
+
+$unit{drone},
+
+{
+       %{ $unit{queen} },
+       attack => [
+               $unit{queen}->{attack}->[0], # claws
+               {
+                       %{ $unit{queen}->{attack}->[1] }, # acid spines
+                       range => $V lt v4.12.0 ? 8 : 7,
+               },
+       ],
+       range => 8,
+},
+
+{
+       %{ $unit{overlord} },
+       speed => .902, # changed in 4.0.0
+       upgrade => [
+               {
+                       %{ $unit{overlord}->{upgrade}->[0] }, # carapace
+                       speed => 2.63-.902,
+               },
+               $unit{overlord}->{upgrade}->[1], # sacs
+       ],
+},
+
+{
+       %{ $unit{overseer} },
+       special => [
+               {
+                       name => 'Oversight Mode',
+                       abbr => 'om',
+                       desc => 'gain 25% vision while immobilized',
+                       speed => 0,
+                       sight => 11 * 1.25,
+                       transform => .54,
+               },
+               @{ $unit{overseer}->{special} }, # changeling, contaminate
+       ],
+},
+
+$unit{larva},
+$unit{spine_crawler},
+
+{
+       %{ $unit{spore_crawler} },
+       attack => [
+               {
+                       %{ $unit{spore_crawler}->{attack}->[0] }, # seeker spores
+                       bonus => {
+                               organic => 15,
+                       },
+               },
+       ],
+       speed => (1.5),
+       creep => 2.6,
+},
+
+$unit{zergling},
+
+{
+       %{ $unit{baneling} },
+       min => 25,
+       attack => [
+               {
+                       %{ $unit{baneling}->{attack}->[0] }, # volatile burst
+                       damage => $V lt v4.12.0 ? 20 : $V lt v5.0.2 ? 18 : 15,
+                       bonus => {
+                               light => $V lt v4.12.0 ? 15 : $V lt v5.0.2 ? 17 : 20,
+                               -light => 2,
+                               structure => 80,
+                               -structure => 5,
+                       },
+                       splash => 1,
+                       range => 0,
+               },
+       ],
+       upgrade => [
+               {
+                       %{ $unit{baneling}->{upgrade}->[0] }, # centrifugal hooks
+                       hp => 5,
+               },
+       ],
+},
+
+{
+       %{ $unit{roach} },
+       special => [
+               {
+                       %{ $unit{roach}->{special}->[0] }, # rapid regeneration
+                       desc => 'regenerates health at 7 HP/s while burrowed',
+               },
+               {
+                       %{ $unit{roach}->{special}->[1] }, # tunneling claws
+                       min => $V lt v4.7.1 ? 150 : 100,
+                       gas => $V lt v4.7.1 ? 150 : 100,
+                       build => 79,
+                       desc => 'move while burrowed at speed of 2.8',
+               },
+       ],
+},
+
+{
+       race => 'zerg',
+       cat => 'hatchery',
+       name => 'Ravager',
+       base => ['Roach'],
+       pop => 3,
+       min => 25,
+       gas => 75,
+       build => $V ge v5.0.11 ? 12.14 : 8.57+.36, # added max random
+       size => 1.5,
+       cargo => 4,
+       armor => 1,
+       hp => 120,
+       attr => {
+               organic => 1,
+       },
+       attack => [
+               {
+                       anti => 1,
+                       name => '?',
+                       damage => 16,
+                       upgrade => 2,
+                       type => 'projectile',
+                       cooldown => 1.14,
+                       range => 6,
+               },
+       ],
+       speed => 3.85,
+       creep => 1.3,
+       sight => 9,
+},
+
+{
+       %{ $unit{hydralisk} },
+       hp => 90,
+       upgrade => [
+               {
+                       %{ $unit{hydralisk}->{upgrade}->[0] }, # grooved spines
+                       attack => [
+                               {
+                                       range => 2,
+                               },
+                       ],
+               },
+               $unit{hydralisk}->{upgrade}->[1], # muscular augments
+       ],
+},
+
+{
+       race => 'zerg',
+       cat => 'lair',
+       name => 'Lurker',
+       base => ['Hydralisk'],
+       pop => 3,
+       min => 50,
+       gas => 100,
+       build => 18,
+       cargo => 4,
+       armor => 1,
+       hp => 200,
+       attr => {
+               armored => 1,
+               organic => 1,
+       },
+       attack => [
+               {
+                       anti => 1,
+                       name => 'Spines',
+                       damage => 20,
+                       upgrade => 2,
+                       splash => 'line',
+                       cooldown => 1.43,
+                       range => $V lt v4.11.0 ? 9 : 8,
+                       bonus => {
+                               armored => 10,
+                               -armored => 1,
+                       },
+               },
+       ],
+       transform => 2.0,
+       upgrade => [
+               $V lt v4.11.0 ? () : {
+                       name => 'Seismic Spines',
+                       attack => [
+                               {
+                                       range => 2,
+                               },
+                       ],
+                       req => 'Hive',
+                       min => 150,
+                       gas => 150,
+                       build => 57,
+               },
+               {
+                       name => 'Adaptive Talons',
+                       desc => 'halves burrow time and increases movement speed',
+                       speed => .413,
+                       transform => $V lt v5.0.9 ? .71 : 1.07,
+               },
+       ],
+       speed => 4.13,
+       creep => 1.3,
+       sight => 10,
+},
+
+{
+       %{ $unit{infestor} },
+       special => [
+               {
+                       %{ $unit{infestor}->{special}->[0] }, # neural parasite
+                       range => $V lt v4.11.0 ? 9 : 8,
+               },
+               $unit{infestor}->{special}->[1], # fungal growth
+               {
+                       name => 'Microbial Shroud',
+                       abbr => 'ms',
+                       desc => 'reduce damage from air by 50% to ground units in target area',
+                       cost => $V lt v4.11.3 ? 100 : 75,
+                       min => $V lt v4.11.3 ? 150 : undef,
+                       gas => $V lt v4.11.3 ? 150 : undef,
+                       build => $V lt v4.11.3 ? 79 : undef,
+                       req => $V lt v4.11.3 ? 'Hive' : undef,
+                       range => 9,
+                       duration => 11,
+                       radius => $V lt v4.11.3 ? 3 : 3.5,
+               },
+               $V ge v4.11.0 ? () : {
+                       %{ $unit{infestor}->{special}->[2] }, # infested terran
+                       attack => [
+                               {
+                                       %{ $unit{infestor}->{special}->[2]->{attack}->[0] }, # rockets
+                                       cooldown => $V lt v4.10.1 ? .95 : 1.14,
+                               },
+                       ],
+               },
+       ],
+},
+
+{
+       % {$unit{nydus_worm} },
+       min => $V lt v4.7.1 ? 100 : $V lt v4.11.0 ? 50 : 75, # Nydus Network costs 150/150
+       gas => $V lt v4.7.1 ? 100 : $V lt v4.11.0 ? 50 : 75,
+       cooldown => $V lt v4.11.0 ? 0 : 14, # Summon Nydus Worm
+},
+
+{
+       %{ $unit{swarm_host} },
+       gas => 75,
+       special => [
+               {
+                       %{ $unit{swarm_host}->{special}->[0] }, # locust
+                       hp => 50,
+                       upgrade => [], # no flying
+               },
+       ],
+       upgrade => [],
+},
+
+$unit{mutalisk},
+$unit{corruptor},
+
+{
+       %{ $unit{brood_lord} },
+       speed => $V ge v5.0.11 ? 2.24 : 1.97,
+},
+
+$unit{viper},
+
+{
+       %{ $unit{ultralisk} },
+       armor => 2,
+       size => $V ge v5.0.11 ? 1.75 : 2,
+       upgrade => [
+               $unit{ultralisk}->{upgrade}->[0], # chitinous plating
+               {
+                       name => 'Anabolic Synthesis',
+                       min => 150,
+                       gas => 150,
+                       build => 42.85,
+                       speed => $V lt v4.8.2 ? .41 : .82,
+                       creep => -.215, # reverse speed increase
+               },
+       ],
+},
+
+]
diff --git a/sc.css b/sc.css
index 9aba72854702d34218518b6f45d131e1d2810a2c..0c0a829c1b670bf950ed237363c626fef4c48264 100644 (file)
--- a/sc.css
+++ b/sc.css
@@ -17,9 +17,6 @@ tr.alt td {
 .units tr th:first-child {
        padding-left: 0;
 }
-.units tbody tr:hover:not(.race) {
-       background: #EEE;
-}
 
 table h2 {
        padding: 1ex 0;
@@ -37,30 +34,9 @@ table h2 {
 .units td.unit + td.unit {
        padding-left: 0;
 }
-.gas {
-       color: #040;
-}
-.min {
-       color: #004;
-}
-.unit-supply {
-       color: #080;
-}
-.unit-o {color: #C08} /* organic */
-.unit-u {color: #44C} /* mechanic */
-.unit-p {color: #0A8} /* psionic */
 .unit-composed {
-       color: #C88;
        font-size: 70%;
 }
-.unit-air {
-       color: #08C;
-}
-.unit-x {color: #888}
-.unit-s {color: #890}
-.unit-m {color: #C70}
-.unit-l {color: #D22}
-.unit-h {color: #804}
 td .unit-jump,
 .hurt .unit-splash {
        position: absolute;
@@ -69,9 +45,6 @@ td .unit-massive {
        float: right;
        width: 0;
 }
-.hurtrel, .units .hurtrel {
-       color: #778;
-}
 td.hurtrel {
        padding-left: 1em;
 }
@@ -79,52 +52,27 @@ td.unit-shield,
 td.hurtrel {
        font-size: 70%;
 }
-tbody .unit-shield {
-       color: #64A;
-}
-.unit-pdd {
-       color: #A8C;
-}
-.unit-splash {
-       color: #4A0;
-}
-.hurt-a {
-       color: #036;
-}
-.hurt-g {
-       color: #063;
-}
-.unit-massive {
-       color: #D88;
-}
 .unit-detect::before {
        content: '!';
-       color: #0A8;
        font-size: 70%;
        vertical-align: super;
 }
 .unit-jump {
        margin-left: -.2em;
-       color: #8A4;
 }
 .unit-magic {
        padding-left: 0.5em;
 }
-.magic-opt::before {
-       color: #000;
+.magic-opt:before {
        content: '(';
        position: absolute;
        margin-left: -0.33em;
 }
-.magic-opt::after {
-       color: #000;
+.magic-opt:after {
        content: ')';
 }
 .magic-perma {
-       text-decoration: underline;
-               text-decoration-color: #8C0;
-          -moz-text-decoration-color: #8C0;
-       -webkit-text-decoration-color: #8C0;
+       font-variant: small-caps;
 }
 
 .units .val {
@@ -157,3 +105,79 @@ tbody .unit-shield {
        margin-right: 2em;
 }
 
+@media (max-width: 52em) {
+       .units thead th:first-child,
+       .units tbody .cat {
+               position: absolute;
+               visibility: hidden;
+       }
+}
+@media (max-width: 48em) {
+       .units {
+               width: auto;
+       }
+       .units th, .units td {
+               vertical-align: top;
+       }
+       .units td {
+               height: 2em;
+       }
+
+       .units td:nth-child(2) {
+               white-space: normal;
+               padding: 0;
+       }
+
+       .units tr.sub td:nth-child(2),
+       .units .cat,
+       .units .unit-speed,
+       .units .hurtrel,
+       .units .unit-pop, .units .unit-type,
+       .units .unit-shield,
+       .units .unit-gas {
+               padding: 0;
+               position: absolute;
+               margin-top: 3.2ex;
+               margin-top: 1rem;
+               min-width: 4em;
+               text-align: right;
+               margin-left: -4.3em;
+               font-size: 70%;
+       }
+       .units th.unit-attr {
+               position: absolute;
+       }
+       .units .unit-type {
+               text-align: left;
+               margin-left: 0;
+       }
+       .units .unit-speed {
+               margin-left: -6em;
+       }
+       .units .alt thead th:first-child,
+       .units .alt .cat,
+       .units .alt .unit-speed,
+       .units .alt .hurtrel,
+       .units .alt .unit-pop, .units .alt .unit-type,
+       .units .alt .unit-shield,
+       .units .alt .gas {
+               margin-top: 2ex;
+       }
+       .units .cat {
+               margin-top: -2ex;
+               margin-left: 0;
+               text-align: left;
+       }
+
+       .units thead th:first-child {
+               margin-left: 0.5em;
+       }
+       .units thead th:first-child,
+       .units th.unit-speed,
+       .units th.hurtrel,
+       .units th.unit-shield,
+       .units th.gas {
+               margin-top: 2.2ex;
+       }
+}
+
diff --git a/sc.plp b/sc.plp
index 3437f8f4aca51ce2da337d4f36f10682a47a93ab..e784cbdd6dbe6a67886bf98e5beb85c92f3c6463 100644 (file)
--- a/sc.plp
+++ b/sc.plp
@@ -1,28 +1,44 @@
 <(common.inc.plp)><:
 use List::Util qw(max sum);
 
-my %scver = (
-       id => 'bw',
-       name => 'Brood War',
-       title => 'starcraft',
-       game => 'StarCraft',
-       major => 1,
-);
-
-if ($Request and $Request eq '2') {
-       %scver = (
-               id => 'hots',
+my %scvers = (
+       bw => {
+               name => 'Brood War',
+               title => 'starcraft',
+               game => 'StarCraft BW',
+               major => 1,
+       },
+       hots => {
                name => 'Heart of the Swarm',
-               title => 'starcraft2',
-               game => 'StarCraft II',
+               title => 'starcraft2 hots',
+               game => 'StarCraft II HotS',
                major => 2,
-       );
+       },
+       lotv => {
+               name => 'Legacy of the Void',
+               title => 'starcraft2 lotv',
+               game => 'StarCraft II LotV',
+               major => 2,
+       },
+       index => 'bw',
+       1     => 'bw',
+       2     => 'lotv',
+);
+
+my $requestver = $scvers{$Request ||= 'index'}
+       or Html(), Abort("Requested version <q>$Request</q> not available", '404 request not found');
+
+if (ref $requestver ne 'HASH') {
+       $header{Location} = "/sc/$requestver";
+       Abort("Canonical URL for $Request is at $requestver", '302 subpage alias');
 }
-my $datafile = "sc-units-$scver{id}.inc.pl";
+
+my %scver = %{$requestver};
+my $datafile = "sc-units-$Request";
 
 Html({
        title => "$scver{title} unit cheat sheet",
-       version => '1.1',
+       version => '1.4',
        description => [
                "Reference of $scver{game} unit properties,"
                . " comparing various statistics of all the units in $scver{name}"
@@ -31,26 +47,27 @@ Html({
        keywords => [
                qw'
                starcraft game unit statistics stats comparison table sheet cheat
-               reference software attributes properties
+               reference software attributes properties patch attribute multiplayer
                ',
-               $scver{major} < 2 ? qw' bw broodwar brood war ' : qw' starcraft2 hots ',
+               $scver{major} < 2 ? qw' bw broodwar brood war ' :
+               qw' starcraft2 lotv hots wol ',
        ],
-       stylesheet => [qw'light'],
-       raw => '<link rel="stylesheet" type="text/css" media="all" href="/sc.css?1.1" title="light">',
-       data => [$datafile],
+       stylesheet => [qw( light dark )],
+       raw => '<link rel="stylesheet" type="text/css" media="all" href="/sc.css?1.2">',
+       data => ["$datafile.inc.pl"],
 });
 
 say "<h1>$scver{game} units</h1>\n";
 
-my $units = do $datafile;
-die "Cannot open unit data: $_\n" for $@ || $! || ();
+my $units = Data($datafile);
 my $patch = shift @{$units}
-       or die "Cannot open unit data: metadata not found\n";
+       or Abort("Cannot open unit data: metadata not found", 501);
 
 say "<p>Unit properties as seen or measured in $scver{name}\n$patch.";
-say "Also see the $_ table." for join(', ',
-       (showlink('StarCraft 2: HotS', '/sc/2'))    x ($scver{major} < 2),
-       (showlink('original SC: Brood War', '/sc')) x ($scver{major} > 1),
+say "Also see the $_ tables." for join(' and ',
+       (showlink('StarCraft 2: LotV', '/sc/lotv'))    x ($Request ne 'lotv'),
+       (showlink(             'HotS', '/sc/hots'))    x ($Request ne 'hots'),
+       (showlink('original SC: Brood War', '/sc/bw')) x ($Request ne 'bw'),
 );
 say "</p>\n";
 
@@ -91,21 +108,21 @@ sub coltoggle {
 }
 :><table class="units">
 <thead><tr>
-       <th></th>
-       <th><:= coltoggle('name', '') :></th>
-       <th class="val min" title=minerals>cost</th>
-       <th class="val gas">gas</th>
-       <th class="val time"><:= coltoggle(qw'build cost') :></th>
+       <th><:= coltoggle(exists $get{order} ? 'race' : 'source' => '') :></th>
+       <th class="unit-name"><:= coltoggle(name => 'name') :></th>
+       <th class="val unit-min" title=minerals><:= coltoggle(cost => 'cost') :></th>
+       <th class="val unit-gas">gas</th>
+       <th class="val time"><:= coltoggle(build => 'build') :></th>
        <th class="unit" colspan="2"><:= coltoggle(qw'size size') :></th>
-       <th class="unit" colspan="2">attr</th>
-       <th class="val unit-hp">HP</th>
+       <th class="unit unit-attr" colspan="2">attr</th>
+       <th class="val unit-hp"><:= coltoggle(HP => 'hp') :></th>
        <th class="val unit-shield">shield</th>
        <th class="val unit-armor" title="armor">⛨</th>
-       <th class="val hurt">attack</th>
-       <th class="hurt hurtrel"><:= coltoggle(qw'dps attack 1') :></th>
+       <th class="val hurt"><:= coltoggle(attack => 'attack') :></th>
+       <th class="hurt hurtrel">dps</th>
        <th class="val unit-range" colspan=3>range</th>
        <th class="val unit-sight">sight</th>
-       <th class="val unit-speed">speed</th>
+       <th class="val unit-speed"><:= coltoggle(speed => 'speed') :></th>
        <th class="unit-magic">specials</th>
 </tr></thead>
 <:
@@ -161,24 +178,26 @@ sub showrangeint {
                        if $attack->{type} eq 'implosive';
        if (my @bonus = sort grep { !/^-/ } keys %{ $attack->{bonus} }) {
                $out .= sprintf('<span class="%s" title="%s">&ge;</span>',
-                       (map {
+                       (
                                $_ eq 'light' ? 'unit-s' :
                                $_ eq 'armored' ? 'unit-l' :
                                $_ eq 'organic' ? 'unit-o' :
                                $_ eq 'massive' ? 'unit-h' :
                                $_ eq 'shields' ? 'unit-shield' :
+                               $_ eq 'structure' ? 'unit-x' :
                                '',
-                       } join '_', @bonus),
-                       join(', ', map {(
+                       ),
+                       (
                                sprintf('+%s vs %s',
                                        showrangeint(
                                                $attack->{bonus}->{$_},
-                                               $attack->{bonus}->{$_} + $attack->{bonus}->{"-$_"} * 3,
+                                               ($upattack->{bonus} // $attack->{bonus})->{$_}
+                                                       + ($upattack->{bonus} // $attack->{bonus})->{"-$_"} * 3,
                                        ),
                                        $_,
                                ),
-                       )} @bonus),
-               );
+                       ),
+               ) for @bonus;
        }
                $out .= '<span class="unit-pdd" title="projectile">•</span>'
                        if $attack->{type} eq 'projectile';
@@ -232,9 +251,12 @@ sub showrangeint {
                my $specials = $row->{special} or return '';
                return join ' ', map {
                        sprintf '<span%s title="%s">%s</span>',
-                               $_->{duration} < 0 && ' class="magic-perma"',
                                join('',
-                                       $_->{name},
+                                       $_->{duration} < 0 && ' class="magic-perma"',
+                                       $_->{detect} && ' class="unit-detect"',
+                               ),
+                               join('',
+                                       $_->{name} // $_->{alt},
                                        $_->{desc} ? ": $_->{desc}" : '',
                                        (map { $_ && " ($_)" } join ', ',
                                                #TODO: apply upgrades
@@ -253,16 +275,24 @@ sub showrangeint {
                $_->{hp} += $_->{shield} if $_->{shield};
 
                return (
-                       '<td class="val min">' . ($_->{min} // ''),
-                       '<td class="val gas">' . ($_->{gas} || ''),
-                       !defined $_->{build} ? '<td>' : sprintf('<td class="val time">%s%.0f',
-                               !!$_->{base} && '<span class="unit-composed">+</span>',
-                               $_->{build} || '0',
+                       '<td class="val unit-min">' . ($_->{min} // ''),
+                       '<td class="val unit-gas">' . ($_->{gas} || ''),
+                       defined $_->{transform} ? sprintf('<td class="val time">%.0f',
+                               $_->{transform},
+                       ) :
+                       !defined $_->{build} ? '<td>' : sprintf('<td class="val time"%s>%s%.0f',
+                               defined $_->{warp} && sprintf(' title="%.0f without warpgate"', $_->{build}),
+                               !!$_->{base} && sprintf(
+                                       '<span class="unit-composed" title="%s">+</span>',
+                                       'from '.join('+', @{ $_->{base} }),
+                               ),
+                               $_->{warp} // $_->{build} || '0',
                        ),
                        sprintf('<td class="unit unit-%s" title="%4$s%3$s">%s',
                                $_->            {cargo} < 0 ? ('supply',           T => 'transport') :
                                $_->{upgraded}->{cargo} < 0 ? ('supply magic-opt', T => 'optional transport') :
-                               $_->{attr}->{flying}    ? ('air', F => 'flying') :
+                               $_->            {attr}->{flying} ? ('air',           F => 'flying') :
+                               $_->{upgraded}->{attr}->{flying} ? ('air magic-opt', F => 'potentially flying') :
                                $_->{attr}->{structure} ? ('x',   B => 'building') :
                                (
                                        [qw( x s m l l h h h h )]->[ $_->{cargo} ],
@@ -271,7 +301,7 @@ sub showrangeint {
                                ),
                                defined $_->{size} && sprintf('⌀%.1f ', $_->{size}),
                        ),
-                       sprintf('<td class="val unit%s">%s',
+                       sprintf('<td class="val unit unit-pop%s">%s',
                                defined $_->{pop} && $_->{pop} < 0 && ' unit-supply',
                                defined $_->{pop} && $_->{pop} == .5 ? '½' : $_->{pop},
                        ),
@@ -300,7 +330,8 @@ sub showrangeint {
                                $_->{attr}->{massive}
                                        && '<span class="unit-massive" title="massive">⚓</span>',
                        ),
-                       '<td class="val unit-hp">' . $_->{hp} // '',
+                       $_->{hp} < 0 ? '<td class="val unit-hp" title="invulnerable">∞' :
+                       '<td class="val unit-hp">' . showrangeint($_->{hp}, $_->{upgraded}->{hp}),
                        $_->{shield} ? sprintf('<td class="val unit-shield">%.0f%%<td',
                                100 * $_->{shield} / $_->{hp}
                        ) : '<td colspan=2',
@@ -328,48 +359,69 @@ sub showrangeint {
                        $_->{attr}->{jump}
                                && qq'<span class="unit unit-jump" title="$_->{attr}->{jump}">↕</span>',
                        '<td class="unit-magic">' . showmagic($_),
-                       !$_->{attack}->[1] ? () : (
-                               '<tr><td colspan=12>', showattack($_, 1), '<td colspan=3>'
-                       ),
-                       !$_->{attack}->[2] ? () : (
-                               '<tr><td colspan=12>', showattack($_, 2), '<td colspan=3>'
-                       ),
+                       (map {(
+                               '<tr class="sub"><th class="cat"><td><td colspan=10>',
+                               showattack($row, $_),
+                               '<td colspan=3>',
+                       )} 1 .. $#{ $_->{attack} }),
                        "\n"
                );
        }
 
+       my @rows = @{$units};
        my $grouped = 1;  # race headers
        if (exists $get{order}) {
                $grouped = 0;
                $get{order} ||= '';
-               if ($get{order} eq 'size') {
+               if ($get{order} eq 'name') {
+                       @rows = sort {$a->{name} cmp $b->{name}} @rows;
+               }
+               elsif ($get{order} eq 'cost') {
+                       $_->{order} = (
+                               $_->{gas}*1.5 + $_->{min} + $_->{pop}/8 + $_->{build}/256/8
+                       ) for @rows;
+               }
+               elsif ($get{order} eq 'build') {
+                       my %unittime = map { ($_->{name} => $_->{warp} // $_->{build}) } @rows;
+                       $unittime{Templar} = $unittime{'High Templar'};
+                       $_->{order} = (
+                               ($_->{warp} // $_->{build})
+                               + ($_->{gas}*1.5 + $_->{min} + $_->{pop}/8)/1024
+                               + ($_->{base} ? ($unittime{$_->{base}->[0]} // 100) + 1 : 0)
+                       ) for @rows;
+               }
+               elsif ($get{order} eq 'size') {
                        $_->{order} = (
                                $_->{pop}*16 + ($_->{size} // $_->{suit}) + $_->{cargo}/8
                                + $_->{hp}/512 + $_->{min}/8192
-                       ) for @$units;
+                       ) for @rows;
                }
-               elsif ($get{order} eq 'cost') {
+               elsif ($get{order} eq 'hp') {
                        $_->{order} = (
-                               $_->{gas}*1.5 + $_->{min} + $_->{pop}/8 + $_->{build}/256/8
-                       ) for @$units;
+                               $_->{hp}*1.01 + $_->{armor} + $_->{shield} + $_->{size}/1024,
+                       ) for @rows;
                }
                elsif ($get{order} eq 'attack') {
-                       $_->{order} = $_->{hp} / 1024 + $_->{shield} / 1008 + max(
+                       $_->{order} = $_->{hp} / 16384 + max(
                                map {
-                                       ($_->{damage} + $_->{upgrade} * 3)
-                                       * ($_->{count} // 1) / ($_->{cooldown} // 1)
+                                       ($_->{dps} ? $_->{dps}->[-1] :
+                                               ($_->{damage} + $_->{upgrade} * 3)
+                                               * ($_->{count} // 1) / ($_->{cooldown} // 1)
+                                       )
                                        * ($_->{splash} ? 1.01 : 1)
                                        * ($_->{type} eq 'implosive' ? .96 : 1)
                                        * ($_->{type} eq 'explosive' ? .98 : 1)
                                } @{ $_->{attack} }
-                       ) for @$units;
+                       ) for @rows;
                }
-               else {
-                       $units->[$_]->{order} = $_ for 0 .. $#$units;
+               elsif ($get{order} eq 'speed') {
+                       $_->{order} = (
+                               ($_->{upgraded}->{speed} // $_->{speed}*1.01)
+                               + $_->{sight}/1024 + $_->{detect}/2048
+                       ) for @rows;
                }
+               @rows = sort {$a->{order} <=> $b->{order}} @rows if exists $rows[0]->{order};
        }
-       my @rows = @{$units};
-       @rows = sort {$a->{order} <=> $b->{order}} @rows unless $grouped;
 
        my ($race, $cat) = ('', '');
        for (@rows) {
@@ -405,7 +457,8 @@ sub showrangeint {
 
 <dl>
 <dt>cost
-       <dd>minerals and gas required to create one unit
+       <dd><span class="unit-min">minerals</span> and
+               <span class="unit-gas">gas</span> required to create one unit
        <dd>includes total expenses if based on existing units
 <dt>build
        <dd>relative time needed to create at least one unit
index 6f88b0a4ae34bb066401a2f581ac7e2ee2f1910c..1223a5abdd08dc8fed510bdb26227aa65b1e41cf 100644 (file)
@@ -1,30 +1,3 @@
-<(common.inc.plp)><:
-
-Html({
-       title => 'screen cheat sheet',
-       version => '1.1',
-       description => [
-               "Interactive cheat sheet for the Screen terminal manager,",
-               "describing the function of each key.",
-       ],
-       keywords => [qw'
-               screen sheet cheat reference overview commands keyboard
-               terminal window manager
-       '],
-       stylesheet => [qw( light dark circus mono red )],
-       keys => 1,
-});
-
-:>
-<h1>Screen cheat sheet</h1>
-
-<h2>normal mode (default)</h2>
-
 <:
-use Shiar_Sheet::Keyboard 2;
-my $info = do 'screen.eng.inc.pl' or die $@ // $!;
-my $keys = Shiar_Sheet::Keyboard->new($info);
-$keys->map($get{map}) or undef $get{map};
-$keys->print_rows($get{rows});
-$keys->print_legends(\%get);
-
+$Request = 'screen';
+Include 'keyboard.plp';
index 512f07b8482cf1bf293e16c09d385a6178fb014a..6498e32e06c196aa0a6669ed1ae573c33933058f 100644 (file)
@@ -106,7 +106,7 @@ if (document.querySelector !== undefined) {
        else {
                // title text (case-insensitive unless caps in input)
                var match = function(row) {
-                       return row.cells[1].textContent.match(query, /[A-Z]/.test(query) ? '' : 'i');
+                       return row.cells[1].textContent.match(new RegExp(query, /[A-Z]/.test(query) ? '' : 'i'));
                };
        }
        filterrows(table, match, action || 'filter');
diff --git a/shell.inc.pl b/shell.inc.pl
new file mode 100644 (file)
index 0000000..933ee5d
--- /dev/null
@@ -0,0 +1,695 @@
+use utf8;
+use strict;
+
++{
+
+agents => {
+       sh => {
+               name => "Bourne shell",
+               os => 'v7',
+       },
+       bash => {
+               name => "GNU Bourne-Again SHell",
+               os => 'linux',
+       },
+       csh => {
+               name => "C Shell",
+       },
+       tcsh => {
+               name => "Tenex C Shell",
+               os => 'freebsd',
+       },
+       ksh => {
+               name => "AT&T KornShell",
+       },
+       es => {
+               name => "Extensible Shell",
+       },
+       rc => {
+               name => "Run Commands",
+               os => 'plan9',
+       },
+       zsh => {
+               name => "Z shell",
+       },
+},
+
+feature => [
+
+       {
+               title   => "Job control",
+               description => "",
+               links => [
+                       {
+                               title =>
+                               url => '',
+                       },
+               ],
+               support => {
+                       bash => {},
+                       csh => {},
+                       es => undef,
+                       ksh => {},
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {},
+                       es => undef,
+                       ksh => {},
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Aliases',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {},
+                       ksh => {},
+                       rc => {},
+                       sh => {
+                               since => 0,
+                       },
+                       tcsh => undef,
+                       zsh => {},
+               },
+               title => 'Shell functions',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {},
+                       ksh => {},
+                       rc => {},
+                       sh => {},
+                       tcsh => undef,
+                       zsh => {},
+               },
+               title => '"Sensible" Input/Output redirection',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {},
+                       es => {
+                               alt => 'function',
+                       },
+                       ksh => {},
+                       rc => {
+                               alt => 'function',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Directory stack',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {},
+                       es => {
+                               optional => 'requires readline',
+                       },
+                       ksh => {},
+                       rc => {
+                               optional => 'requires readline',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Command history',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {
+                               optional => 'requires readline',
+                       },
+                       ksh => {},
+                       rc => {
+                               optional => 'requires readline',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Command line editing',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {
+                               optional => 'requires readline',
+                       },
+                       ksh => {},
+                       rc => {
+                               optional => 'requires readline',
+                       },
+                       sh => undef,
+                       tcsh => {
+                               note => 'emulation is thought by many to be incomplete',
+                               partial => 1,
+                       },
+                       zsh => {},
+               },
+               title => 'Vi Command line editing',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {
+                               optional => 'requires readline',
+                       },
+                       ksh => {},
+                       rc => {
+                               optional => 'requires readline',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Emacs Command line editing',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {
+                               optional => 'requires readline',
+                       },
+                       ksh => undef,
+                       rc => {
+                               optional => 'requires readline',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Rebindable Command line editing',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {},
+                       es => {
+                               optional => 'requires readline',
+                       },
+                       ksh => {},
+                       rc => {
+                               optional => 'requires readline',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'User name look up',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => undef,
+                       es => {
+                               alt => 'function',
+                       },
+                       ksh => undef,
+                       rc => {
+                               alt => 'function',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Login/Logout watching',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {
+                               since => 0,
+                       },
+                       es => {
+                               optional => 'requires readline',
+                       },
+                       ksh => {},
+                       rc => {
+                               optional => 'requires readline',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Filename completion',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {
+                               since => undef,
+                       },
+                       es => {
+                               optional => 'requires readline',
+                       },
+                       ksh => {},
+                       rc => {
+                               optional => 'requires readline',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Username completion',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {
+                               since => undef,
+                       },
+                       es => {
+                               optional => 'requires readline',
+                       },
+                       ksh => {},
+                       rc => {
+                               optional => 'requires readline',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Hostname completion',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {
+                               optional => 'requires readline',
+                       },
+                       ksh => undef,
+                       rc => {
+                               optional => 'requires readline',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'History completion',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => undef,
+                       es => undef,
+                       ksh => undef,
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Fully programmable Completion',
+       },
+       {
+               support => {
+                       bash => {
+                               alt => 'patch',
+                               note => 'unofficial patches exist to perform this',
+                       },
+                       csh => undef,
+                       es => undef,
+                       ksh => undef,
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {
+                               alt => 'function',
+                               note => 'This can be done via the shells programmable completion mechanism.',
+                       },
+                       zsh => {
+                               alt => 'function',
+                               note => 'This can be done via the shells programmable completion mechanism.',
+                       },
+               },
+               title => 'Mh Mailbox completion',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => undef,
+                       es => undef,
+                       ksh => {},
+                       rc => undef,
+                       sh => undef,
+                       tcsh => undef,
+                       zsh => {},
+               },
+               title => 'Co Processes',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {},
+                       es => undef,
+                       ksh => {},
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Builtin artithmetic evaluation',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => undef,
+                       ksh => {},
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Can follow symbolic links invisibly',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => undef,
+                       es => undef,
+                       ksh => undef,
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Periodic command execution',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {},
+                       ksh => {},
+                       rc => {},
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Custom Prompt (easily)',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => undef,
+                       es => undef,
+                       ksh => undef,
+                       rc => undef,
+                       sh => undef,
+                       tcsh => undef,
+                       zsh => {},
+               },
+               title => 'Sun Keyboard Hack',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => undef,
+                       es => undef,
+                       ksh => undef,
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Spelling Correction',
+       },
+       {
+               support => {
+                       bash => {
+                               since => undef,
+                       },
+                       csh => undef,
+                       es => {},
+                       ksh => undef,
+                       rc => {},
+                       sh => undef,
+                       tcsh => undef,
+                       zsh => {},
+               },
+               title => 'Process Substitution',
+       },
+       {
+               support => {
+                       bash => 'sh',
+                       csh => 'csh',
+                       es => 'rc',
+                       ksh => 'sh',
+                       rc => 'rc',
+                       sh => 'sh',
+                       tcsh => 'csh',
+                       zsh => 'sh',
+               },
+               title => 'Underlying Syntax',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {},
+                       ksh => {
+                               alt => 'patch',
+                               note => "A version called 'pdksh' is freely available, but does not have the full functionality of the AT&T version.",
+                       },
+                       rc => {},
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Freely Available',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {},
+                       es => {
+                               alt => 'function',
+                       },
+                       ksh => {},
+                       rc => {
+                               alt => 'function',
+                       },
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Checks Mailbox',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => undef,
+                       es => undef,
+                       ksh => undef,
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Tty Sanity Checking',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {},
+                       ksh => {},
+                       rc => {},
+                       sh => {},
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Can cope with large argument lists',
+       },
+       {
+               support => {
+                       bash => {
+                               alt => 'config',
+                               note => 'Only by specifying a file via the ENV environment variable.',
+                       },
+                       csh => {},
+                       es => undef,
+                       ksh => {
+                               alt => 'config',
+                               note => 'Only by specifying a file via the ENV environment variable.',
+                       },
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Has non-interactive startup file',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {},
+                       es => undef,
+                       ksh => {
+                               alt => 'config',
+                               note => 'Only by specifying a file via the ENV environment variable.',
+                       },
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'Has non-login startup file',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {},
+                       es => {},
+                       ksh => undef,
+                       rc => {},
+                       sh => undef,
+                       tcsh => undef,
+                       zsh => {},
+               },
+               title => 'Can avoid user startup files',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => undef,
+                       ksh => {},
+                       rc => undef,
+                       sh => undef,
+                       tcsh => undef,
+                       zsh => undef,
+               },
+               title => 'Can specify startup file',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => undef,
+                       es => {},
+                       ksh => undef,
+                       rc => undef,
+                       sh => undef,
+                       tcsh => undef,
+                       zsh => undef,
+               },
+               title => 'Low level command redefinition',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => undef,
+                       es => {},
+                       ksh => undef,
+                       rc => {},
+                       sh => undef,
+                       tcsh => undef,
+                       zsh => undef,
+               },
+               title => 'Has anonymous functions',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => {},
+                       es => {},
+                       ksh => {},
+                       rc => {},
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'List Variables',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {},
+                       ksh => {},
+                       rc => {},
+                       sh => {},
+                       tcsh => undef,
+                       zsh => {},
+               },
+               title => 'Full signal trap handling',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => {},
+                       es => {
+                               alt => 'function',
+                       },
+                       ksh => {},
+                       rc => undef,
+                       sh => undef,
+                       tcsh => {},
+                       zsh => {},
+               },
+               title => 'File no clobber ability',
+       },
+       {
+               support => {
+                       bash => {},
+                       csh => undef,
+                       es => {},
+                       ksh => {},
+                       rc => {},
+                       sh => undef,
+                       tcsh => undef,
+                       zsh => {},
+               },
+               title => 'Local variables',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => undef,
+                       es => {},
+                       ksh => undef,
+                       rc => undef,
+                       sh => undef,
+                       tcsh => undef,
+                       zsh => undef,
+               },
+               title => 'Lexically scoped variables',
+       },
+       {
+               support => {
+                       bash => undef,
+                       csh => undef,
+                       es => {},
+                       ksh => undef,
+                       rc => undef,
+                       sh => undef,
+                       tcsh => undef,
+                       zsh => undef,
+               },
+               title => 'Exceptions',
+       },
+],
+
+}
diff --git a/shell.plp b/shell.plp
new file mode 100644 (file)
index 0000000..75ea57e
--- /dev/null
+++ b/shell.plp
@@ -0,0 +1,87 @@
+<(common.inc.plp)><:
+use List::Util qw(sum max first);
+
+Html({
+       title => 'Shell compatibility cheat sheet',
+       version => '1.0',
+       stylesheet => [qw'circus dark mono red light'],
+       data => ['shell.inc.pl'],
+});
+
+say "<h1>Shell compatibility</h1>\n";
+
+my $data = Data('shell');
+my @agents = keys %{ $data->{agents} };
+
+print '<table class="mapped">';
+print '<col>';  # should match first thead row
+printf '<colgroup span="%d">', 1 for @agents;
+say '</colgroup><col>';
+
+my $header = join('',
+       '<tr>',
+       '<th>feature',
+       (map {
+               sprintf('<th%s>%s',
+                       (map {
+                               sprintf ' title="%s"', Entity($_)
+                       } $data->{agents}->{$_}->{name} // ()),
+                       $_
+               )
+       } @agents),
+);
+print '<thead>', $header;
+say '</thead>';
+say '<tfoot>', $header;
+
+sub saytitlecol {
+       my ($row) = @_;
+       print '<td>', Entity($row->{title});
+}
+
+my %TSTATS = (
+       l5 => '✔',
+       l4 => '✔',
+       l3 => '±',
+       l2 => '*',
+       l1 => '✘',
+);
+
+sub saysupportcols {
+       my ($row, $agent) = @_;
+       my $support = $row->{support}->{$agent};
+       my $level = (
+               !defined $support ? 'l1' :
+               ref $support ne 'HASH' ? 'l0' :
+               $support->{alt} ? 'l2' :
+               $support->{partial} ? 'l3' :
+               $support->{optional} ? 'l4' :
+               'l5'
+       );
+       my $title = join(' ',
+               defined $support ? 'supported' : 'unsupported',
+               'in', $data->{agents}->{$agent}->{abbr} // $agent,
+       );
+       $title .= " ($_)"
+               for ref $support && ($support->{note} // $support->{optional}) || ();
+       my $header = defined $support ? '✔' : '✘';
+       printf('<td class="%s" title="%s">%s',
+               join(' ', X => $level),
+               $title,
+               $TSTATS{$level} // (ref $support ? '?' : $support),
+       );
+}
+
+say '<tbody>';
+for my $row (sort {
+       $a->{title} cmp $b->{title}
+} @{ $data->{feature} }) {
+       (my $id = lc $row->{title}) =~ s/\W+/-/g;
+       printf '<tr id="%s">', $id;
+       saytitlecol($row);
+       saysupportcols($row, $_) for @agents;
+       say '</tr>';
+}
+say '</tbody>';
+say '</table>';
+
index ef730a49cccf4a54229ead63436d141b919c66ea..cf22e34c75d64c3e4fa341900741fba5a4b30534 100644 (file)
@@ -10,9 +10,10 @@ if ($source =~ s{(?<=\Q.inc.pl\E)/jsonp?$}{} and -r $source) {
                my $data = do $source or die $@ || $! || 'read error';
                require JSON;
                my $converter = JSON->new;
-               $converter->utf8->indent->space_after->canonical;
+               $converter->indent->space_after->canonical;
 
                $header{content_type} = 'application/json';
+               $header{'Access-Control-Allow-Origin'} = '*';
                $header{content_type} = 'text/plain' if exists $get{debug};
                print $_, '(' for $get{callback} // ();
                print $converter->encode($data);
@@ -28,7 +29,7 @@ if ($source =~ s{(?<=\Q.inc.pl\E)/jsonp?$}{} and -r $source) {
 
 Html({
        title => "$source source code",
-       version => '1.1',
+       version => '1.3',
        description => !$source ? 'Index of source files for this site.' : [
                "Source code of the $source file at this site,",
                "with syntax highlighted and references linked."
@@ -42,34 +43,53 @@ Html({
 
 say '';
 
-if (not $source) {
-       print "<h1>Source files</h1>";
+if (not $source or -d $source) {
+       PLP_START {
+               print "<h1>Source files</h1>";
+       };
+
+       if ($source and $source ne 'tools') {
+               Abort("Directory index not permitted", '403 source not allowed');
+       }
 
        print "<p>Project code distributed under the AGPL. Please contribute back.</p>";
        say '<ul>';
-       for (glob '*.plp') {
-               chomp;
+       for (glob($source ? "$source/*" : '*.plp')) {
                say '<li>', showlink($_, "/source/$_");
        }
        say "</ul>\n";
 }
 else {
        my $href = showlink($source, $source =~ m{\A (\w+) \.plp \z}x && "/$1");
-       say "<h1>Source of $href</h1>";
+       PLP_START {
+               say "<h1>Source of $href</h1>";
+       };
 
+       my $path = $source;
        if ($source =~ m{(?:/|^)\.}) {
-               die "File request not permitted\n";
+               Abort("File request not permitted", '403 source not allowed');
        }
        elsif ($source =~ s{::}{/}g or !-e $source) {
                $source .= '.pm';
                for (0 .. $#INC) {
                        -e ($_ = "$INC[$_]/$source") or next;
-                       $source = $_;
+                       $path = $_;
                        last;
                }
        }
-       -r $source or die "Requested file not found\n";
-       my $size = (stat $source)->[7];
+       -r $path or Abort("Requested file not found", '404 source not found');
+       my $size = (stat $path)->[7];
+
+       my $cachefile = "source/$source.html";
+       if (-e $cachefile and (stat $cachefile)->[9] >= (stat $path)->[9]) {
+               say '<pre>';
+               print decode_utf8(ReadFile($cachefile));
+               say '</pre>';
+               exit;
+       }
+       -e or mkdir for $cachefile =~ s{[^/]+\z}{}r; # dirname
+       open my $cache, '>', $cachefile
+               or Alert("Could not save cache", "Opening $cachefile failed: $!");;
 
        if (my $hl = eval {
                $size < 32_768 or die 'large files take too long to parse';
@@ -78,14 +98,18 @@ else {
                        or die 'early versions are buggy under FastCGI';
                delete $Text::VimColor::SYNTAX_TYPE{Underlined};
                return Text::VimColor->new(
-                       file => $source,
-                       vim_options => [@Text::VimColor::VIM_OPTIONS, '+:set enc=utf-8'],
+                       file => $path,
+                       vim_options => [@Text::VimColor::VIM_OPTIONS,
+                               '+:set enc=utf-8',
+                               '+:let perl_sub_signatures=1',
+                       ],
                )->marked;
        }) {
                my %TYPETAG = (
                        Statement => 'strong',
                        Error     => 'em',
                        Todo      => 'em',
+                       PreProc   => 'strong',
                );
 
                say '<pre>';
@@ -93,30 +117,30 @@ else {
                        my ($type, $contents) = @{$_};
                        $contents = decode_utf8($contents);
                        my $tag = $type && ($TYPETAG{$type} || 'span');
-                       my $arg = '';
-                       print "<$tag$arg class=\"sy-\l$type\">" if $tag;
-                       if (!$type || $type eq 'Constant'
-                       and $contents =~ s{^(['"]?)($incname)(?=\1$)}{}) {
-                               # link other page sources, stylesheets, and javascript
-                               print $1 . showlink($2, "/source/$2");
-                       }
-                       if (!$type and $contents =~ s/^(\s*)([A-Z]\w+(?:::\w+)+)(?![^;\s])//) {
-                               # link perl module names (Xx::Xx...)
-                               print $1 . showlink($2, "/source/$2");
-                       }
-                       if ($type && $type eq 'Comment'
-                       and $contents =~ s{^(.*? by )(tools/\S+)}{}) {
-                               # link generator scripts (by tools/...)
-                               print $1 . showlink($2, "/source/$2");
-                       }
-                       print Text::VimColor::_xml_escape($contents);
-                       print "</$tag>" if $tag;
+                       my $line = Text::VimColor::_xml_escape($contents);
+
+                       # link other page sources, stylesheets, and javascript
+                       $line =~ s{ ^(['"]?) \K ($incname) (?=\1$) }{ showlink($2, "/source/$2") }xe
+                               if !$type || $type eq 'Constant';
+                       # link relative page locations in html output
+                       $line =~ s{ ^(&quot;)\K (/\w+) (?= (?:/\w+)* \1$) }{ showlink($2, "/source$2.plp") }xe
+                               if $type && $type eq 'Constant';
+                       # link perl module names (Xx::Xx...)
+                       $line =~ s{ ^\s* \K ([A-Z]\w+(?:::\w+)+) (?![^;\s]) }{ showlink($1, "/source/$1") }xe
+                               if !$type;
+                       # link generator scripts (by tools/...)
+                       $line =~ s{ ^.*? by\  \K (tools/\S+) }{ showlink($1, "/source/$1") }xe
+                               if $type && $type eq 'Comment';
+
+                       $line = qq(<$tag class="sy-\l$type">$line</$tag>) if $tag;
+                       print $line;
+                       print {$cache} $line if $cache;
                }
                say '</pre>';
        }
        else {
                say '<pre>';
-               print EscapeHTML(decode_utf8(ReadFile($source)));
+               print EscapeHTML(decode_utf8(ReadFile($path)));
                say '</pre>';
        }
 
index 88c12453ae8fe5939ad669bdfe8fa1eb3eceac42..e8234c66d7eca9092458a050248e93931ff5eae7 100644 (file)
@@ -3,10 +3,14 @@ use utf8;
 use Shiar_Sheet::Colour 1.05;
 
 +{
-       default => [qw( ansi ansi88 ansi256 )],
-       more    => [qw( ansi mirc pastel slutty solarized ansi88 ansi256 )],
-       ansi    => [qw( cga putty xterm tango app html cgarne )],
-       legacy  => [qw( c64 msx mac2 risc arnegame db16 cpc cpcboy shiar32 ppu 2c03 shovel 64 )],
+       default => [qw( vte ansi256 )],
+       more    => [qw( vte zxarne mirc pastel slutty solarized rplace2022 ansi88 ansi256 )],
+       retro   => [qw( pico8 pyxel sweetie16 db16 systemmini16 endesga arnegame )],
+       vte     => [qw( ansi cga putty xterm ubuntu tango app campbell html cgarne )],
+       nes     => [qw( ppu 2c03 shovel )],
+       legacy  => [qw( c64 msx1 msx2 mac2 risc cpc cpcboy nes 64 ansi88 retro )],
+       '4bit'  => [qw( pico8 sweetie16 arnegame cgarne ansi html zxarne c64 msx2 )],
+       dosbox  => [qw( cga ibm5153 scumm_amiga agi_amiga_v1 agi_amiga_v2 agi_amiga_v3 agi_amigaish colodore_sat50 colodore_sat60 tandy_warm dga16 )],
 
        xcolors => sub {
                my $pals = do 'data/termcol-xcolor.inc.pl' or die $! || $@;
@@ -23,10 +27,11 @@ use Shiar_Sheet::Colour 1.05;
                        808080:gray  FF0000:red     00FF00:lime  FFFF00:yellow
                        0000FF:blue  FF00FF:fuchsia 00FFFF:aqua  FFFFFF:white
                )],
-               parent => 'cga',
+               parent => 'ansi',
        },
-       cga => {
+       ansi => {
                # linux console, kde?
+               title => 'ANSI/CGA/EGA',
                list => [qw(
                        000000 AA0000 00AA00 AA5500 0000AA AA00AA 00AAAA AAAAAA
                        555555 FF5555 55FF55 FFFF55 5555FF FF55FF 55FFFF FFFFFF
@@ -34,13 +39,76 @@ use Shiar_Sheet::Colour 1.05;
                # reset bold dim italic underline blink fastblink reverse hidden
                hueorder => [ 1,3,2,6,4,5, 0,7 , 9,11,10,14,12,13, 8,15 ],
        },
+       cga => {
+               href => 'https://int10h.org/blog/2022/06/ibm-5153-color-true-cga-palette/',
+               title => 'CGA on an IBM 5153 monitor',
+               list => [qw(
+                       000000 0000C4 00C400 00C4C4 C40000 C400C4 C47E00 C4C4C4
+                       4E4E4E 4E4EDC 4EDC4E 4EF3F3 DC4E4E F34EF3 F3F34E FFFFFF
+               )],
+               parent => 'ansi',
+               ansiorder => [ 0,4,2,6,1,5,3,7, 8,12,10,14,9,13,11,15 ],
+       },
+
+       ibm5153 => {
+               list => [qw( 000000 0000A5 00A500 00A5A5 A50000 A500A5 A56900 A5A5A5 4C4C4C 4C4CDE 4CDE4C 4CF2F2 DE4C4C F24CF2 F2F24C FFFFFF)],
+               parent => 'cga',
+       },
+       scumm_amiga => {
+               parent => 'cga',
+               list => [qw( 000000 0000BA 00BA00 00BABA BA0000 BA00BA BA7500 BABABA 757575 7575FF 00FF00 00FFFF FF8989 FF00FF FFFF00 FFFFFF)],
+       },
+       agi_amiga_v1 => {
+               parent => 'cga',
+               list => [qw( 000000 0000FF 008100 00D6BE C20000 BE7DD6 815500 BEBEBE 7D7D7D 00BEFF 00EA00 00FFD6 FF9581 FF7D00 EAEA00 FFFFFF)],
+       },
+       agi_amiga_v2 => {
+               parent => 'cga',
+               list => [qw( 000000 0000FF 008100 00D6BE C20000 BE7DD6 815500 BEBEBE 7D7D7D 00BEFF 00EA00 00FFD6 FF9581 D600FF EAEA00 FFFFFF)],
+       },
+       agi_amiga_v3 => {
+               parent => 'cga',
+               list => [qw( 000000 0000BE 00BE00 00BEBE BE0000 BE00BE C27D00 BEBEBE 7D7D7D 0000FF 00FF00 00FFFF FF0000 FF00FF FFFF00 FFFFFF)],
+       },
+       agi_amigaish => {
+               title => 'Amiga-ish CGA for AGI ports',
+               parent => 'cga',
+               list => [qw( 000000 0000FF 00AA00 00AAAA CE0000 BE71DE 8D5000 BEBEBE 555555 00BEFF 00CE55 55FFFF FF9D8D FF55FF EEEE00 FFFFFF)],
+       },
+       colodore_sat50 => {
+               title => 'DOSBox Colodore',
+               list => [qw( 000000 2C2C99 55AE4C 388985 813038 8D3C99 8D5028 B2B2B2 484848 716DEE AAFFA1 75CECA C66D71 D68DE2 EEF271 FFFFFF)],
+               parent => 'cga',
+       },
+       colodore_sat60 => {
+               title => 'Colodore (C64-ish CGA)',
+               list => [qw( 000000 2C28B2 4CB640 309189 8D2C34 9934A5 994C20 B2B2B2 484848 6D69FF 9DFF91 69D6CE D2656D DE89EA F2F65D FFFFFF)],
+               parent => 'cga',
+       },
+       tandy_warm => {
+               list => [qw( 0C0C0C 000C9D 087514 308D9D AA1800 AE309D B26124 AAAAAA 505050 5969F2 44BE50 40DEFA FF7150 FF85F6 F6E248 F2F2FA)],
+               parent => 'cga',
+       },
+       dga16 => {
+               list => [qw( 000000 001875 108D00 14BAD2 710C08 6D1C9D B25014 BAB2AA 484840 0861C6 99CE00 71F6D6 EA9D00 FF79DA FFF255 FFFFFF )],
+               parent => 'cga',
+       },
+
+       ubuntu => {
+               title => 'setvtrgb defaults for Ubuntu kbd',
+               list => [qw(
+                       010101 DE382B 39B54A FFC706 006FB8 762671 2CB5E9 CCCCCC
+                       808080 FF0000 00FF00 FFFF00 0000FF FF00FF 00FFFF FFFFFF
+               )],
+               parent => 'ansi',
+       },
        xterm => {
                # rxvt except for blues
                list => [qw(
                        000000 CC0000 00CC00 CCCC00 4682B4 CC00CC 00CCCC E5E5E5
                        4C4C4C FF0000 00FF00 FFFF00 1E90FF FF00FF 00FFFF FFFFFF
                )],
-               parent => 'cga',
+               parent => 'ansi',
        },
        tango => {
                # default Gnome theme
@@ -48,7 +116,7 @@ use Shiar_Sheet::Colour 1.05;
                        2E3436 CC0000 4E9A06 C4A000 3465A4 75507B 06989A D3D7CF
                        555753 EF2929 8AE234 FCE94F 729FCF AD7FA8 34E2E2 EEEEEC
                )],
-               parent => 'cga',
+               parent => 'ansi',
        },
        xkcd => {
                title => 'human averages in xkcd survey results',
@@ -59,7 +127,7 @@ use Shiar_Sheet::Colour 1.05;
                        929591:grey  e50000:red     aaff32:lime  ffff14:yellow
                        0343df:blue  ed0dd9:fuchsia 00ffff:cyan  ffffff:white
                )],
-               parent => 'cga',
+               parent => 'ansi',
        },
        android => {
                href => 'http://developer.android.com/guide/practices/ui_guidelines/icon_design.html',
@@ -78,7 +146,7 @@ use Shiar_Sheet::Colour 1.05;
                        000000 990B00 35A600 999900 0000B3 B304B2 31A6B3 BFBFBF
                        666666 E51600 48D901 E5E600 0100FF E607E5 48E6E6 E6E5E6
                )],
-               parent => 'cga',
+               parent => 'ansi',
        },
        iterm => ['putty'], # identical in v2.2.1
        pastel => {
@@ -87,7 +155,16 @@ use Shiar_Sheet::Colour 1.05;
                        4F4F4F FF6C60 A8FF60 FFFFB6 96CBFE FF73FD C6C5FE EEEEEE
                        7C7C7C FFB6B0 CEFFAC FFFFCC B5DCFF FF9CFE DFDFFE FFFFFF
                )],
-               parent => 'cga',
+               parent => 'ansi',
+       },
+       campbell => {
+               name => 'Campbell',
+               title => 'Windows 10 Console', # as of v1709
+               list => [qw(
+                       0C0C0C C50F1F 13A10E C19C00 0037DA 881798 3A96DD CCCCCC
+                       767676 E74856 16C60C F9F1A5 3B78FF B4009E 61D6D6 F2F2F2
+               )],
+               parent => 'ansi',
        },
        putty => {
                name => 'PuTTY',
@@ -95,7 +172,7 @@ use Shiar_Sheet::Colour 1.05;
                        000000 BB0000 00BB00 BBBB00 0000BB BB00BB 00BBBB BBBBBB
                        555555 FF5555 55FF55 FFFF55 5555FF FF55FF 55FFFF FFFFFF
                )],
-               parent => 'cga',
+               parent => 'ansi',
        },
        slutty => {
                name => 'SluTTY',
@@ -105,7 +182,25 @@ use Shiar_Sheet::Colour 1.05;
                        000000 9C1D1D 6C9446 AC9A47 335786 8F6496 486768 E0DCDC
                        2F2F2F CD5757 8FC35B D1C45E 5C81A9 BC95B7 76CBCB EEEEEC
                )],
-               parent => 'cga',
+               parent => 'ansi',
+       },
+       falcon => {
+               name => 'Falcon',  # v2.0
+               href => 'https://github.com/fenetikm/falcon',
+               list => [qw(
+                       000004 FF3600 718E3F FFC552 635196 FF761A 34BFA4 B4B4B9
+                       020221 FF8E78 B1BF75 FFD392 99A4BC FFB07B 85CCBF F8F8FF
+               )],
+               parent => 'ansi',
+       },
+       shiar => {
+               name => 'Shiar TUI',
+               list => [qw(
+                       220000 CC0000 88BB00 CCAA00 770000 CC4822 6899A0 CCCCCC
+                       686868 CC8B7B 8BBB7B C0C070 4499BB DD7700 44BB99 F8F8F8
+                       000000:bg B0B0B0:fg FFFFFF:bd
+               )],
+               parent => 'ansi',
        },
        mirc => {
                name => 'mIRC',
@@ -162,8 +257,19 @@ use Shiar_Sheet::Colour 1.05;
                ansiorder => [ 0,2,5,9,6,4,3,15 , 11,10,13,7,14,8,12,1 ],
                hueorder => [ 2,8,7,5,3,6,4,9 , 10,13,14,0,11,12,15,1 ],
        },
+       jw64 => {
+               name => 'JW-64',
+               url => 'https://lospec.com/palette-list/jw-64',
+               list => [qw(
+                       000000 ffffff a82f2f 63d4f0 b437b4 54c048 403fc0 e0e040
+                       b46429 644020 e0806c 404040 8c8c8c a0f66e 6496f4 c8c8c8
+               )],
+#                      000000 404040 8c8c8c c8c8c8 644020 a82f2f b46429 e0806c
+#                      403fc0 6496f4 63d4f0 e0e040 b437b4 54c048 a0f66e ffffff
+               parent => 'c64',
+       },
 
-       msx => [qw( msx1 msx2 arnejmp )],
+       msx => [qw( msx1 msx2 arnejmp simplejpc )],
        msx1 => {
                name => 'MSX',
                list => [ map {
@@ -198,6 +304,16 @@ use Shiar_Sheet::Colour 1.05;
                )],
                parent => 'msx1',
        },
+       simplejpc => {
+               href => 'http://pixeljoint.com/pixelart/119844.htm',
+               title => 'SimpleJPC-16 by Adigun Polack',
+               name => 'SimpleJPC',
+               parent => 'msx1',
+               list => [qw(
+                       050403 221F31 316F23 7CC264 404A68 678FCB 543516 8BE1E0
+                       A14D3F EA9182 E1B047 F5EE9B 9B6E2D A568D4 9A93B7 FEFEFE
+               )],
+       },
 
        arnegame => {
                href => 'http://androidarts.com/palette/16pal.htm',
@@ -221,11 +337,22 @@ use Shiar_Sheet::Colour 1.05;
                        000000 8A3622 0C7E45 AA5C3D 2234D1 5C2E78 44AACC B5B5B5
                        5E606E EB8A60 6CD947 FFD93F 4C81FB E23D69 7BE2F9 FFFFFF
                )],
-               parent => 'cga',
+               parent => 'ansi',
+       },
+       zxarne => {
+               href => 'http://androidarts.com/Amiga/ZX.htm',
+               title => "version 5.2",
+               name => 'ZXArne',
+               list => [qw(
+                       000000 A73211 629A31 E8BC50 313390 A15589 28A4CB BFBFBD
+                       3C351F D85525 9CD33C F1E782 1559DB CD7A50 65DCD6 F2F1ED
+               )],
+               parent => 'ansi',
+               ansiorder => [ 0,1,2,13,4,5,6,7 , 8,9,10,3,12,11,14,15 ], # Purple is orange
        },
        db16 => {
                href => 'http://pixeljoint.com/forum/forum_posts.asp?TID=12795',
-               title => "DawnBringer's 16 color palette v1.0",
+               title => "DawnBringer's 16 color palette v1.0, old default on TIC-80",
                name => 'DawnBringer16',
                list => [qw(
                        140C1C 442434 30346D 4E4A4E 854C30 346524 D04648 757161
@@ -255,6 +382,67 @@ use Shiar_Sheet::Colour 1.05;
                )],
                ansiorder => [ 1,10,15,8,13,11,14,6 , 0,9,2,3,4,12,5,7 ],
        },
+       pico8 => {
+               name => 'PICO-8',
+               href => 'https://www.lexaloffle.com/pico-8.php', # https://pico-8.fandom.com/wiki/Palette
+               list => [qw(
+                       000000 1D2B53 7E2553 008751 AB5236 5F574F C2C3C7 FFF1E8
+                       FF004D FFA300 FFEC27 00E436 29ADFF 83769C FF77A8 FFCCAA
+               )],
+               ansiorder => [ 0,8,3,4,1,2,13,6 , 5,14,11,10,12,15,12,7 ], # 2x12, 0x9
+       },
+       pyxel => {
+               name => 'Pyxel', # python retro game engine
+               href => 'https://github.com/kitao/pyxel#color-palette',
+               list => [qw(
+                       000000 2B335F 7E2072 19959C 8B4852 395C98 A9C1FF EEEEEE
+                       D4186C D38441 E9C35B 70C6A9 7696DE A3A3A3 FF9798 EDC7B0
+               )],
+               ansiorder => [ 0,8,3,9,1,2,6,13 , 4,14,11,10,12,15,5,7 ], # 2x12, 0x9
+       },
+       sweetie16 => {
+               name => 'SWEETIE-16',
+               title => "TIC-80 default for new cartridges",
+               href => 'https://twitter.com/search?q=%23sweetie16',
+               list => [qw(
+                       1A1C2C 5D275D B13E53 EF7D57 FFCD75 A7F070 38B764 257179
+                       29366F 3B5DC9 41A6F6 73EFF7 F4F4F4 94B0C2 566C86 333C57
+               )],
+               ansiorder => [ 0,2,6,3,9,15,7,13 , 14,8,5,4,10,1,11,12 ],
+       },
+       endesga => {
+               href => 'https://www.patreon.com/ENDESGA',
+               list => [qw(
+                       E4A672 B86F50 743F39 3F2832 9E2835 E53B44 FB922B FFE762
+                       63C64D 327345 193D3F 4F6781 AFBFD2 FFFFFF 2CE8F4 0484D1
+               )],
+               ansiorder => [ 3,4,9,1,15,2,10,12 , 11,5,8,7,14,0,6,13 ],
+       },
+       systemmini16 => {
+               name => 'System Mini 16',
+               href => 'https://lospec.com/palette-list/system-mini-16',
+               list => [qw(
+                       000000 68605C B0B0B8 FCFCFC 1C38AC 7070FC A82814 FC4848
+                       208800 70F828 B82CD0 FC74EC AC581C F8A850 3CD4E4 F8EC20
+               )],
+               ansiorder => [ 0,6,8,12,4,10,13,2 , 1,7,9,15,5,11,14,3 ],
+       },
+       rplace2017 => {
+               name => 'r/place 2017',
+               list => [qw(
+                       FFFFFF E4E4E4 888888 222222 FFA7D1 E50000 E59500 A06A42
+                       E5D900 94E044 02BE01 00D3DD 0083C7 0000EA CF6EE4 820080
+               )],
+               ansiorder => [ 3,5,10,6,13,15,7,1 , 2,4,9,8,12,14,11,0],
+       },
+       rplace2022 => {
+               name => 'r/place 2022 day 1',
+               list => [qw(
+                       FF4500 FFA800 FFD635 00CC78 7EED56 2450A4 3690EA 51E9F4
+                       811E9F B44AC0 FF99AA 9C6926 000000 898D90 D4D7D9 FFFFFF
+               )],
+               ansiorder => [ 12,0,3,1,5,8,11,14 , 13,10,4,2,6,9,7,15 ],
+       },
 
        cpc => {
                name => 'Amstrad CPC',
index c510cd24d1bcdbdcd5bf4e151f8dc6d6d02f2f11..f76e293a31e3dcfbf4173abcfe3efe352f138569 100644 (file)
@@ -1,8 +1,27 @@
 <(common.inc.plp)><:
 
+if (my ($name) = $Request =~ /(.+)\.gpl\z/) {
+       my $palettes = Data('termcol');
+       my $palette = $palettes->{$name}
+               or Abort("Palette '$name' not found", 404);
+       ref $palette ne 'ARRAY'
+               or Abort("Group contains multiple palettes: ".join(', ', @{$palette}));
+
+       $header{content_type} = 'text/x-gimp-gpl';
+       say 'GIMP Palette';
+       say 'Name: ', $palette->{name} // $name;
+       say 'Columns: 8';
+       say '#';
+       for (@{ $palette->{list} }) {
+               my ($rgb, $name) = split /:/, $_, 3;
+               say join ' ', unpack('C*', pack 'H6', $rgb), $name;
+       }
+       exit;
+}
+
 Html({
        title => ($Request ? 'terminal colour' : 'colour palettes') . ' cheat sheet',
-       version => '1.2',
+       version => '1.4',
        description => [!$Request ? "Comparison of various colour palettes." : (
                "Index of all terminal/console colour codes,",
                "with an example result of various environments.",
@@ -16,7 +35,7 @@ Html({
 });
 
 my @draw = map { [$_, s/\W+\z//] } grep { $_ } split m(/),
-       $get{img} // exists $get{img} && 'indi.png';
+       $get{img} // exists $get{img} && 'compile.png';
 
 my @termlist;
 push @termlist, split /\W+/, $Request || 'default';
@@ -44,8 +63,7 @@ use Shiar_Sheet::Colour 1.04;
 use List::Util qw( min max );
 use POSIX qw( ceil );
 
-my $palettes = do 'termcol.inc.pl';
-die "Cannot open palette data: $_\n" for $@ || $! || ();
+my $palettes = Data('termcol');
 
 sub colcell {
        my $name = shift // return "<td>\n";
diff --git a/tools/icomoon-selection.json b/tools/icomoon-selection.json
new file mode 100644 (file)
index 0000000..402da47
--- /dev/null
@@ -0,0 +1 @@
+{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M981.408 670.096c-26.608-61.968-90.608-64.24-177.104-51.856 0.656-144.448 24.992-282.4 71.232-433.152 21.504-53.984-138.72-152.016-363.52-153.072-224.8 1.056-385.040 99.072-363.536 153.056 45.968 149.84 70.144 288.752 71.008 433.12-86.384-12.336-150.304-10.032-176.912 51.888-57.952 115.152 125.216 321.92 469.424 321.92 344.224 0 527.376-206.768 469.408-321.904zM199.712 765.488s21.712-75.92 18.24-116.064c188.864 169.984 399.264 169.984 588.112 0 0.72 40.96 18.224 116.064 18.224 116.064-185.92 176.912-438.64 176.912-624.576 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["tophat"]},"attrs":[{}],"properties":{"order":14,"id":0,"name":"tophat","prevSize":32,"code":127913},"setIdx":0,"setId":3,"iconIdx":0},{"icon":{"paths":["M1024 576v-64h-193.29c-5.862-72.686-31.786-139.026-71.67-192.25h161.944l70.060-280.24-62.090-15.522-57.94 231.76h-174.68c-0.892-0.694-1.796-1.374-2.698-2.056 6.71-19.502 10.362-40.422 10.362-62.194 0.002-105.76-85.958-191.498-191.998-191.498s-192 85.738-192 191.5c0 21.772 3.65 42.692 10.362 62.194-0.9 0.684-1.804 1.362-2.698 2.056h-174.68l-57.94-231.76-62.090 15.522 70.060 280.24h161.944c-39.884 53.222-65.806 119.562-71.668 192.248h-193.29v64h193.37c3.802 45.664 15.508 88.812 33.638 127.75h-123.992l-70.060 280.238 62.090 15.524 57.94-231.762h112.354c58.692 78.032 147.396 127.75 246.66 127.75s187.966-49.718 246.662-127.75h112.354l57.94 231.762 62.090-15.524-70.060-280.238h-123.992c18.13-38.938 29.836-82.086 33.636-127.75h193.37z"],"tags":["bug","virus","error"],"defaultCode":59801,"grid":16,"attrs":[]},"attrs":[],"properties":{"ligatures":"bug, virus","name":"bug","order":2,"id":154,"prevSize":32,"code":128027},"setIdx":2,"setId":1,"iconIdx":153},{"icon":{"paths":["M567.656 736.916c-81.944 38.118-158.158 37.716-209.34 34.020-61.052-4.41-110.158-21.124-131.742-35.732-13.3-9.006-31.384-5.522-40.39 7.782-9.004 13.302-5.52 31.386 7.782 40.39 34.698 23.486 96.068 40.954 160.162 45.58 10.866 0.784 22.798 1.278 35.646 1.278 55.782 0 126.626-5.316 202.42-40.57 14.564-6.778 20.878-24.074 14.104-38.64-6.776-14.566-24.076-20.872-38.642-14.108zM890.948 693.816c2.786-252.688 28.762-730.206-454.97-691.612-477.6 38.442-350.964 542.968-358.082 711.95-6.308 89.386-35.978 198.648-77.896 309.846h129.1c13.266-47.122 23.024-93.72 27.232-138.15 7.782 5.428 16.108 10.674 24.994 15.7 14.458 8.518 26.884 19.844 40.040 31.834 30.744 28.018 65.59 59.774 133.712 63.752 4.572 0.262 9.174 0.394 13.676 0.394 68.896 0 116.014-30.154 153.878-54.382 18.14-11.612 33.818-21.64 48.564-26.452 41.91-13.12 78.532-34.296 105.904-61.252 4.276-4.208 8.242-8.538 11.962-12.948 15.246 55.878 36.118 118.758 59.288 181.504h275.65c-66.174-102.224-134.436-202.374-133.052-330.184zM124.11 556.352c0-0.016 0-0.030-0.002-0.046-4.746-82.462 34.71-151.832 88.126-154.936 53.412-3.106 100.56 61.228 105.304 143.692 0 0.014 0.004 0.030 0.004 0.044 0.256 4.446 0.368 8.846 0.37 13.206-16.924 4.256-32.192 10.436-45.872 17.63-0.052-0.612-0.092-1.216-0.152-1.83 0-0.008 0-0.018 0-0.026-4.57-46.81-29.572-82.16-55.852-78.958-26.28 3.204-43.88 43.75-39.312 90.558 0 0.010 0.004 0.018 0.004 0.026 1.992 20.408 7.868 38.636 16.042 52.444-2.034 1.604-7.784 5.812-14.406 10.656-4.97 3.634-11.020 8.058-18.314 13.43-19.882-26.094-33.506-63.58-35.94-105.89zM665.26 760.178c-1.9 43.586-58.908 84.592-111.582 101.044l-0.296 0.096c-21.9 7.102-41.428 19.6-62.104 32.83-34.732 22.224-70.646 45.208-122.522 45.208-3.404 0-6.894-0.104-10.326-0.296-47.516-2.778-69.742-23.032-97.88-48.676-14.842-13.526-30.19-27.514-49.976-39.124l-0.424-0.244c-42.706-24.104-69.212-54.082-70.908-80.194-0.842-12.98 4.938-24.218 17.182-33.4 26.636-19.972 44.478-33.022 56.284-41.658 13.11-9.588 17.068-12.48 20-15.264 2.096-1.986 4.364-4.188 6.804-6.562 24.446-23.774 65.36-63.562 128.15-63.562 38.404 0 80.898 14.8 126.17 43.902 21.324 13.878 39.882 20.286 63.38 28.4 16.156 5.578 34.468 11.902 58.992 22.404l0.396 0.164c22.88 9.404 49.896 26.564 48.66 54.932zM652.646 657.806c-4.4-2.214-8.974-4.32-13.744-6.286-22.106-9.456-39.832-15.874-54.534-20.998 8.116-15.894 13.16-35.72 13.624-57.242 0-0.010 0-0.022 0-0.030 1.126-52.374-25.288-94.896-58.996-94.976-33.71-0.078-61.95 42.314-63.076 94.686 0 0.010 0 0.018 0 0.028-0.038 1.714-0.042 3.416-0.020 5.11-20.762-9.552-41.18-16.49-61.166-20.76-0.092-1.968-0.204-3.932-0.244-5.92 0-0.016 0-0.036 0-0.050-1.938-95.412 56.602-174.39 130.754-176.402 74.15-2.014 135.828 73.7 137.772 169.11 0 0.018 0 0.038 0 0.052 0.874 43.146-10.66 82.866-30.37 113.678z"],"tags":["tux","brand","linux"],"defaultCode":60093,"grid":16,"attrs":[]},"attrs":[],"properties":{"ligatures":"tux, brand52","name":"linux","order":5,"id":446,"prevSize":32,"code":128039},"setIdx":2,"setId":1,"iconIdx":445},{"icon":{"paths":["M791.498 544.092c-1.294-129.682 105.758-191.876 110.542-194.966-60.152-88.020-153.85-100.078-187.242-101.472-79.742-8.074-155.596 46.948-196.066 46.948-40.368 0-102.818-45.754-168.952-44.552-86.916 1.292-167.058 50.538-211.812 128.38-90.304 156.698-23.126 388.84 64.89 515.926 43.008 62.204 94.292 132.076 161.626 129.58 64.842-2.588 89.362-41.958 167.756-41.958s100.428 41.958 169.050 40.67c69.774-1.296 113.982-63.398 156.692-125.796 49.39-72.168 69.726-142.038 70.924-145.626-1.548-0.706-136.060-52.236-137.408-207.134zM662.562 163.522c35.738-43.358 59.86-103.512 53.28-163.522-51.478 2.096-113.878 34.29-150.81 77.55-33.142 38.376-62.148 99.626-54.374 158.436 57.466 4.484 116.128-29.204 151.904-72.464z"],"tags":["apple","brand"],"defaultCode":60094,"grid":16,"attrs":[]},"attrs":[],"properties":{"ligatures":"apple, brand53","name":"apple","order":6,"id":447,"prevSize":32,"code":127822},"setIdx":2,"setId":1,"iconIdx":446},{"icon":{"paths":["M896 384c-35.2 0-64 28.8-64 64v256c0 35.2 28.8 64 64 64s64-28.8 64-64v-256c0-35.2-28.8-64-64-64zM128 384c-35.2 0-64 28.8-64 64v256c0 35.2 28.8 64 64 64s64-28.8 64-64v-256c0-35.2-28.802-64-64-64zM224 736c0 53.020 42.98 96 96 96v0 128c0 35.2 28.8 64 64 64s64-28.8 64-64v-128h128v128c0 35.2 28.8 64 64 64s64-28.8 64-64v-128c53.020 0 96-42.98 96-96v-352h-576v352z","M798.216 320.002c-9.716-87.884-59.004-163.792-129.62-209.646l32.024-64.046c7.904-15.806 1.496-35.028-14.31-42.932s-35.030-1.496-42.932 14.312l-32.142 64.286-8.35-3.316c-28.568-9.502-59.122-14.66-90.886-14.66-31.762 0-62.316 5.158-90.888 14.656l-8.348 3.316-32.142-64.282c-7.904-15.808-27.128-22.212-42.932-14.312-15.808 7.904-22.214 27.126-14.312 42.932l32.022 64.046c-70.616 45.852-119.904 121.762-129.622 209.644v32h574.222v-31.998h-1.784zM416 256c-17.674 0-32-14.328-32-32 0-17.648 14.288-31.958 31.93-31.996 0.032 0 0.062 0.002 0.094 0.002 0.018 0 0.036-0.002 0.052-0.002 17.638 0.042 31.924 14.35 31.924 31.996 0 17.672-14.326 32-32 32zM608 256c-17.674 0-32-14.328-32-32 0-17.646 14.286-31.954 31.924-31.996 0.016 0 0.034 0.002 0.050 0.002 0.032 0 0.064-0.002 0.096-0.002 17.64 0.038 31.93 14.348 31.93 31.996 0 17.672-14.326 32-32 32z"],"tags":["android","brand","os","mobile"],"defaultCode":60096,"grid":16,"attrs":[]},"attrs":[],"properties":{"ligatures":"android, brand55","name":"android","order":7,"id":449,"prevSize":32,"code":129302},"setIdx":2,"setId":1,"iconIdx":448},{"icon":{"paths":["M412.23 511.914c-47.708-24.518-94.086-36.958-137.88-36.958-5.956 0-11.952 0.18-17.948 0.708-55.88 4.624-106.922 19.368-139.75 30.828-8.708 3.198-17.634 6.576-26.83 10.306l-89.822 311.394c61.702-22.832 116.292-33.938 166.27-33.938 80.846 0 139.528 30.208 187.992 61.304 22.962-77.918 78.044-266.090 94.482-322.324-11.95-7.284-24.076-14.57-36.514-21.32zM528.348 591.070l-90.446 314.148c26.832 15.372 117.098 64.050 186.212 64.050 55.792 0 118.252-14.296 190.834-43.792l86.356-301.976c-58.632 18.922-114.876 28.52-167.464 28.52-95.95 0-163.114-31.098-205.492-60.95zM292.822 368.79c77.118 0.798 134.152 30.208 181.416 60.502l92.752-317.344c-19.546-11.196-70.806-39.094-107.858-48.6-24.386-5.684-50.020-8.616-77.204-8.616-51.796 0.976-108.388 13.946-172.888 39.8l-88.44 310.596c64.808-24.436 120.644-36.34 172.086-36.34 0.046 0.002 0.136 0.002 0.136 0.002zM1024 198.124c-58.814 22.832-116.208 34.466-171.028 34.466-91.686 0-159.292-31.802-203.094-62.366l-91.95 318.236c61.746 39.708 128.29 59.878 198.122 59.878 56.948 0 115.94-13.68 175.462-40.688l-0.182-2.222 3.734-0.886 88.936-306.418z"],"tags":["windows","brand","os"],"defaultCode":60097,"grid":16,"attrs":[]},"attrs":[],"properties":{"ligatures":"windows, brand56","name":"windows7","order":3,"id":450,"prevSize":32,"code":129696},"setIdx":2,"setId":1,"iconIdx":449},{"icon":{"paths":["M0.35 512l-0.35-312.074 384-52.144v364.218zM448 138.482l511.872-74.482v448h-511.872zM959.998 576l-0.126 448-511.872-72.016v-375.984zM384 943.836l-383.688-52.594-0.020-315.242h383.708z"],"tags":["windows8","brand","os"],"defaultCode":60098,"grid":16,"attrs":[]},"attrs":[],"properties":{"ligatures":"windows8, brand57","name":"windows8","order":4,"id":451,"prevSize":32,"code":129695},"setIdx":2,"setId":1,"iconIdx":450},{"icon":{"paths":["M258.278 446.542l-146.532-253.802c93.818-117.464 238.234-192.74 400.254-192.74 187.432 0 351.31 100.736 440.532 251h-417.77c-7.504-0.65-15.092-1-22.762-1-121.874 0-224.578 83.644-253.722 196.542zM695.306 325h293.46c22.74 57.93 35.234 121.004 35.234 187 0 280.826-226.1 508.804-506.186 511.926l209.394-362.678c29.48-42.378 46.792-93.826 46.792-149.248 0-73.17-30.164-139.42-78.694-187zM326 512c0-102.56 83.44-186 186-186s186 83.44 186 186c0 102.56-83.44 186-186 186s-186-83.44-186-186zM582.182 764.442l-146.578 253.878c-246.532-36.884-435.604-249.516-435.604-506.32 0-91.218 23.884-176.846 65.696-251.024l209.030 362.054c41.868 89.112 132.476 150.97 237.274 150.97 24.3 0 47.836-3.34 70.182-9.558z"],"tags":["chrome","browser","internet","brand"],"defaultCode":60121,"grid":16,"attrs":[]},"attrs":[],"properties":{"ligatures":"chrome, browser","name":"chrome","order":8,"id":474,"prevSize":32,"code":129535},"setIdx":2,"setId":1,"iconIdx":473},{"icon":{"paths":["M1022.526 334.14l-11.86 76.080c0 0-16.954-140.856-37.732-193.514-31.846-80.688-46.014-80.040-46.108-79.922 21.33 54.204 17.462 83.324 17.462 83.324s-37.792-102.998-137.712-135.768c-110.686-36.282-170.57-26.364-177.488-24.486-1.050-0.008-2.064-0.010-3.030-0.010 0.818 0.062 1.612 0.146 2.426 0.212-0.034 0.020-0.090 0.042-0.082 0.052 0.45 0.548 122.306 21.302 143.916 50.996 0 0-51.76 0-103.272 14.842-2.328 0.666 189.524 23.964 228.746 215.674 0 0-21.030-43.876-47.040-51.328 17.106 52.036 12.714 150.776-3.576 199.85-2.096 6.312-4.24-27.282-36.328-41.75 10.28 73.646-0.616 190.456-51.708 222.632-3.982 2.504 32.030-115.31 7.242-69.762-142.708 218.802-311.404 100.972-387.248 49.11 38.866 8.462 112.654-1.318 145.314-25.612 0.042-0.030 0.078-0.056 0.118-0.086 35.468-24.252 56.472-41.964 75.334-37.772 18.874 4.214 31.438-14.726 16.78-31.53-14.676-16.838-50.314-39.978-98.524-27.366-34 8.904-76.134 46.522-140.448 8.432-49.364-29.25-54.012-53.546-54.45-70.376 1.218-5.966 2.754-11.536 4.576-16.624 5.682-15.87 22.912-20.658 32.494-24.438 16.256 2.792 30.262 7.862 44.968 15.406 0.19-4.894 0.252-11.39-0.018-18.76 1.41-2.802 0.538-11.252-1.722-21.58-1.302-10.308-3.42-20.974-6.752-30.692 0.012-0.002 0.020-0.010 0.030-0.014 0.056-0.018 0.108-0.040 0.156-0.070 0.078-0.044 0.146-0.112 0.208-0.19 0.012-0.020 0.030-0.034 0.044-0.052 0.082-0.124 0.154-0.272 0.198-0.466 1.020-4.618 12.022-13.524 25.718-23.1 12.272-8.58 26.702-17.696 38.068-24.752 10.060-6.248 17.72-10.882 19.346-12.098 0.618-0.466 1.358-1.012 2.164-1.636 0.15-0.116 0.3-0.232 0.454-0.354 0.094-0.074 0.19-0.148 0.286-0.226 5.41-4.308 13.484-12.448 15.178-29.578 0.004-0.042 0.010-0.080 0.012-0.122 0.050-0.504 0.092-1.014 0.13-1.534 0.028-0.362 0.050-0.726 0.072-1.096 0.014-0.284 0.032-0.566 0.044-0.856 0.030-0.674 0.050-1.364 0.060-2.064 0-0.040 0.002-0.076 0.004-0.116 0.022-1.658-0.006-3.386-0.104-5.202-0.054-1.014-0.126-1.93-0.298-2.762-0.008-0.044-0.018-0.092-0.028-0.136-0.018-0.082-0.036-0.164-0.058-0.244-0.036-0.146-0.076-0.292-0.122-0.43-0.006-0.018-0.010-0.032-0.016-0.046-0.052-0.16-0.112-0.314-0.174-0.464-0.004-0.006-0.004-0.010-0.006-0.016-1.754-4.108-8.32-5.658-35.442-6.118-0.026-0.002-0.050-0.002-0.076-0.002v0c-11.066-0.188-25.538-0.194-44.502-0.118-33.25 0.134-51.628-32.504-57.494-45.132 8.040-44.46 31.276-76.142 69.45-97.626 0.722-0.406 0.58-0.742-0.274-0.978 7.464-4.514-90.246-0.124-135.186 57.036-39.888-9.914-74.654-9.246-104.616-2.214-5.754-0.162-12.924-0.88-21.434-2.652-19.924-18.056-48.448-51.402-49.976-91.208 0 0-0.092 0.072-0.252 0.204-0.020-0.382-0.056-0.76-0.072-1.142 0 0-60.716 46.664-51.628 173.882-0.022 2.036-0.064 3.986-0.12 5.874-16.432 22.288-24.586 41.020-25.192 45.156-14.56 29.644-29.334 74.254-41.356 141.98 0 0 8.408-26.666 25.284-56.866-12.412 38.022-22.164 97.156-16.436 185.856 0 0 1.514-19.666 6.874-47.994 4.186 55.010 22.518 122.924 68.858 202.788 88.948 153.32 225.67 230.74 376.792 242.616 26.836 2.212 54.050 2.264 81.424 0.186 2.516-0.178 5.032-0.364 7.55-0.574 30.964-2.174 62.134-6.852 93.238-14.366 425.172-102.798 378.942-616.198 378.942-616.198z"],"tags":["firefox","browser","internet","brand"],"defaultCode":60122,"grid":16,"attrs":[]},"attrs":[],"properties":{"ligatures":"firefox, browser2","name":"firefox","order":9,"id":475,"prevSize":32,"code":129418},"setIdx":2,"setId":1,"iconIdx":474},{"icon":{"paths":["M15.4 454.6c30-236.8 191.6-451.6 481.2-454.6 174.8 3.4 318.6 82.6 404.2 233.6 43 78.8 56.4 161.6 59.2 253v107.4h-642.6c3 265 390 256 556.6 139.2v215.8c-97.6 58.6-319 111-490.4 43.6-146-54.8-250-207.6-249.4-354.6-4.8-190.6 94.8-316.8 249.4-388.6-32.8 40.6-57.8 85.4-70.8 163h362.8c0 0 21.2-216.8-205.4-216.8-213.6 7.4-367.6 131.6-454.8 259v0z"],"tags":["edge","browser","brand"],"defaultCode":60124,"grid":16,"attrs":[]},"attrs":[],"properties":{"name":"edge","ligatures":"edge, browser4","order":10,"id":477,"prevSize":32,"code":9428},"setIdx":2,"setId":1,"iconIdx":476}],"height":1024,"metadata":{"name":"osicon"},"preferences":{"showGlyphs":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"icon-","metadata":{"fontFamily":"osicon","majorVersion":1,"minorVersion":0},"metrics":{"emSize":1024,"baseline":6.25,"whitespace":50},"embed":false,"showSelector":false,"showMetrics":false,"showMetadata":false,"showVersion":false},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"classSelector":".icon","name":"icomoon"},"historySize":50,"showCodes":true,"gridSize":16}}
\ No newline at end of file
diff --git a/tools/lastword b/tools/lastword
new file mode 100755 (executable)
index 0000000..b8e94f9
--- /dev/null
@@ -0,0 +1,9 @@
+#!/usr/bin/env -S perl -ni
+use 5.014;
+use warnings;
+use lib '.';
+use Shiar_Sheet::DB;
+
+my $db = Shiar_Sheet::DB->connect;
+my $query = $db->select(word => 'max(updated)', \['updated > ?', $_]);
+say ($query->list or exit 1);
index c1817585d8a6d04cc7cd21375d1086197ed5033b..5ab3cbaf048ed1c60ac0ad6d6fa590bbe4f00fce 100755 (executable)
@@ -6,8 +6,9 @@ use Data::Dump 'pp';
 use JSON::PP;
 use File::stat;
 use Time::Piece;
+use List::Util 'uniq';
 
-our $VERSION = '1.02';
+our $VERSION = '1.04';
 
 my %BROWSERJOIN = (
        edge    => 'ie',
@@ -28,21 +29,59 @@ delete $data->{eras};
 for (values %{ $data->{data} }) {
        delete @{$_}{qw[ usage_perc_a usage_perc_y ]};
 }
-for (values %{ $data->{agents} }) {
-       delete $_->{usage_global};
-       shift @{ $_->{versions} } while !defined $_->{versions}->[0];
-}
+while (my ($browser, $alias) = each %BROWSERJOIN) {
+       my $agent =        $data->{agents}->{$browser} or next;
+       my $amend = delete $data->{agents}->{$alias}   or next;
+       unless ($agent->{prefix} eq $amend->{prefix}) {
+               $_->{prefix} ||= $amend->{prefix} for @{ $amend->{version_list} };
+       }
+       unshift @{ $agent->{version_list} }, @{ $amend->{version_list} };
+       $agent->{browser} = sprintf '%s (former %s)',
+               $agent->{browser}, $amend->{browser};
 
-while (my ($browser, $target) = each %BROWSERJOIN) {
-       my $agent1 = delete $data->{agents}->{$browser} or next;
-       my $agent2 =        $data->{agents}->{$target}  or next;
-       splice @{ $agent2->{versions} }, -3, 3, @{ $agent1->{versions} };
-       $agent2->{browser} = sprintf '%s (former %s)',
-               $agent1->{browser}, $agent2->{browser};
+       # prefer deprecated name if newer is convoluted (Chr/And.)
+       $agent->{abbr} = $amend->{abbr} if $agent->{abbr} =~ m{/};
 
        for (values %{ $data->{data} }) {
-               my $stats = delete $_->{stats}->{$browser} or next;
-               $_ = { %{$_}, %{$stats} } for $_->{stats}->{$target};
+               my $stats = delete $_->{stats}->{$alias} or next;
+               $_ = { %{$_}, %{$stats} } for $_->{stats}->{$browser};
+       }
+}
+
+for my $feature (values %{ $data->{data} }) {
+       while (my ($attr, $row) = each %{$feature}) {
+               delete $feature->{$attr} if ref $row eq '' and $row eq '';
+       }
+}
+
+while (my ($agent, $row) = each %{ $data->{agents} }) {
+       delete $row->{usage_global};
+       delete $row->{prefix_exceptions};  # duplicate of version_list->prefix
+       $row->{versions} = [ uniq map { $_->{version} } @{ $row->{version_list} } ];
+
+       # convert metadata list into (cleaned) lookup table
+       my %version_lookup;
+       for (@{ $row->{version_list} }) {
+               delete $_->{era};
+               delete $_->{prefix} unless $_->{prefix};
+               $version_lookup{ delete $_->{version} } = $_;
+       }
+       $row->{version_list} = \%version_lookup;
+
+       # omit identical values from subsequent versions
+       for my $feature (values %{ $data->{data} }) {
+               my $cmp;  # same value to be omitted
+               my $verstats = $feature->{stats}->{$agent};
+               for my $version (@{ $row->{versions} }) {
+                       defined $verstats->{$version}
+                               or warn "missing feature $feature->{title} for $agent $version";
+                       if (defined $cmp and $verstats->{$version} eq $cmp) {
+                               delete $verstats->{$version};
+                       }
+                       else {
+                               $cmp = $verstats->{$version};
+                       }
+               }
        }
 }
 
index 64738ae80acc41c11b135b412ff0afdd940f377e..8bf3d54fb62bea78a537947f7360209a7031ea34 100755 (executable)
@@ -3,11 +3,12 @@ use 5.014;
 use warnings;
 use utf8;
 no if $] >= 5.018, warnings => 'experimental::smartmatch';
+use lib '.';
 
-use open OUT => ':utf8', ':std';
+use open OUT => ':encoding(utf-8)', ':std';
 use Data::Dump 'pp';
 
-our $VERSION = '1.02';
+our $VERSION = '1.03';
 
 my %info = (
        # prepare presentational string for some control(lish) entries
@@ -17,11 +18,14 @@ my %info = (
        "\x{200B}" => {string => '␣'}, # nbsp: ~ in TeX
        "\x{200C}" => {string => '|'}, # ISO-9995-7-081 lookalike (alt: ∣ ⊺ ⟙)
        "\x{200D}" => {string => '⁀'}, # join (alt: ∤ |ͯ ⨝)
+       (map {( $_ => {string => chr(9676).$_.chr(9676)} )} map {chr} # combining double
+               0x35C .. 0x362, 0x1DCD, 0x1DFC,
+       ),
 );
 $info{chr $_} //= {} for 32 .. 126;
 
 eval {
-       my $tables = do 'unicode-table.inc.pl' or die $@ || $!;
+       my $tables = do './unicode-table.inc.pl' or die $@ || $!;
        for (values %$tables) {
                for (values %$_) {
                        for (@$_) {
@@ -34,6 +38,14 @@ eval {
        1;
 } or warn "Failed reading unicode tables: $@";
 
+for my $layout ('macos-abc', 'windows') {
+       eval {
+               my $kbd = do "./keyboard/altgr/$layout.eng.inc.pl" or die $@ || $!;
+               $info{$_} //= {} for map {s/◌//g; m/\A./g} values %{ $kbd->{key} };
+               1;
+       } or warn "Failed reading additional keyboard map $layout: $@";
+}
+
 eval {
        require HTML::Entities;
        our %char2entity;
@@ -46,9 +58,9 @@ eval {
 } or warn "Failed importing html entities: $@";
 
 my %diinc = (
-       'data/digraphs-rfc.inc.pl' => 'u-di',
-       'data/digraphs-shiar.inc.pl' => 'u-prop',
-       'data/digraphs-vim.inc.pl' => 'u-vim',
+       './data/digraphs-rfc.inc.pl' => 'u-di',
+       './data/digraphs-shiar.inc.pl' => 'u-prop',
+       './data/digraphs-vim.inc.pl' => 'u-vim',
 );
 for (sort keys %diinc) {
        -e $_ or next;
@@ -64,16 +76,17 @@ for (sort keys %diinc) {
 
 eval {
        # read introducing unicode versions for known characters
-       my $agemap = do 'data/unicode-age.inc.pl' or die $@ || $!;
+       my $agemap = do './data/unicode-age.inc.pl' or die $@ || $!;
        for my $chr (keys %info) {
                my $version = $agemap->{ord $chr} or next;
                $info{$chr}->{class}->{'u-v'.$version}++
        }
        1;
-} or warn "Failed including unicode version data $@";
+} or warn "Failed including unicode version data: $@";
 
 for my $chr (keys %info) {
        my $cp = ord $chr;
+       #my $info = glyph_mkinfo($cp) or next;
        # attempt to get unicode character information
        my $info = eval {
                require Unicode::UCD;
diff --git a/tools/mkclioptions b/tools/mkclioptions
new file mode 100755 (executable)
index 0000000..020f025
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/env perl
+use 5.014;
+use warnings;
+use utf8;
+use re '/msx';
+use open OUT => ':encoding(utf-8)', ':std';
+
+our $VERSION = '1.00';
+
+my %group = (
+       core => [qw(
+               cat chgrp chmod chown cp date df ln ls mkdir mknod mktemp mv
+               readlink rm rmdir touch uname
+       )],
+       usr => [qw(
+               base64 basename chcon cksum comm csplit cut dircolors
+               dirname du env expand expr factor fmt fold groups head hostid id
+               install join link logname md5sum mkfifo nice nl nohup nproc numfmt od
+               paste pathchk pinky pr printenv printf ptx realpath runcon seq sha1sum
+               shred shuf sort split stat
+               stdbuf sum tac tail tee test timeout tr truncate tsort tty unexpand
+               uniq unlink users wc who whoami yes
+       )],
+       -usr => [qw(
+               base32 b2sum sha224sum sha256sum sha384sum sha512sum
+       )],
+       bsd => [qw(
+               calendar col column hexdump look ncal
+       )],
+       more => [qw(
+               find xargs  free skill htop uptime watch  sed awk perl figlet less
+               curl wget ping fping ssh nc
+       )],
+       corerest => [qw(
+               dd pwd sleep stty sync
+       )],
+       git => [map {"git $_"} qw( log status )],
+);
+
+say '# automatically generated by tools/mkclioptions';
+say '+{';
+
+for my $program (map { $group{$_} ? @{$group{$_}} : $_ } @ARGV) {
+       my $help = eval {
+               local $/;
+               open my $output, '-|', split(/\h/, $program), '--help';
+               warn "$program exited with status $?\n" if $?;
+               return readline $output;
+       } or next;
+
+       printf "'%s' => {\n", $program;
+
+       while ($help =~ m{ ^\h+ (-\N*?) (?: \h{3,} (\N*) )? \n }g) {
+               #TODO: continuations
+               my ($options, $explain) = ($1, $2);
+               $explain =~ s/_\010//g if defined $explain;  # underlines in less
+               my ($short) = $options =~ m{ (?<! \H) -([^-]) (?! \w) }
+                       or 0 or next;  #TODO: option to keep long
+               printf "  '%s' => [q{%s}, q{%s}],\n", $short // '', $options, $explain // '';
+       }
+
+       say '},';
+}
+
+say '}';
+
+__END__
+
+=head1 NAME
+
+mkclioptions
+
+=head1 SYNOPSIS
+
+    mkclioptions ls >clioptions.inc.pl
+
+=head1 AUTHOR
+
+Mischa POSLAWSKY <perl@shiar.org>
+
+=head1 LICENSE
+
+Licensed under the GNU Affero General Public License version 3.
+
index 56f85c41f0d921bfe1a5b88ae46cff534b35cf6c..09158634dd12f7381653c62d0e6779a82ea4e2d7 100755 (executable)
@@ -86,7 +86,7 @@ for (values %cc) {
                s/(?<=.)\(.*\)\s*//;
                s/ republic\b//gi;
                s/ islands?\b//gi;
-               s/\bthe //g;
+               s/\bthe //gi;
                s/ and / & /g and s/(?<=.)[a-z ]+//g;
                s/ of / /g;
                s/\bsa?int /st /gi;
index 2d1fdbd8380b27cb4d620e5d99bee3ac110bee45..5117b7690117cb12bed52963260d0d885a621195 100755 (executable)
@@ -4,9 +4,10 @@ use strict;
 use warnings;
 use utf8;
 
-use open OUT => ':utf8', ':std';
+use open OUT => ':encoding(utf-8)', ':std';
+use JSON ();
 
-our $VERSION = '1.06';
+our $VERSION = '1.07';
 
 # import and combine various digraph data
 push @INC, 'data';
@@ -24,29 +25,50 @@ my $di = { %{$vim // {}}, %{$rfc}, %{$extra // {}} };
 my $uninfo = do 'unicode-char.inc.pl'
        or warn "could not include unicode details: ", $@ // $!;
 
-# output perl code of hash
-# (assume no backslashes or curlies, so we can just q{} w/o escaping)
-print "# automatically generated by $0\n";
-print "use utf8;\n";
-print "+{\n";
-printf '(map {$_=>0} qw{%s}),'."\n", join(' ',
+# output json map of character info
+my %table;
+$table{$_} = 0 for (
        grep { !defined $di->{$_} }
        map { substr($_, 1, 1).substr($_, 0, 1) } sort keys %{$di}
 );
-printf "q{%s}=>[%s],\n", s/(?=[\\}])/\\/gr, join(',',
+$table{$_} = [
        ord $di->{$_},   # original code point
-       map {"'$_'"}
        $uninfo->{ $di->{$_} }->[1] // '',  # name
-       join(' ',
+       (
                $rfc->{$_}
-                       ? $vim->{$_} ? 'l4' : 'l1'  # vim+rfc or rfc only
+                       ? $vim->{$_} ? 'l5' : 'l1'  # vim+rfc or rfc only
+                       : $vimold && $vimold->{$_} ? 'l4'  # compat vim if known
                        : $vim->{$_} ? 'l3' : 'l2', # vim only or neither
-               $vimold && $vim->{$_} && !$vimold->{$_} ? 'ex' : (), # new vim feature
        ),
-       ($uninfo->{ $di->{$_} }->[0] // '') =~ s/ u-di| u-prop| ex//gr,  # class
+       ($uninfo->{ $di->{$_} }->[0] // '') =~ s/ u-di| u-prop//gr,  # class
        $uninfo->{ $di->{$_} }->[4] // (),  # string
-) for sort keys %{$di};
-print "}\n";
+] for sort keys %{$di};
+
+print JSON->new->ascii->canonical->encode({
+       title => 'RFC-1345',
+       key  => \%table,
+       intro => join("\n",
+               'Character mnemonics following compose key ⎄:',
+               'i^k in <a href="/vi">Vim</a>,',
+               '^u^\ in <a href="/readline">Emacs</a>,',
+               '^a^v in <a href="/screen">Screen</a>.',
+               'Similar but different from <a href="/digraphs/xorg">X.Org</a>.',
+               'Also see <a href="/unicode">common Unicode</a>.</p>',
+               '<p class="aside">Unofficial <span class="u-l2">proposals</span>',
+               'are available as <a href="/digraphs.vim">ex commands</a>.',
+       ),
+       flag => {
+               l5 => 'full support',
+               l4 => 'vim extension',
+               l3 => 'vim v8.0',
+               l2 => 'proposal',
+               l1 => 'not in vim',
+       },
+       flagclass => {
+               l5 => '', # common
+               l3 => 'u-l5', # rare
+       },
+});
 
 __END__
 
@@ -56,15 +78,14 @@ mkdigraphlist - Output character list of combined digraph data
 
 =head1 SYNOPSIS
 
-    mkdigraphlist >digraphs.inc.pl
-    perl -e'$di = do "digraphs.inc.pl"; print chr $di->{DO}->[0]'
+    mkdigraphlist | jq -r '.key."DO"[0]' | perl -nE 'say chr' # $
 
 =head1 DESCRIPTION
 
 Combines precompiled digraph includes of rfc (1345), vim, and shiar
 and outputs a complete map including character details and usage classes.
 
-The value can either be a scalar string containing another
+The C<key> values can either be a scalar string containing another
 digraph which can be considered identical (usually inverted),
 or an array ref containing at least the resulting character's
 Unicode code point value.  If available, the following UCD data
@@ -72,9 +93,11 @@ is appended: character name, usage classes, unicode classes,
 and replacement output string.
 For example:
 
- +{
-   AE => [198, 'LATIN CAPITAL LETTER AE', 'u-di', 'Latin Lu Xl u-v11'],
-   EA => 'AE',
+  {
+   "key": {
+    "AE" => [198, "LATIN CAPITAL LETTER AE", "u-di", "Latin Lu Xl u-v11"],
+    "EA" => "AE",
+   }
   }
 
 =head1 AUTHOR
diff --git a/tools/mkdigraphs-plan9 b/tools/mkdigraphs-plan9
new file mode 100755 (executable)
index 0000000..41615ef
--- /dev/null
@@ -0,0 +1,79 @@
+#!/usr/bin/env perl
+use 5.014;
+use warnings;
+use utf8;
+use open IO => ':encoding(utf-8)', ':std';
+use Data::Dump 'pp';
+
+our $VERSION = '1.01';
+
+# translation table for deprecated code points
+my %replace = (
+       0xF015 => '⎇',  # alt
+       0xF016 => '⇧',  # shift
+       0xF017 => '⎈',  # control
+);
+
+# expect input data source at command line
+@ARGV or die "Specify input source file or - for STDIN\n";
+
+# convert each character line to perl code
+# (assume no backslashes or curlies, so we can just q{} w/o escaping)
+say "# automatically generated by $0";
+say 'use utf8;';
+say '+{';
+while ($_ = readline) {
+       my ($chrhex, $mnems, $sample, $name) = m{\A([0-9A-F ]{5}) (.{11}) (.)\h(.*)}i
+               or warn("syntax error on line $.: $_"), next;
+       $chrhex =~ s/ $//;
+       my $chrnum = hex $chrhex;
+       my $chr = chr $chrnum;
+       $chr eq $sample
+               or warn("character mismatch on line $.: $_"), next;
+       $chr = $replace{$chrnum} or next if defined $replace{$chrnum};
+       my $chrstr = pp($replace{$chrnum} // $chr);
+       for my $mnem (split / /, $mnems) {
+#              next if length $mnem != 2;
+               say "q{$mnem} => $chrstr, # $name";
+       }
+}
+say '}';
+
+__END__
+
+=head1 NAME
+
+mkdigraphs-plan9 - Output digraph data from Plan9 keyboard data
+
+=head1 SYNOPSIS
+
+Extract digraphs from text specifications as a perl hash:
+
+    mkdigraphs-plan9 keyboard >digraphs-plan9.inc.pl
+
+Input can be the literal RFC (or similar) document:
+
+    curl https://9fans.github.io/usr/local/plan9/lib/keyboard | mkdigraphs-plan9 -
+
+Test by printing the character for DO (should be a dollar sign):
+
+    perl -e'$di = do "digraphs-plan9.inc.pl"; print chr $di->{DO}'
+
+=head1 DESCRIPTION
+
+Parses the official RFC-1345 document, searching the
+'character mnemonic table' for all digraph definitions.
+If successful, Perl code is output resulting in a hash
+with Unicode code points keyed by digraph.
+Obsolete values (references to private use area)
+are converted to modern alternatives.
+Any errors and warnings are given at STDERR.
+
+=head1 AUTHOR
+
+Mischa POSLAWSKY <perl@shiar.org>
+
+=head1 LICENSE
+
+Licensed under the GNU Affero General Public License version 3.
+
index 989c92651a4a2e5c44e3a588fafd93f3d07caa8e..48ce7f06171436a30d7b284dda8a30f8085fa393 100755 (executable)
@@ -2,7 +2,7 @@
 use 5.014;
 use warnings;
 use utf8;
-use open OUT => ':utf8', ':std';
+use open OUT => ':encoding(utf-8)', ':std';
 use charnames ':full';
 use Data::Dump 'pp';
 
index 55134b4bb7487b4a52ff8a790a6fa6d89c16b9b6..f3582e8d8949ae4f9ba87c4bc80b52d68e28118e 100755 (executable)
@@ -1,11 +1,11 @@
 #!/usr/bin/env perl
 use 5.014;
 use warnings;
-use open IN => ':utf8', ':std';
+use open IN => ':encoding(utf-8)', ':std';
 
 our $VERSION = '1.03';
 
-my $di = do "data/digraphs-rfc.inc.pl"
+my $di = do "./data/digraphs-rfc.inc.pl"
        or warn "official digraphs not included for comparison: ", $@ // $!;
 
 say "# automatically generated by $0";
index aeed5fe036226bc9c86e888357cd529097fe39d6..be53fff65de0229be48c5796df5882c876a6c51a 100755 (executable)
 use 5.014;
 use warnings;
 use utf8;
-use open IO => ':utf8', ':std';
+use open IO => ':encoding(utf-8)', ':std';
 use re '/msx';
+use JSON 'decode_json';
 use Data::Dump 'pp';
+use Shiar_Sheet::FormatChar;
 
 our $VERSION = '1.01';
 
-my $keysymh;
-open $keysymh, '<', 'data/keysymdef.h'
-       or open $keysymh, '<', '/usr/include/X11/keysymdef.h'
-       or die "Could not find keysym definitions: $!\n";
-
-my %keysym;
-while (readline $keysymh) {
-       m{
-               \A  [#]define[ ]XK_ (?<name>[a-zA-Z_0-9]+)
-               \h+ 0x(?<value>[0-9a-f]+)
-               \h* [/][*] [\h(] U[+] (?<unicode>[0-9A-F]{4,6})
-       } or next;
-       $keysym{ $+{name} } = chr hex $+{unicode};
-}
+my $matchvim;  # enable to prefer best compatibility
+
+my $symname = eval {
+       open my $keysymh, '<', 'data/keysymdef.json' or die $!;
+       local $/;
+       return decode_json(readline $keysymh);
+} or die "Could not read keysym definitions: $@\n";
 
-say "# automatically generated by $0";
-say '+{';
+my $vidi = eval {
+       open my $jsfh, '<', 'data/digraphs.json' or die $!;
+       local $/;
+       return JSON->new->decode(readline $jsfh);
+} or warn "Could not read comparison digraphs: $@\n";
 
+my %table;
 while ($_ = readline) {
        my ($mnem, $chr, $trail) = m/\A <Multi_key> \h (.*?) \h+ : \h "([^"]+)" \h* (.*)/
                or next;
        $chr =~ s/\\(.)/$1/g;
        $mnem !~ m/<dead | <KP_ | <U[0-9A-Fa-f]{4}/ or next;  # skip non-standard keys
-       $mnem =~ s{<([^>]+)> \h?}{$keysym{$1} // die "reference to unknown keysym $1\n"}eg;
-       $mnem !~ m/[^\x20-\x7F]/ or next;  # skip unicode
-#      (state $seen = {})->{$chr}++ and next;
-       printf "%s => %s,\n", pp($mnem), pp($chr);
+       eval {
+               $mnem =~ s{<([^>]+)> \h?}{$symname->{$1} // die "reference to unknown keysym $1\n"}eg;
+               1;
+       } or warn($@), next;
+       $mnem =~ m/\A [\x20-\x7F]{2} \z/ or next;  # only interested in two ascii
+
+       my $alias = \(state $seen = {})->{$chr};  # assume first is preferred
+       my $cp = ord $chr;
+       my ($class, $name, undef, undef, $string) = @{
+               Shiar_Sheet::FormatChar->glyph_info($cp)
+       };
+       my $comparison = (
+               !$vidi->{key}->{$mnem} ? 'l3' :  # free
+               $vidi->{key}->{$mnem}->[0] != $cp ? 'l1' :  # conflict
+               $vidi->{key}->{$mnem}->[2] eq 'l5' ? 'l5' :  # rfc
+               'l4'  # any
+       );
+
+       if (${$alias}) {
+               # aliases an earlier occurrence
+               if ($matchvim and ${$alias}->[2] lt $comparison) {
+                       # replace lower compatibility level
+                       ${$alias}->[3] = 'l0';
+                       ${$alias}->[2] .=  ' u-' . ${$alias}->[2];
+                       ${$alias} = undef;
+               }
+               else {
+                       $class = 'l0';
+                       my $menm = substr($mnem, 1, 1).substr($mnem, 0, 1);
+                       if ($table{$menm} && $table{$menm}[0] == $cp) {
+                               # unannotated if identical to reversed input
+                               $cp = 0;
+                       }
+                       else {
+                               $class .= ' ex';
+                       }
+               }
+       }
+
+       $table{$mnem} = [ $cp, $name, $comparison, $class, $string // () ];
+       ${$alias} //= $table{$mnem};
 }
 
-say '}';
+print JSON->new->canonical->indent->encode({
+       title => 'X.Org',
+       key   => \%table,
+       intro => join("\n",
+               'Character mnemonics following compose key ⎄:',
+               'in the X Window System (Shift+AltGr by default).',
+               'Differences from <a href="/digraphs">RFC-1345</a> are indicated.',
+               'Also see <a href="/unicode">common Unicode</a>.',
+       ),
+       keywords => [qw( xorg x11 x )],
+       flag  => {
+               'l5' => "matching RFC-1345",
+               'l4' => "matching Vim extension",
+               'l3' => "unique to Xorg",
+               'l1' => "conflict",
+               ('l0' => "Xorg preference") x !!$matchvim,
+               'l0 ex' => "alias",
+       },
+       flagclass => {
+               l5 => 'u-l4',
+               l4 => 'u-l5',
+       },
+});
 
 __END__
 
@@ -48,13 +106,13 @@ mkdigraphs-xorg - Output Xorg compose sequences
 =head1 SYNOPSIS
 
 
-    mkdigraphs-xorg /usr/share/X11/locale/en_US.UTF-8/Compose >digraphs-xorg.inc.pl
-    perl -e'$di = do "digraphs-xorg.inc.pl"; print chr $di->{AT}'
+    mkdigraphs-xorg /usr/share/X11/locale/en_US.UTF-8/Compose |
+    jq -r '.key."AT"[0]' | perl -nE 'say chr' # @
 
 =head1 DESCRIPTION
 
 Extracts Multi_key definitions from X11/Xorg Compose.pre include file.
-If successful, Perl code is output resulting in a hash
+If successful, a JSON object is output containing a digraphs list in C<key>
 with Unicode code points keyed by mnemonics.
 Any errors and warnings are given at STDERR.
 
index e7cd4f53183c6de1cc7654c92de39b4331e0d83c..0b669d4ca99b4d903bdccd6dbe3fa7c99d0309f3 100755 (executable)
@@ -3,11 +3,11 @@ use 5.014;
 use warnings;
 use utf8;
 
-use open OUT => ':utf8', ':std';
+use open OUT => ':encoding(utf-8)', ':std';
 use File::Basename 'basename';
 use Data::Dump 'pp';
 
-our $VERSION = '1.01';
+our $VERSION = '1.02';
 
 my @fontlist;
 
@@ -28,7 +28,8 @@ for my $fontfile (glob 'data/font/*'.$incsuffix) {
                (map { "($_)" } $year || ()),
        );
        push @fontlist, $fontmeta;
-       $cover{$fontid} = { map { (chr $_ => 1) } @fontrange };
+       my $fontrange = $fontmeta->{cover};
+       $cover{$fontid} = { map { (chr $_ => 1) } $fontmeta->{cover}->@* };
 }
 
 my %charlist;
@@ -75,7 +76,7 @@ eval {
        use Unicode::UCD 'charinfo';
        for my $code (0 .. 256**2*2) {
                my $charinfo = charinfo($code) or next;
-               next if $charinfo->{category} =~ /^[MC]/;  # ignore Marks and "other" Control chars
+               next if $charinfo->{category} =~ /^[C]/;  # ignore "other" Control chars
                push @{ $charlist{$_}->{ $charinfo->{$_} } }, chr $code
                        for qw( script category block );
                push @{ $charlist{version}->{$_} }, (chr $code) x ($agemap->{$code} <= $_)
diff --git a/tools/mkimg-google b/tools/mkimg-google
new file mode 100755 (executable)
index 0000000..7615c27
--- /dev/null
@@ -0,0 +1,30 @@
+#!/bin/sh
+set -u
+
+CURL='curl -sSf'
+
+if true
+then
+       QUERYURL='https://duckduckgo.com/?iar=images&iax=images&ia=images&iaf=type:photo&q='
+       ARGMATCH="vqd='([^']+)"
+       JSONQUERY='https://duckduckgo.com/i.js?l=nl-nl&o=json&num=2'
+else
+       CURL="$CURL -A /"
+       QUERYURL="https://www.google.com/search?tbm=isch&pws=0&hl=nl&num=1&q="
+       ARGMATCH='<img [^>]+src="(http[^"]+)"'
+fi
+
+while read q
+do
+       q="${q%%/*}"
+       [ -e "$q.jpg" ] && continue
+       echo "$q"
+       QUERYARG="%22$q%22"
+       QUERYRES=$($CURL "$QUERYURL$QUERYARG" | perl -nE "say for /$ARGMATCH/" | head -1)
+       if [ -n "$JSONQUERY" ]
+       then
+               $CURL "$JSONQUERY&vqd=$QUERYRES&q=$QUERYARG" -o "$q.json" || continue
+               QUERYRES="$(jq -r '.results[0].thumbnail' "$q.json")"
+       fi
+       $CURL "$QUERYRES" -o "$q.jpg" || continue
+done
diff --git a/tools/mkimgthumb b/tools/mkimgthumb
new file mode 100755 (executable)
index 0000000..e9d6eb2
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/env perl
+use 5.014;
+use warnings;
+use lib $0 =~ s{[^/]+$}{..}r; # project root
+use Shiar_Sheet::ImagePrep '1.03';
+
+our $VERSION = '1.00';
+
+my %opt;
+
+if (@ARGV and $ARGV[0] =~ /^-/) {
+       require Getopt::Long;
+       Getopt::Long->import(qw( 2.33 :config gnu_getopt ));
+       GetOptions(\%opt,
+               'jpg=s',
+               'webp=s',
+       ) or exit 64;
+}
+
+%opt or %opt = (
+       jpg  => '300x200',
+       webp => '630x420@30',
+);
+
+my @ffs;
+for (keys %opt) {
+       push @ffs, my $ff = [$_];
+       my $r = $opt{$_};
+       push @{$ff}, -quality => $1 if $r =~ s/@(\d+)//;
+       push @{$ff}, -resize => !/\dx\d+$/ ? $_ : ("$_^", -extent => $_)
+               for split / /, $r;
+}
+
+my $target = '..';
+$target = pop @ARGV if @ARGV >= 2 and -d $ARGV[-1];
+
+my $failcount = 0;
+
+for my $src (@ARGV) {
+       my ($name, @cmds) = split /:(?<!\\:)/, $src =~ s/\.(\w+)\z//r;
+       my $ext = $1 // '*';
+       print $name;
+       next if $name =~ m/\./;
+       unless (-e $src) {
+               ($src) = grep {-e} glob qq<"$name"{,:*}.$ext> or next;
+       }
+       s/\\(.)/$1/g for @cmds;
+       print ':';
+
+       if (@cmds and $cmds[0] =~ /^\d/) {
+               # crop shorthand from initial dimension argument
+               my @crop = split /\D/, shift @cmds;
+               unshift @cmds, -gravity => 'southeast', -chop => "$crop[2]%x$crop[3]%"
+                       if @crop > 2;
+               unshift @cmds, -chop => "$crop[0]%x$crop[1]%";
+       }
+       push @cmds, -gravity => 'north';
+       eval {
+               my $image = Shiar_Sheet::ImagePrep->new($src);
+               for (@ffs) {
+                       my ($ff, @ffcmds) = @{$_};
+                       print " $ff";
+                       $image->convert("$target/$name.$ff", [@cmds, @ffcmds]);
+               }
+               1;
+       } or do {
+               say ' FAILED';
+               warn ref $@ eq 'ARRAY' ? $@->[1] : $@ if $@;
+               $failcount++;
+       };
+}
+continue {
+       say '';
+}
+
+exit $failcount;
diff --git a/tools/mkjson b/tools/mkjson
new file mode 100755 (executable)
index 0000000..dace34a
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/env perl
+use 5.012;
+use warnings;
+use JSON;
+use re '/msx';
+
+my %opt;
+my $jsonify = JSON->new->utf8->canonical;
+$jsonify->pretty if $opt{pretty};
+
+my $data = do "./$ARGV[0]" or die $@;
+print $jsonify->encode($data)
+       =~ s{\[ \K\n ([^][]+) (?=\])}{$1 =~ s/(?:\A|\n) \s*//gr}reg;
diff --git a/tools/mkkeyboard-xkb-symbols b/tools/mkkeyboard-xkb-symbols
new file mode 100755 (executable)
index 0000000..a7049fa
--- /dev/null
@@ -0,0 +1,119 @@
+#!/usr/bin/env perl
+use 5.014;
+use warnings;
+use utf8;
+use re '/msx';
+use JSON ();
+use Data::Dump 'pp';
+
+our $VERSION = '1.00';
+
+my $symname = eval {
+       open my $keysymh, '<', 'data/keysymdef.json' or die $!;
+       local $/;
+       return JSON->new->decode(readline $keysymh);
+} or die "Could not read keysym definitions: $@\n";
+
+my %geochar = (
+       TLDE => ["`", "~"],
+       AE01 => ['1', "!"],
+       AE02 => ['2', "\@"],
+       AE03 => ['3', "#"],
+       AE04 => ['4', "\$"],
+       AE05 => ['5', "%"],
+       AE06 => ['6', "^"],
+       AE07 => ['7', "&"],
+       AE08 => ['8', "*"],
+       AE09 => ['9', "("],
+       AE10 => ['0', ")"],
+       AE11 => ["-", "_"],
+       AE12 => ["=", "+"],
+
+       AD01 => ["q", "Q"],
+       AD02 => ["w", "W"],
+       AD03 => ["e", "E"],
+       AD04 => ["r", "R"],
+       AD05 => ["t", "T"],
+       AD06 => ["y", "Y"],
+       AD07 => ["u", "U"],
+       AD08 => ["i", "I"],
+       AD09 => ["o", "O"],
+       AD10 => ["p", "P"],
+       AD11 => ["[", "{"],
+       AD12 => ["]", "}"],
+
+       AC01 => ["a", "A"],
+       AC02 => ["s", "S"],
+       AC03 => ["d", "D"],
+       AC04 => ["f", "F"],
+       AC05 => ["g", "G"],
+       AC06 => ["h", "H"],
+       AC07 => ["j", "J"],
+       AC08 => ["k", "K"],
+       AC09 => ["l", "L"],
+       AC10 => [";", ":"],
+       AC11 => ["'", '"'],
+
+       LSGT => ["§", "±"], # mac
+       AB01 => ["z", "Z"],
+       AB02 => ["x", "X"],
+       AB03 => ["c", "C"],
+       AB04 => ["v", "V"],
+       AB05 => ["b", "B"],
+       AB06 => ["n", "N"],
+       AB07 => ["m", "M"],
+       AB08 => [",", "<"],
+       AB09 => [".", ">"],
+       AB10 => ["/", "?"],
+       BKSL => ["\\","|"],
+);
+
+my %res;
+while (readline) {
+       my ($pos, $def) = m/^\h* key \h+ <(\w+)> \h+ \{ (.+?) \};/ or next;
+       my @mode = map { [split /,\h*/] } $def =~ m/\[ \h* (.*?) \h* \]/g;
+       for my $shift (0, 1) {
+               defined(my $chr = $mode[0]->[$shift + 2])
+                       or warn "missing $pos +$shift\n";
+               if ($chr =~ m/^U ([A-F0-9]+) $/) {
+                       $chr = chr hex $1;
+               }
+               elsif (defined $symname->{$chr}) {
+                       $chr = $symname->{$chr};
+               }
+               else {
+                       warn "unknown symbol $chr at $pos\n";
+               }
+               $res{$geochar{$pos}->[$shift] // $pos} = $chr;
+               #$res{$pos}[$shift] = $symname->{$chr} // $chr; # geochar
+       }
+}
+say pp \%res;
+
+__END__
+
+=head1 NAME
+
+mkkeyboard-xkb-symbols - Character map of an xkb symbols file
+
+=head1 SYNOPSIS
+
+    cat /usr/share/X11/xkb/symbols/us |
+    perl -ne 'print if /^xkb_symbols "intl"/../^\};/' |
+    mkkeyboard-xkb-symbols >map-us-intl.inc.pl
+
+=head1 DESCRIPTION
+
+Parses C<key> declarations inside an C<xkb_symbols> section
+and returns a perl hash of normalised qwerty input to unicode output
+of 3rd and 4th levels (altgr and shift+altgr modes)
+to be manually cleaned and integrated in a keyboard page include.
+
+=head1 AUTHOR
+
+Mischa POSLAWSKY <perl@shiar.org>
+
+=head1 LICENSE
+
+Licensed under the GNU Affero General Public License version 3.
+
index 12255b7fec34cc44736434fd2cf07f1c0d71ee14..0b4a7344ea6b83a5e207b97248d40fb84fb04653 100755 (executable)
@@ -2,18 +2,29 @@
 use 5.014;
 use warnings;
 
-our $VERSION = '1.00';
+our $VERSION = '1.04';
 
 use File::stat;
 use Time::Piece;
 
 my @pages = (
        [qw( index )],
-       [qw( vi digraphs charset browser writing sc/2 termcol )],
-       [qw( readline latin unicode countries emoji perl )],
-       [qw( vimperator mutt nethack mplayer font )],
-       [qw( apl less screen digits sc termcol/legacy )],
-       [qw( chars/table/html source )],
+       [qw( vi digraphs charset browser writing sc/lotv termcol dieren )],
+       [qw( readline latin unicode countries emoji perl keyboard/altgr )],
+       [qw(
+               vimperator mutt nethack mplayer/mpv font codec
+               keyboard/altgr/windows keyboard/altgr/macos
+               dieren/uitgebreid dieren/beknopt
+       )],
+       [qw(
+               apl less screen digits sc/bw sc/hots termcol/legacy mplayer
+               digraphs/xorg
+               keyboard/altgr/macos-abc keyboard/altgr/msx keyboard/altgr/ukext
+               keyboard/altgr/eurkey keyboard/altgr/apl keyboard/altgr/spacecadet
+               keyboard/altgr/ipa keyboard/altgr/boyeg keyboard/altgr/drix
+               keyboard/altgr/symbolics keyboard/altgr/msx-graph
+       )],
+       [qw( chars/table/html sample source plan )],
 );
 
 my %freq = (
@@ -31,15 +42,20 @@ for my $group (@pages) {
        state $prio = 1;
        for my $file (@{$group}) {
                (my $page = $file) =~ s/\Aindex\z//;
-               $file =~ s{/.*}{};
-               $file .= '.plp';
+               if (-e "$file.eng.inc.pl") {
+                       $file .= '.eng.inc.pl';
+               }
+               else {
+                       $file =~ s{/.*}{};
+                       $file .= '.plp';
+               }
                my $stat = stat $file or do {
                        warn "missing file $file\n";
                        next;
                };
 
                print '<url>';
-               print "<loc>http://sheet.shiar.nl/$page</loc>";
+               print "<loc>https://sheet.shiar.nl/$page</loc>";
                printf '<changefreq>%s</changefreq>', $freq{$page} // 'monthly';
                printf '<priority>%.2f</priority>', $prio;
                printf '<lastmod>%s</lastmod>', localtime($stat->mtime)->date;
index 8cfe446a2b63e295bc5b714e170e894226d2561b..b14539a444f9ac750c93971885c7657d338d5f9f 100755 (executable)
@@ -7,7 +7,7 @@ use Font::TTF::Font;
 use Getopt::Long;
 use Cwd 'abs_path';
 
-our $VERSION = '1.01';
+our $VERSION = '1.02';
 
 GetOptions(\my %opt,
        'verbose|v!',
@@ -72,13 +72,13 @@ for ($outfile || ()) {
                undef
        ) for $meta{os} || ();
 
-       say "# automatically generated by $0";
-       say '+', pp(\%meta), ',';
-
        my $support = $ttf->{cmap}->find_ms->{val};
        warn scalar keys %$support, " characters read from $ttfuri\n"
                if $opt{verbose};
-       say pp(sort { $a <=> $b } keys %$support);
+       $meta{cover} = [sort { $a <=> $b } keys %$support];
+
+       say "# automatically generated by $0";
+       say '+', pp(\%meta);
 }
 
 __END__
index c751f7679907d935ef577bdfbe2eaca0e0b4c593..a43c5733c2bce9cb38e2cc92f9bef5b37797d10e 100755 (executable)
@@ -60,7 +60,7 @@ given ($browser) {
                continue;
        }
        when ('MSIE') {
-               $browser = $mobile ? 'ie_mob' : 'ie';
+               $browser = $mobile ? 'ie_mob' : 'edge';
                continue;
        }
        when ('Opera') {
@@ -79,11 +79,11 @@ given ($browser) {
                $browser = $mobile ? 'ios_saf' : 'safari';
        }
        when ('Chrome') {
-               $browser = $mobile ? 'android' : 'chrome';
+               $browser = $mobile ? 'and_chr' : 'chrome';
                s/\.\d+$// for $version;
        }
        when ('Android') {
-               $browser = 'android';
+               $browser = 'and_chr';
        }
        when ('BlackBerry') {
                $browser = 'bb';
index d728cae330f8d0445498452c7468ca553d125908..0548fc97f65f0207f81d7dbc205569b54ffef819 100755 (executable)
@@ -8,8 +8,8 @@ use Text::CSV;
 our $VERSION = '1.01';
 
 my %BROWSERID = qw(
-       IE          ie
-       Edge        ie
+       IE          edge
+       Edge        edge
        Firefox     firefox
        Safari      safari
        Safari-iPad ios_saf
@@ -24,8 +24,8 @@ my %BROWSERID = qw(
        BlackBerry  bb
 
        IEMobile    ie_mob
-       Android     android
-       Chrome-for-Android android
+       Android     and_chr
+       Chrome-for-Android and_chr
        UC-Browser  and_uc
        QQ-Browser  and_qq
        iPhone      ios_saf
index a4ca1cd1c38048367e48f8474a30e65769710e59..71d6e5fa077000cf2fb40541d3ab8ab4fb6cf0e8 100755 (executable)
@@ -4,13 +4,13 @@ use warnings;
 
 use Data::Dump 'pp';
 
-our $VERSION = '1.00';
+our $VERSION = '1.03';
 
 my %BROWSERID = qw(
-       IE                      ie
+       IE                      edge
        IE-Mobile               ie_mob
-       Edge                    ie
-       Edge-Mobile             ie
+       Edge                    edge
+       Edge-Mobile             edge
        Firefox                 firefox
        Firefox-Mobile          and_ff
        Safari                  safari
@@ -18,13 +18,16 @@ my %BROWSERID = qw(
        Mobile-Safari-UIWebView ios_saf
        Chrome                  chrome
        Chromium                chrome
-       Chrome-Mobile           android
-       Chrome-Mobile-iOS       android
-       Android                 android
+       Chrome-Mobile           and_chr
+       Chrome-Mobile-iOS       and_chr
+       Android                 and_chr
        Opera                   opera
        Opera-Mini              op_mini
        BlackBerry-WebKit       bb
        UC-Browser              and_uc
+       Samsung-Internet        samsung
+       Google                  0
+       Other                   0
 );
 
 my %count = (
@@ -32,15 +35,28 @@ my %count = (
        -site   => 'https://analytics.wikimedia.org/',
 );
 
-my $recent = qr/^2018-/;
-
 (readline =~ y/\t//) == 3 or die "unexpected amount of columns in header\n";
+my @lines = readline;
+
+my $recent;  # minimum date to include
+for (reverse @lines) {
+       my ($date) = /(\S+)/;
+       $recent ne $date or next if $recent;  # same day
+       $recent = $date;  # override older date
+       last if state $i++ >= 2;  # repeat twice
+}
 
-while (my $row = readline) {
+for my $row (@lines) {
+       $row =~ s/\r?\n\z//;
        my ($date, $name, $version, $pct) = split /\t/, $row;
-       $date =~ $recent or next;
+       $date ge $recent or next;
        $name =~ y/ /-/;
-       my $browser = $BROWSERID{$name} or next;
+       my $browser = $BROWSERID{$name};
+       if (not $browser) {
+               warn "unknown browser: $name v$version ($pct)\n"
+                       unless defined $browser or $pct < .005;
+               next;
+       }
        $version =~ s/\A-\z/0/;
        $count{$browser}{$version} += $pct;
        $count{-total} += $pct;
diff --git a/tools/mkwordlist b/tools/mkwordlist
new file mode 100755 (executable)
index 0000000..7611b9f
--- /dev/null
@@ -0,0 +1,26 @@
+#!/usr/bin/env perl
+use 5.014;
+use warnings;
+
+BEGIN { push @INC, '.' }
+use Shiar_Sheet::DB;
+use open ':std' => ':encoding(utf-8)';
+my $db = Shiar_Sheet::DB->connect;
+
+say 'use utf8;';
+
+use Data::Dump 'pp';
+my %rows;
+my $lang = shift @ARGV or die "Missing language\n";
+{
+       my %filter = (lang => $lang);
+       my $cols = "ref, array_to_string(form || alt, '/'), prio, id, sub";
+       %rows = $db->select(_word => $cols, \%filter)->map_arrays;
+       defined $_->[-1] or pop @$_ for values %rows;
+       $rows{''} = [
+               (undef) x 3,
+               [$db->select(word => 'id', {cat => undef, ref => undef})->flat]
+       ];
+       say pp \%rows
+               =~ s/\\x\{([0-9A-F]+)\}/chr hex $1/ger;
+}
diff --git a/tools/mkwordthumb b/tools/mkwordthumb
new file mode 100755 (executable)
index 0000000..d79a002
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/env perl
+use 5.014;
+use warnings;
+use lib '.';
+use Shiar_Sheet::ImagePrep;
+use Shiar_Sheet::DB;
+use JSON ();
+
+our $VERSION = '1.01';
+
+my $db = Shiar_Sheet::DB->connect;
+my %filter = @ARGV ? (id => shift) : ();
+my $query = $db->select(word => '*', \%filter);
+
+while (my $row = $query->hash) {
+       my $image = Shiar_Sheet::ImagePrep->new("data/word/org/$row->{id}.jpg");
+       eval {
+               my $meta = eval { JSON->new->decode($row->{image} // '{}') }
+                       or die ["Invalid JSON metadata in image column.", $@];
+               $image->generate("data/word/32/$row->{id}.jpg", $meta->{convert});
+       } or warn "$row->{id}: @{$@}";
+}
diff --git a/tools/mkxkeysymdef b/tools/mkxkeysymdef
new file mode 100755 (executable)
index 0000000..cecf9d4
--- /dev/null
@@ -0,0 +1,41 @@
+#!/usr/bin/env perl
+use 5.014;
+use warnings;
+use utf8;
+use re '/mnsx';
+use JSON;
+
+our $VERSION = '1.01';
+
+my (%keysym, %keyval);
+while (readline) {
+       m{
+               \A  [#]define[ ]XK_ (?<name>[a-zA-Z_0-9]+)
+               \h+ 0x(?<value>[0-9a-fA-F]+)
+               ( \h* [/][*] [\h(] U[+] (?<unicode>[0-9A-F]{4,6}) )?
+       } or next;
+       my $cp = $+{unicode} // $keyval{ $+{value} } or next;
+       $keysym{ $+{name} } = chr hex $cp;
+       $keyval{ $+{value} } = $cp;
+}
+
+print JSON->new->ascii->canonical->indent->encode(\%keysym);
+
+__END__
+
+=head1 NAME
+
+mkxkeysymdef - Map Xorg key symbol names to Unicode characters
+
+=head1 SYNOPSIS
+
+    mkxkeysymdef /usr/incnlude/X11/keysymdef.h >keysymdef.json
+
+=head1 AUTHOR
+
+Mischa POSLAWSKY <perl@shiar.org>
+
+=head1 LICENSE
+
+Licensed under the GNU Affero General Public License version 3.
+
diff --git a/tools/perlinc-static b/tools/perlinc-static
deleted file mode 100755 (executable)
index 4edf5ac..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env perl
-use 5.014;
-use warnings;
-
-use Data::Dumper;
-
-my @data = do $ARGV[0] or die $@;
-print Data::Dumper->new([\@data])->Terse(1)->Quotekeys(0)->Indent(1)->Dump;
diff --git a/tools/word.pg.sql b/tools/word.pg.sql
new file mode 100644 (file)
index 0000000..9f972dc
--- /dev/null
@@ -0,0 +1,96 @@
+CREATE TABLE login (
+       username   text        NOT NULL UNIQUE,
+       pass       text,
+       email      text,
+       fullname   text,
+       editlang   text[],
+       id         integer     NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
+);
+
+CREATE TABLE word (
+       form       text,
+       alt        text[],
+       lang       text                 DEFAULT 'en',
+       cat        integer              REFERENCES word (id),
+       ref        integer              REFERENCES word (id),
+       prio       smallint             DEFAULT '1'
+                                       CHECK (prio >= 0 OR ref IS NOT NULL),
+       grade      integer,
+       cover      boolean     NOT NULL DEFAULT FALSE,
+       image      jsonb                CHECK (image->>'source' ~ '^https?://'
+                                          AND jsonb_typeof(image->'convert') = 'array'),
+       wptitle    text,
+       story      text,
+       creator    integer              REFERENCES login (id),
+       created    timestamptz          DEFAULT now(),
+       updated    timestamptz,
+       id         integer     NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
+);
+
+COMMENT ON COLUMN word.form       IS 'preferred textual representation';
+COMMENT ON COLUMN word.alt        IS 'alternate forms with equivalent meaning';
+COMMENT ON COLUMN word.lang       IS 'ISO 639 language code matching wikipedia subdomain';
+COMMENT ON COLUMN word.cat        IS 'primary hierarchical classification';
+COMMENT ON COLUMN word.ref        IS 'reference to equivalent en translation';
+COMMENT ON COLUMN word.prio       IS 'difficulty level or importance; lower values have precedence';
+COMMENT ON COLUMN word.grade      IS 'ascending hierarchical order, preceding default alphabetical';
+COMMENT ON COLUMN word.cover      IS 'highlight if selected';
+COMMENT ON COLUMN word.image      IS 'metadata of illustrations, including downloaded URI and ImageMagick convert options';
+COMMENT ON COLUMN word.wptitle    IS 'reference Wikipedia article';
+COMMENT ON COLUMN word.story      IS 'paragraph defining or describing the entity, wikipedia intro';
+COMMENT ON COLUMN word.updated    IS 'last significant change';
+COMMENT ON COLUMN word.creator    IS 'user responsible for initial submit';
+
+CREATE TABLE kind (
+       word       integer     NOT NULL REFERENCES word (id),
+       cat        integer     NOT NULL REFERENCES word (id),
+                                       UNIQUE (word, cat),
+       truth      smallint    NOT NULL DEFAULT '50',
+       creator    integer              REFERENCES login (id),
+       created    timestamptz          DEFAULT now(),
+       updated    timestamptz,
+       id         integer     NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
+);
+
+COMMENT ON COLUMN kind.truth      IS 'link validity ranging from 0 (never) to 100 (always)';
+
+CREATE OR REPLACE VIEW _word_ref AS
+       SELECT
+               r.form, r.alt, r.lang,
+               coalesce(r.cat,     w.cat    ) cat, --TODO translate w?
+               coalesce(r.ref,     r.id     ) "ref",
+               coalesce(r.prio,    w.prio   ) prio,
+               coalesce(r.grade,   w.grade  ) grade,
+               coalesce(r.cover,   w.cover  ) cover,
+               coalesce(r.image,   w.image  ) image,
+               coalesce(r.wptitle, w.wptitle) wptitle,
+               coalesce(r.story,   w.story  ) story,
+               r.creator, r.created, r.updated,
+               CASE WHEN nullif(r.image, '{}') IS NOT NULL THEN r.id
+                    WHEN nullif(w.image, '{}') IS NOT NULL THEN w.id END id -- image id
+       FROM word r
+       LEFT JOIN word w ON w.id = r.ref;
+
+CREATE OR REPLACE VIEW _word_tree AS
+       WITH RECURSIVE r AS (
+               SELECT w.ref, w.lang, w.cat, w.grade, w.form, NULL::int trans
+               FROM _word_ref w
+       UNION ALL
+               SELECT r.ref, r.lang, w.cat,
+                       CASE WHEN w.lang=r.lang OR t.lang=r.lang THEN r.grade ELSE w.grade END,
+                       CASE WHEN w.lang=r.lang OR t.lang=r.lang THEN r.form ELSE w.form END,
+                       CASE WHEN w.lang=r.lang OR t.lang=r.lang THEN w.id END
+               FROM r JOIN word w ON w.id = r.cat
+                 LEFT JOIN word t ON w.id = t.ref AND t.lang = r.lang
+               WHERE r.trans IS NULL
+       )
+       SELECT ref, lang, trans cat, grade, form FROM r WHERE trans IS NOT NULL
+               ORDER BY cat, grade, form, ref;
+
+CREATE OR REPLACE VIEW _word AS
+       SELECT
+               (SELECT array_agg(coalesce(ref, id)) FROM _word_tree
+                       WHERE cat = w.ref AND lang = w.lang) sub,
+               w.*
+       FROM _word_ref w
+       ;
diff --git a/tools/wordpairs b/tools/wordpairs
new file mode 100755 (executable)
index 0000000..9ba87f4
--- /dev/null
@@ -0,0 +1,13 @@
+#!/usr/bin/env perl
+use 5.014;
+use warnings;
+use lib '.';
+use Shiar_Sheet::DB;
+use Data::Dump 'pp';
+
+my $db = Shiar_Sheet::DB->connect;
+my $query = $db->select(
+       'word w JOIN word a ON w.id = a.ref' => 'w.id, a.id',
+       {'a.lang' => undef}
+);
+say pp($query->map or exit 1);
index 01d537af3c1dcad071cdd6ffdaf5fed78ff102b0..d1875ef12e4a92ca0848e09b8c8339aee65aa12d 100644 (file)
@@ -15,7 +15,7 @@ punctuation => {
                ". \x{2003} \x{2002} \x{2007}  \x{A0} \x{2009}  \x{200B} \x{200C}",  # spaces
                # em en figure  nobreak hyphen  break joiner
        ],
-       version  => [qw{. α β}],
+       version  => [qw{. α β . ℵ ℶ }],
 },
 
 symbols => {
@@ -26,7 +26,7 @@ symbols => {
 #              qw{. 🔔 ❀ -⛨ 🌰 }, # bells, roses, shields, acorns
 #              qw{. ⚚ ⚘ ⚔ ⚒ }, # merchant, clergy, nobility, peasant
 #              qw{. ❂ 🏆 ⚔ -🔧 }, # coins, cups, swords, clubs
-               qw{. ▽̶ ▽ △ △̶ }, # earth, water, fire, air
+               qw{. 🜃 🜄 🜂 🜁 }, # earth, water, fire, air
                qw{. ☷ ☵ ☲ ☰ }, # earth, water, fire, heaven [cq ☴ wind]
                qw{. 土 水 火 金 }, # earth, water, fire, metal
        ],
@@ -60,7 +60,7 @@ keys => {
        player   => [qw{. ► ⏩ ⏭ ◼ ⚫ . ◄ ⏪ ⏮ ⏏ -❚❚ }], # play, fast, skip, stop, record
        android  => [qw{. ↩ ⌂ ❐ ⋮ . 🔍 -⌽ 📞 🔊 }], # back, home, windows, menu; search, power, receive, sound
        android4 => [qw{. ◁ ⚪ ◻ ⋮ }], # back, home, recent, menu
-       ps       => [qw{. ○ ☓ □ △ }], # circle, cross, square, triangle
+       ps       => [qw{. ○ ☓ □ △ . ⦾ ⮾ 🟗 🟕 }], # circle, cross, square, triangle
        gamepad  => [qw{. Ⓐ Ⓑ ◭ ◮ }], # A, B, L1, R1
 },
 
@@ -84,11 +84,11 @@ arrows => {
        double   => [qw{. ⇖ ⇑ ⇗ ⇔ . ⇐ - ⇒ ⇕ . ⇙ ⇓ ⇘ - }],
        white    => [qw{. ⬁ ⇧ ⬀ ⬄ . ⇦ - ⇨ ⇳ . ⬃ ⇩ ⬂ - }],
        black    => [qw{. ⬉ ⬆ ⬈ ⬌ . ⬅ - ➡ ⬍ . ⬋ ⬇ ⬊ - }],
+       triangle => [qw{. ⭦ ⭡ ⭧ ⭤ . ⭠ - ⭢ ⭥ . ⭩ ⭣ ⭨ - }],
        block    => [qw{. ◩ ⬒ ⬔   . ◧ □ ◨   . ⬕ ⬓ ◪   }],
        blacktri => [qw{. ◤ ▲ ◥   . ◀ ◆ ▶   . ◣ ▼ ◢   }],
        whitetri => [qw{. ◸ △ ◹   . ◁ ◇ ▷   . ◺ ▽ ◿   }],
-       -large   => [qw{.      .  -    .      }],
-       -heavy   => [qw{.      .  -    .      }],
+       barb     => [qw{. 🡬 🡩 🡭   . 🡨 - 🡪   . 🡯 🡫 🡮   }],
        dash     => [qw{. ⇢ ⇣ . ⇡ ⇠ }],
        twohead  => [qw{. ↠ ↡ . ↟ ↞ }],
        frombar  => [qw{. ↦ ↧ . ↥ ↤ }],
@@ -131,6 +131,7 @@ block => {
        fill4    => [qw{. ▘ ▀ ▝ . ▌ █ ▐ . ▖ ▄ ▗ }],
        fill4i   => [qw{. ▛ ▚ ▟ . ▙ ▞ ▜ }],
        fill8    => [qw{. ▁ ▂ ▃ ▄ ▅ ▆ ▇ ▔ . ▏ ▎ ▍ ▌ ▋ ▊ ▉ ▕ }],
+       colour   => [qw{. 🟫 🟥 🟧 🟨 🟩 🟦 🟪}],
 },
 
 latin => {
@@ -181,19 +182,19 @@ ipa => {
                .>Voiced_Plosive            b  -ȸ >  >  d  ɖ  ɟ  ɡ  ɢ  =  =  =
                .>Nasal                     m  ɱ  >  >  n  ɳ  ɲ  ŋ  ɴ  =  =  =
                .>Trill                     ʙ  -  >  >  r  -  -  =  ʀ  -  -я =
-               .>Tap/flap                 -ⱳ  ⱱ  >  >  ɾ  ɽ  -  =  ɢ̆  >  -ʡ̯ =
+               .>Tap/flap                 -ⱳ  ⱱ  >  >  ɾ  ɽ  - -ɡ̆  ɢ̆  >  -ʡ̯ =
                .>Fricative                 ɸ  f  θ  s  ʃ  ʂ  ç  x  χ  ħ  ʜ  h
                .>Voiced_fricative          β  v  ð  z  ʒ  ʐ  ʝ  ɣ  ʁ  ʕ  ʢ  ɦ
-               .>Lateral_fricative         =  =  >  >  ɬ  ꞎ  - -Ɬ  -  =  =  =
-               .>Voiced_lateral_fricative  =  =  >  >  ɮ -ɮ̢  -  -  -  =  =  =
+               .>Lateral_fricative         =  =  >  >  ɬ  ꞎ -𝼆 -Ɬ  -  =  =  =
+               .>Voiced_lateral_fricative  =  =  >  >  ɮ -𝼅  -  -  -  =  =  =
                .>Approximant               -  ʋ  >  >  ɹ  ɻ  j  ɰ  -  -ʕ̞ =  =
                .>Lateral_approximant       =  = -l̪  >  l  ɭ  ʎ  ʟ -ʟ̠  =  =  =
                .>Click                     ʘ  -  ǀ  ǁ  ǃ -‼  ǂ -ʞ  -  =  =  =
                .>Implosive                 ɓ  ɗ̪  >  >  ɗ -ᶑ  ʄ  ɠ  ʛ  =  =  =
                .>Articulation              ʷ  ᶹ   ̪   ͇  -  ˞  ʲ  ˠ   ̴  >  ˤ  ʰ
        }], #TODO: > Labial  > > > Coronal  > Dorsal  > > Laryngeal
-       consco => [qw{
-               co:coarticulated .>sç ɕ .>zʝ ʑ .>ʃx ɧ .>jʷ ɥ .>lˠ ɫ .>hw̥ ʍ .>ɰʷ w
+       consco => [chr(865), qw{
+               co:coarticulated .>sç ɕ .>zʝ ʑ .>ʃx ɧ .>jʷ ɥ .>lˠ ɫ .>hw̥ ʍ .>ɰʷ w
        }],
        vowels => [(
                '-',
@@ -229,6 +230,22 @@ ipa => {
                . ̆ ˑ ː
                . ‿ | ‖
        }],
+       diacritics => [
+               '.' => (map chr, 809, 781, 815, 785), # syllabic
+               '.' => (map chr, '>', 'ʰ', '>', 794), # aspirated
+               '.' => (map chr, 771, 'ⁿ', 734, 'ˡ'), # nasal/rhotic/lateral
+               '.' => (map chr, '>', 812, 805, 778), # voiced
+               '.' => (map chr, '>', 804, '>', 816), # breathy
+               '.' => (map chr, 810, 838, '>', 828), # dental
+               '.' => (map chr, '>', 826, '>', 827), # apical
+               '.' => (map chr, 799,6856, 800, 772), # advanced
+               '.' => (map chr, '>', 776, 829,7498), # centralized
+               '.' => (map chr, 797, 724, 798, 725), # raised
+               '.' => (map chr, 825, 855, 796, 849), # rounding
+               '.' => (map chr, '>', 'ʷ', '>', 'ʲ'),
+               '.' => (map chr, 'ˠ', 'ˤ', '>', 820), # velar/pharyngeal
+               '.' => (map chr, 792, '꭪', 793, '꭫'), # tounge root
+       ],
 },
 
 japanese => {
index 7949edcdfd7763214b90e7d27631f88249cd67aa..a7d545992ac5247af24276413ddf29df8db3098e 100644 (file)
@@ -2,7 +2,7 @@
 
 Html({
        title => 'unicode glyph cheat sheet',
-       version => '1.2',
+       version => '1.4',
        description => [
                "Common Unicode characters with digraph or code point, layed out for quick location.",
                "Includes general symbols, arrows, drawing characters, and IPA letters.",
@@ -73,7 +73,7 @@ my @config = qw(
                        control
                        command
                        android=0
-                       ps
+                       ps=0
                        ?player
        Mathematics
                math/size
@@ -85,11 +85,11 @@ my @config = qw(
                        double
                        white
                        black
+                       triangle
+                       barb
                        block
                        blacktri
                        whitetri
-                       ?-large
-                       ?-heavy
        Line_drawing
                lines/double
                        doubleh
@@ -107,6 +107,7 @@ my @config = qw(
                        fill4
                        fill4i
                        fill8
+                       colour
        IPA
                ipa/cons
                        consco
@@ -128,7 +129,7 @@ splice @config, 4, 2, qw(
 
 $_ and m{/*+(.+)} and @config = split /[ ]/, $1 for $Request, $get{q};
 
-my $tables = do 'unicode-table.inc.pl' or die $@ || $!;
+my $tables = Data('unicode-table');
 
 $glyphs->print(map {
        $_ = /(.*)\?(.*)/ ? ($verbose ? $2 : $1) : $_;
@@ -144,11 +145,11 @@ $glyphs->print(map {
                $group = $1 if s{^([^/]+)/}{};
                my @select = s/=(.*)// ? split(/=/, $1) : ();
                my $table = $tables->{$group}->{$_}
-                       or die "Unknown table specified: $group/$_";
+                       or Abort("Unknown table specified: $group/$_", 404);
 
                if (@select) {
                        my $rowlen;
-                       for ($rowlen = 1; $rowlen++; $rowlen <= $#$table) {
+                       for ($rowlen = 1; $rowlen <= $#$table; $rowlen++) {
                                last if $table->[$rowlen] =~ /\./;
                        }
                        my @cells = map {
diff --git a/vi.plp b/vi.plp
index 9a115803b2d4c8dab08aa670f54ee59f853b2a88..669f428ec7e4bb8d1ac5dc05b88e316dfe15da25 100644 (file)
--- a/vi.plp
+++ b/vi.plp
@@ -1,29 +1,3 @@
-<(common.inc.plp)><:
-
-Html({
-       title => 'vi cheat sheet',
-       version => '1.3',
-       description => [
-               "Interactive cheat sheet for vi text editors, notably Vim,",
-               "describing each key in various modes.",
-       ],
-       keywords => [qw'
-               vi vim nvi sheet cheat reference overview commands keyboard
-       '],
-       stylesheet => [qw( light dark circus mono red )],
-       keys => 1,
-});
-
-:>
-<h1>vi/vim cheat sheet</h1>
-
-<h2>normal mode (default)</h2>
-
 <:
-use Shiar_Sheet::Keyboard 2.07;
-my $info = do 'vi.eng.inc.pl' or die $@;
-my $keys = Shiar_Sheet::Keyboard->new($info);
-$keys->map($get{map}) or undef $get{map};
-$keys->print_rows($get{rows});
-$keys->print_legends(\%get);
-
+$Request = 'vi';
+Include 'keyboard.plp';
index cd165a8b92ef9e8efc9e613c72839ccb3075496d..50baa1b7ae8fa9b2a82ca1efa743bc0481f04db8 100644 (file)
@@ -1,30 +1,3 @@
-<(common.inc.plp)><:
-
-Html({
-       title => 'vimperator cheat sheet',
-       version => '1.2',
-       description => [
-               "Interactive cheat sheet for the Vimperator (or Pentadactyl)",
-               "Firefox extension, describing the function of each key.",
-       ],
-       keywords => [qw'
-               vimperator firefox pentadactyl vim sheet cheat reference overview
-               commands keyboard browser vimfx vimium cvim
-       '],
-       stylesheet => [qw( light dark circus mono red )],
-       keys => 1,
-});
-
-:>
-<h1>Vimperator cheat sheet</h1>
-
-<h2>normal mode (default)</h2>
-
 <:
-use Shiar_Sheet::Keyboard 2.07;
-my $info = do 'vimperator.eng.inc.pl' or die $@;
-my $keys = Shiar_Sheet::Keyboard->new($info);
-$keys->map($get{map}) or undef $get{map};
-$keys->print_rows($get{rows});
-$keys->print_legends(\%get);
-
+$Request = 'vimperator';
+Include 'keyboard.plp';
diff --git a/word.plp b/word.plp
new file mode 100644 (file)
index 0000000..aedca9d
--- /dev/null
+++ b/word.plp
@@ -0,0 +1,27 @@
+<(common.inc.plp)><:
+
+my ($name) = $Request ? $Request =~ m{\A(\w+)} : 'finder';
+if (-e (my $page = "word/$name.plp")) {
+       utf8::downgrade($page); # unicode filename breaks contents encoding
+       Include $page;
+}
+elsif (-e "word/$name.js") {
+       Html({
+               raw => join("\n",
+                       (map {qq(<script src="/word/$_"></script>)}
+                               'put.min.js', 'quiz.js', "$name.js",
+                       ),
+                       (map {qq(<link rel="stylesheet" type="text/css" href="/$_">)}
+                               grep {-e} "word/$name.css"
+                       ),
+               ),
+       });
+       say '<h1>Words</h1>';
+       say '<section id="quiz"></section>';
+       say "<script>new Word\u$name()</script>";
+}
+else {
+       Html();
+       say '<h1>Words</h1>';
+       die ["Page <q>$name</q> not found"];
+}
diff --git a/word/debug.css b/word/debug.css
new file mode 100644 (file)
index 0000000..a074696
--- /dev/null
@@ -0,0 +1,89 @@
+figure:before {
+       content: '';
+       position: absolute;
+       width: 100%;
+       height: 100%;
+}
+figure .debug {
+       position: absolute;
+       left: .1em;
+}
+
+/* essential = transparent white */
+.level0 > figure:before {
+       box-shadow: inset 0 0 2em #FFF;
+}
+
+/* ubiquitous = cyan */
+.level1 > figure:before {
+       box-shadow: inset 0 0 1em #0FF;
+}
+.level1 > figure figcaption {
+       background: #8FFA;
+}
+
+/* basic = green */
+.level2 > figure {
+       filter: hue-rotate(-70deg) sepia(.4) hue-rotate(70deg) saturate(2);
+}
+.level2 > figure:before {
+       box-shadow: inset 0 0 1em #6FA;
+}
+.level2 > figure figcaption {
+       background: #AFDC;
+}
+
+/* common = lime */
+.level3 > figure {
+       filter: hue-rotate(-45deg) sepia(.6) hue-rotate(45deg) saturate(2);
+}
+.level3 > figure:before {
+       box-shadow: inset 0 0 1em #2F0;
+}
+.level3 > figure figcaption {
+       background: #8F8C;
+}
+
+/* distinctive = yellow */
+.level4 > figure {
+       filter: hue-rotate(-15deg) sepia(.7) hue-rotate(15deg) saturate(2);
+}
+.level4 > figure:before {
+       box-shadow: inset 0 0 1em #FD0;
+}
+.level4 > figure figcaption {
+       background: #FE8C;
+}
+
+/* specialised = orange */
+.level5 > figure {
+       filter: hue-rotate(10deg) sepia(.8) hue-rotate(-10deg) saturate(2);
+}
+.level5 > figure:before {
+       box-shadow: inset 0 0 2em #F80;
+}
+.level5 > figure figcaption {
+       background: #FC8C;
+}
+
+/* rare = red */
+.level6 > figure {
+       filter: hue-rotate(40deg) sepia(.9) hue-rotate(-40deg) saturate(2);
+}
+.level6 > figure:before {
+       box-shadow: inset 0 0 3em #F00;
+}
+.level6 > figure figcaption {
+       background: #F88C;
+}
+
+/* invisible = grey */
+.level7 > figure {
+       filter: sepia(.9) hue-rotate(-60deg);
+}
+.level7 > figure:before {
+       box-shadow: inset 0 0 3em #804;
+}
+.level7 > figure figcaption {
+       background: #C68C;
+}
diff --git a/word/edit.plp b/word/edit.plp
new file mode 100644 (file)
index 0000000..55fbeac
--- /dev/null
@@ -0,0 +1,347 @@
+<(../common.inc.plp)><:
+
+my $editorurl = '/word/edit';
+s{\Aedit(/|\z)}{} for $Request // ();
+
+Html({
+       title => 'words cheat sheet admin',
+       version => '1.0',
+       nocache => 1,
+       raw => <<'EOT',
+<link rel="stylesheet" type="text/css" media="all" href="/word/editor.css" />
+<script src="/word/editor.js"></script>
+EOT
+});
+
+use List::Util qw( pairs pairkeys );
+use Shiar_Sheet::FormRow;
+use SQL::Abstract '1.83';
+use JSON;
+
+my $db = eval {
+       require Shiar_Sheet::DB;
+       Shiar_Sheet::DB->connect;
+} or Abort('Database error', 501, $@);
+
+my $user = eval {
+       if (defined $post{username}) {
+               $cookie{login} = EncodeURI(join ':', @post{qw( username pass )});
+       }
+       elsif (exists $fields{logout}) {
+               require CGI::Cookie;
+               if (AddCookie(CGI::Cookie->new(
+                       -name    => 'login',
+                       -value   => '',
+                       -path    => $editorurl,
+                       -expires => 'now',
+               )->as_string)) {
+                       delete $cookie{login};
+                       die "Logged out as requested\n";
+               }
+               Alert("Failed to log out", "Login cookie could not be removed.");
+       }
+
+       my $cookiedata = $cookie{login} or return;
+       my ($name, $key) = split /[:\v]/, DecodeURI($cookiedata);
+       my %rowmatch = (username => $name, pass => $key);
+       my $found = $db->select(login => '*', \%rowmatch)->hash
+               or die "Invalid user or password\n";
+
+       eval {
+               require CGI::Cookie;
+               my $httpcookie = CGI::Cookie->new(
+                       -name    => 'login',
+                       -value   => join(':', @{$found}{qw( username pass )}),
+                       -path    => $editorurl,
+               ) or die "prepared object is empty\n";
+               AddCookie($httpcookie->as_string);
+       } or Abort(["Unable to create login cookie", $@], 403);
+
+       return $found;
+} or do {
+       say '<h1>Login to edit words</h1>';
+       Alert('Access denied', $@) if $@;
+       say '<form action="?" method="post" class="inline"><ul>';
+       my $loginform = bless {%post}, 'Shiar_Sheet::FormRow';
+       say '<li>', $loginform->input(@{$_}), '</li>' for pairs (
+               username => {-label => 'User name'},
+               pass     => {-label => 'Password', type => 'password'},
+       );
+       say '<li><input type="submit" value="Login" /></li>';
+       say '</ul></form>';
+       exit;
+};
+
+my %lang = (
+       '' => ['(reference)'],
+       nl => ["\N{REGIONAL INDICATOR SYMBOL LETTER N}\N{REGIONAL INDICATOR SYMBOL LETTER L}", 'nederlands'],
+       en => ["\N{REGIONAL INDICATOR SYMBOL LETTER G}\N{REGIONAL INDICATOR SYMBOL LETTER B}", 'english'],
+       eo => [qq'<span style="color:green">\N{BLACK STAR}</span>', 'esperanto'],
+       ru => ["\N{REGIONAL INDICATOR SYMBOL LETTER R}\N{REGIONAL INDICATOR SYMBOL LETTER U}", 'русский'],
+       zh => ["\N{REGIONAL INDICATOR SYMBOL LETTER C}\N{REGIONAL INDICATOR SYMBOL LETTER N}", '中文'],
+       la => ["\N{PUSHPIN}", 'latin'],
+);
+my @wordcols = pairkeys
+my %wordcol = (
+       lang    => {-label => 'Language', -select => {
+               map { $_ => "@{$lang{$_}}" } keys %lang
+       }},
+       cat     => [{-label => 'Category'}, 'ref'],
+       ref     => {-label => 'Reference'},
+       prio    => [
+               {-label => 'Level', -select => sub {
+                       my ($row) = @_;
+                       my @enum = qw[
+                               essential ubiquitous basic common distinctive specialised rare invisible
+                       ];
+                       return {
+                               ('' => 'parent') x (defined $row->{ref}),
+                               map { $_ => $enum[$_] } 0 .. $#enum
+                       };
+               }},
+               'cover', 'grade',
+       ],
+       cover   => {-label => 'Highlighted', type => 'checkbox'},
+       grade   => {-label => 'Order', type => 'number'},
+       form    => {-label => 'Title'},
+       alt     => {-label => 'Synonyms', -multiple => 1},
+       wptitle => {-label => 'Wikipedia'},
+       source  => {-label => 'Image', -json => 'image', -src => sub {
+               return "data/word/org/$_[0]->{id}.jpg";
+       }},
+       convert => {-label => 'Convert options', -json => 'image', -multiple => 1, -src => sub {
+               return "data/word/32/$_[0]->{id}.jpg";
+       }},
+       crop32  => {-json => 'image', type => 'hidden'}, # set by javascript interface
+       story   => {-label => 'Story', type => 'textarea', hidden => 'hidden'},
+);
+
+if (my $search = $fields{q}) {
+       my %filter = $search eq '^' ? (cat => undef, ref => undef) :
+               (form => {ilike => '%'.parseinput($search).'%'});
+       my $results = $db->select(word => '*', \%filter);
+       say '<h1>Search</h1><ul>';
+       printf("<li><small>%s</small> %s %s</li>\n",
+               $_->{id}, showlink($_->{form}, "$editorurl/$_->{id}"),
+               sprintf('<img src="/%s" style="height:3ex; width:auto" />', $wordcol{convert}->{-src}->($_)) x defined $_->{image}
+       ) for $results->hashes;
+       say "</ul>\n";
+       exit;
+}
+
+my ($find) = map {{id => $_}} $fields{id} || $Request || ();
+my $row;
+if ($find) {
+       $row = $db->select(word => '*', $find)->hash
+               or Abort("Word not found", 404);
+}
+
+if (exists $get{copy}) {
+       $row = {%{$row}{ qw(prio lang cat) }};
+}
+elsif (defined $post{form}) {{
+       sub parseinput {
+               return if not length $_[0];
+               require Encode;
+               return Encode::decode_utf8($_[0]);
+       }
+
+       my $replace = $row;  # currently stored
+       $row = {};  # proposed update
+       while (my ($col, $colinfo) = each %wordcol) {
+               ref $colinfo eq 'HASH' or $colinfo = {};
+               my @val = map { parseinput($_) } $post{'@'.$col}->@*;
+               my $val = $colinfo->{-multiple} && @val ? \@val : $val[-1];
+               if (my $jsoncol = $colinfo->{-json}) {
+                       $row->{$jsoncol}->{$col} = $val;  # hash will be encoded
+                       ref $_ eq 'HASH' or $_ = decode_json($_) for $replace->{$jsoncol} // ();
+               }
+               else {
+                       $row->{$col} = $val;
+               }
+       }
+       my $imagecol = $row->{image};  # backup image subcolumns
+       while (my ($col, $val) = each %{$row}) {
+               # convert json subcolumns to database string
+               ref $val eq 'HASH' or next;
+               $val = { %{$_}, %{$val} } for $replace->{$col} // ();  # preserve unknown
+               defined $val->{$_} or delete $val->{$_} for keys %{$val};  # delete emptied
+               $row->{$col} = encode_json($val);
+       }
+
+       if (!$row->{form} and $row->{lang}) {
+               if ($row->{ref} ne 'delete') {
+                       Alert("Empty title",
+                               "Confirm removal by setting <em>Reference</em> to <q>delete</q>."
+                       );
+               }
+               else {
+                       $db->delete(word => $find);
+                       Alert("Entry removed");
+               }
+               next;
+       }
+
+       eval {
+               my %res = (returning => '*');
+               $row->{creator} = $user->{id} unless $find;
+               $row->{updated} = ['now()'];
+               my $query = $find ? $db->update(word => $row, $find, \%res) :
+                       $db->insert(word => $row, \%res);
+               $row = $query->hash;
+       } or do {
+               Alert("Entry could not be saved", $@);
+               next;
+       };
+
+       eval {
+               while (my ($lang, $val) = each %post) {
+                       my $field = $lang;
+                       $lang =~ s/^trans-// or next;
+                       $val = parseinput($val) or next;
+                       my %subrow = (
+                               ref   => $row->{id},
+                               lang  => $lang,
+                               form  => $val,
+                               prio  => undef,
+                       );
+                       $subrow{wptitle} = $1 if $subrow{form} =~ s/\h*\[(.*)\]$//; # [Link] shorthand
+                       $subrow{alt} = [split m{/}, $1] if $subrow{form} =~ s{/(\S.*)}{}; # /alternates shorthand
+                       $db->insert(word => \%subrow);
+                       delete $fields{$field};
+               }
+               return 1;
+       } or Alert('Error creating translation entries', $@);
+
+       require Shiar_Sheet::ImagePrep;
+       my $image = Shiar_Sheet::ImagePrep->new($wordcol{source}->{-src}->($row));
+       my $reimage = eval {
+               ($imagecol->{source} // '') ne ($replace->{image}->{source} // '') or return;
+               $image->download($imagecol->{source});
+       };
+       !$@ or Alert(["Source image not found", $@]);
+
+       $reimage ||= $row->{convert} ~~ $replace->{image}->{convert};  # different
+       $reimage ||= $row->{cover}   ~~ $replace->{image}->{cover};  # resize
+       $reimage++ if $fields{rethumb};  # force refresh
+       if ($reimage) {
+               eval {
+                       $image->generate($wordcol{convert}->{-src}->($row), $imagecol);
+               } or do {
+                       my ($warn, @details) = ref $@ ? @{$@} : $@;
+                       Alert([ "Thumbnail image not generated", $warn ], @details);
+               };
+       }
+}}
+else {
+       $row->{lang} //= $user->{editlang}->[0] unless exists $row->{lang};
+       $row->{$_} = $get{$_} for keys %get;
+       $row->{prio} = defined $row->{ref} ? undef : 4 unless exists $row->{prio};
+}
+
+eval {
+       my $imagerow = $row->{image} && JSON->new->decode(delete $row->{image}) || {};
+       while (my ($col, $val) = each %{$imagerow}) {
+               $row->{$col} = $val;
+       }
+       1;
+} or Alert("Error decoding image metadata", $@);
+
+my $title = $row->{id} ? "entry <small>#$row->{id}</small>" : 'new entry';
+bless $row, 'Shiar_Sheet::FormRow';
+:>
+<h1>Words <:= $title :></h1>
+
+<div class="inline">
+
+<form action="?" method="post">
+<input id="id" name="id" value="<:= $row->{id} // '' :>" type="hidden" />
+<ul>
+<:
+for my $col (@wordcols) {
+       my $info = $wordcol{$col} or next;
+       my ($attr, @span) = ref $info eq 'ARRAY' ? @{$info} : $info;
+       next if delete $attr->{hidden} and not $row->{$col};
+       my $title = ref $attr ? delete $attr->{-label} : $attr;
+       printf '<li><label for="%s">%s</label><div>', $col, $title;
+               printf '<span class=inline>';
+               print $row->input($col => $attr);
+               if (my $imgsrc = $attr->{-src}) {
+                       my $hide = $col eq 'source';
+                       printf '<figure id="%spreview">', $col unless $hide;
+                       printf('<img src="/%s" alt="%s"%s />',
+                               $_, $row->{form}, $hide && qq( id="${col}preview" hidden)
+                       ) for grep { -e } $imgsrc->($row);
+                       printf '</figure>' unless $hide;
+               }
+               print $row->input($_ => delete $wordcol{$_}) for @span;
+               print '</span>';
+       say '</div></li>';
+}
+
+if (not $row->{ref}) {
+       printf '<li><label for="%s">%s</label><div><ul class="inline multiinput" id="%1$s">',
+               'trans', 'Translations';
+       my @children = !$row->{id} ? () :
+               $db->select(word => '*', {ref => $row->{id}}, 'lang, id')->hashes;
+       while (my ($lang, $val) = each %fields) {
+               $lang =~ s/^trans-// or next;
+               push @children, { lang => $lang, form => $val };
+       }
+       my %existing = map { $_->{lang} => 1 } $row, @children;
+       $existing{$_} or push @children, { lang => $_ } for @{$user->{editlang}};
+
+       for my $ref (@children) {
+               printf(
+                       '<li><label for="%s" title="%3$s">%s </label>',
+                       "trans-$ref->{lang}", @{$lang{ $ref->{lang} }}, # flag, name
+               );
+               printf(
+                       $ref->{id} ? '<a id="%s" href="%s">%s</a></li>' :
+                       '<input id="%s" name="%1$s" value="%3$s" />',
+                       "trans-$ref->{lang}", "$editorurl/$ref->{id}", Entity($ref->{form} // ''),
+               );
+       }
+       say '</ul></div></li>';
+}
+:>
+</ul>
+<p>
+       <input type="submit" value="Save" />
+       <input type="submit" value="New" formaction="<:= $editorurl :>?copy=cat" />
+</p>
+</form>
+
+<:
+if ($row->{id}) {
+:>
+<section id="nav">
+<h2>Hierarchy</h2>
+
+<:
+say '<ul>';
+my $parents = $db->select(word => '*', [{id => $row->{cat}}, {id => $row->{ref}}]);
+while (my $ref = $parents->hash) {
+       printf '<li><a href="%s/%d">%s</a></li>', $editorurl, $ref->{id}, Entity($ref->{form});
+}
+say "<li><strong>$_</strong></li>" for Entity($row->{form});
+my $children = $db->select(word => '*', {cat => $row->{id}, ref => undef}, 'grade, id');
+while (my $ref = $children->hash) {
+       printf '<li><a href="%s/%d">%s</a></li>', $editorurl, $ref->{id}, Entity($ref->{form});
+}
+:>
+<li><form action="<:= $editorurl :>">
+       <input type="hidden" name="cat" value="<:= $row->{id} :>" />
+       <input type="hidden" name="lang" value="<:= $row->{lang} :>" />
+       <input type="submit" value="Add" />
+</form></li>
+</ul>
+
+<form id="search">
+       <input type="search" name="q" value="" placeholder="search" /><button type="submit">🔍</button>
+</form>
+</section>
+<:
+}
+:>
+</div>
diff --git a/word/editor.css b/word/editor.css
new file mode 100644 (file)
index 0000000..88a4c49
--- /dev/null
@@ -0,0 +1,137 @@
+dl {
+       display: inline-grid;
+       grid: auto-flow / min-content repeat(10, auto);
+}
+
+form > ul {
+       display: table;
+}
+form > ul > li {
+       display: table-row;
+}
+form > ul > li > * {
+       display: table-cell;
+       padding-right: .5em;
+}
+form > ul > li > label {
+       /* th */
+       text-align: right;
+}
+form > ul > li > label + * {
+       /* td */
+       width: 40em;
+}
+
+.multiinput,
+input, textarea, select {
+       box-sizing: border-box;
+       flex-grow: 1;
+}
+input:not([type=submit]), textarea {
+       padding: .4rem;
+       font-family: monospace;
+}
+input[type=number] {
+       max-width: 7em;
+}
+select {
+       padding: .3rem .2rem; /* TODO: input */
+}
+#convertpreview {
+       width: 300px;
+       height: 200px;
+       align-self: start;
+       flex-shrink: 0;
+       position: relative;
+       overflow: hidden;
+}
+
+.popup {
+       display: flex;
+       flex-wrap: wrap;
+       justify-content: space-evenly;
+       gap: 1em;
+       position: fixed;
+       left: 0;
+       top: 0;
+       max-width: 100%;
+       max-height: 100%; /* scroll */
+       margin: auto;
+       overflow: auto;
+       background: rgba(0, 0, 0, .8);
+       border: 1px solid #CCC;
+       z-index: 1;
+}
+.popup img {
+       height: 20vh;
+       width: auto;
+}
+img {
+       max-width: 100%;
+       object-fit: contain;
+}
+
+h1 {
+       margin-bottom: 1ex;
+}
+.inline {
+       display: inline-flex;
+       align-items: baseline;
+       margin: 0 -1ex; /* inner gap */
+}
+.inline > * {
+       margin: 1px 1ex;
+}
+.inline .inline {
+       display: flex;
+       margin: 0;
+}
+.inline.multiinput {
+       flex-wrap: wrap;
+       justify-content: space-between;
+}
+.inline.multiinput > :last-child {
+       text-align: right;
+       flex-grow: 1;
+}
+.multiinput > input {
+       width: 10em;
+}
+
+#nav > ul,
+#nav > ul strong,
+#nav form {
+       margin: 1ex 0;
+       display: inline-block;
+}
+
+#nav {
+       position: relative;
+}
+form#search {
+       display: block;
+       position: absolute;
+       width: 100%;
+       text-align: right;
+       top: -7ex;
+}
+#search input {
+       width: 100%;
+       transition: all .5s ease-in;
+}
+#search:not(:focus-within) input {
+       width: 0;
+       padding-left: 0;
+       padding-right: 0;
+       visibility: hidden;
+}
+#search button {
+       position: absolute;
+       right: 0;
+       height: 100%; /* like input */
+       border: 0;
+       background: none;
+       color: inherit;
+       font: inherit;
+       cursor: pointer;
+}
diff --git a/word/editor.js b/word/editor.js
new file mode 100644 (file)
index 0000000..5277402
--- /dev/null
@@ -0,0 +1,259 @@
+document.addEventListener('DOMContentLoaded', () => {
+       document.querySelectorAll('#search').forEach(p => {
+               let [input, button] = p.children;
+               button.onclick = e => {
+                       if (input.value && input.offsetWidth > 50) {
+                               return true; // bubble to submit
+                       }
+                       // make visible first
+                       input.focus();
+                       e.preventDefault();
+                       return false;
+               };
+       });
+
+       document.querySelectorAll('.multiinput > input[id]').forEach(el => {
+               el.oninput = e => {
+                       if (e.target.value == '') return;
+                       // insert another empty input element option
+                       let add = e.target.cloneNode(true);
+                       add.value = '';
+                       add.oninput = e.target.oninput;
+                       e.target.parentNode.appendChild(add);
+                       e.target.oninput = undefined;
+                       e.target.removeAttribute('id');
+               };
+       });
+
+       let wpinput = document.getElementById('wptitle');
+       if (wpinput) {
+               let wpbutton = wpinput.parentNode.appendChild(document.createElement('button'));
+               wpbutton.type = 'button';
+               wpbutton.append('Download');
+               wpbutton.onclick = () => {
+                       let wptitle = wpinput.value || document.getElementById('form').value;
+                       let wplang = document.getElementById('lang').value;
+                       if (wplang == 'la') wplang = 'en'; // most likely presence of scientific names
+                       let wpapi = `https://${wplang}.wikipedia.org/w/api.php`;
+                       let wppage = wpapi+'?action=parse&format=json&origin=*&prop=text|langlinks&page='+wptitle;
+                       fetch(wppage).then(res => res.json()).then(json => {
+                               if (json.error) throw `error returned: ${json.error.info}`;
+                               wpinput.value = json.parse.title;
+
+                               let wptext = json.parse.text['*'];
+                               let transrow = document.getElementById('trans-la');
+                               if (transrow && !transrow.value && wptext) {
+                                       const binom = wptext.match(/ class="binomial">.*?<i>(.*?)<\/i>/);
+                                       if (binom) {
+                                               transrow.value = binom[1]
+                                       }
+                               }
+
+                               // translations from language links
+                               let wplangs = json.parse.langlinks;
+                               if (wplangs) wplangs.forEach(wptrans => {
+                                       let transrow = document.getElementById('trans-' + wptrans.lang);
+                                       if (!transrow || transrow.value) return;
+                                       transrow.value = wptrans['*'].replace(/([^,(]*).*/, (link, short) => {
+                                               return short.toLocaleLowerCase(wptrans.lang).trimEnd() + ' [' + link + ']';
+                                       });
+                               });
+
+                               // copy first paragraph to story
+                               let storyinput = document.getElementById('story');
+                               if (storyinput && !storyinput.value && wptext) {
+                                       storyinput.value = wptext
+                                               .replace(/<h2.*/s, '') // prefix
+                                               .replace(/<table.*?<\/table>/sg, '') // ignore infobox
+                                               .match(/<p>(.*?)<\/p>/s)[0] // first paragraph
+                                               .replace(/<[^>]*>/g, '') // strip html tags
+                               }
+
+                               // list images in article html
+                               let imginput = document.getElementById('source');
+                               if (!imginput || imginput.value) return;
+                               let wpimages = wptext.match(/<img\s[^>]+>/g);
+                               let wpselect = wpinput.parentNode.appendChild(document.createElement('ul'));
+                               wpselect.className = 'popup';
+                               wpimages.forEach(img => {
+                                       let selectitem = wpselect.appendChild(document.createElement('li'));
+                                       selectitem.insertAdjacentHTML('beforeend', img);
+                                       selectitem.onclick = e => {
+                                               let imgsrc = e.target.src
+                                                       .replace(/^(?=\/\/)/, 'https:')
+                                                       .replace(/\/thumb(\/.+)\/[^\/]+$/, '$1');
+                                               imginput.value = imgsrc;
+                                               wpselect.remove();
+                                               return false;
+                                       };
+                               });
+                       }).catch(error => alert(error));
+                       return false;
+               };
+               wpbutton = wpinput.parentNode.appendChild(document.createElement('button'));
+               wpbutton.type = 'button';
+               wpbutton.append('Visit');
+               wpbutton.onclick = () => {
+                       let wptitle = wpinput.value || document.getElementById('form').value;
+                       let wplang = document.getElementById('lang').value;
+                       let wpurl =
+                               wplang == 'la' ? `https://species.wikimedia.org/wiki/${wptitle}` :
+                               `https://${wplang}.wikipedia.org/wiki/${wptitle}`;
+                       window.open(wpurl, 'sheet-wikipedia').focus();
+                       return false;
+               };
+       }
+
+       let imgpreview = document.getElementById('sourcepreview');
+       if (imgpreview) {
+               let imginput = document.getElementById('source');
+               imginput.parentNode.parentNode.append(imgpreview); // separate row
+               let previewbutton = imginput.parentNode.appendChild(document.createElement('button'));
+               previewbutton.type = 'button';
+               previewbutton.append('View');
+               previewbutton.onclick = () => {
+                       previewbutton.childNodes[0].nodeValue = imgpreview.hidden ? 'Hide' : 'View';
+                       imgpreview.hidden = !imgpreview.hidden;
+               };
+       }
+
+       let thumbpreview = document.getElementById('convertpreview');
+       if (thumbpreview && imgpreview) {
+               thumbpreview.onclick = e => {
+                       thumbpreview.onclick = null; // setup once
+                       const cropinput = document.getElementById('crop32');
+                       const imgselect = thumbpreview.children[0];
+                       const canvas = [thumbpreview.clientWidth, thumbpreview.clientHeight];
+                       const border = [canvas[0], canvas[0] * imgpreview.height / imgpreview.width];
+                       const minscale = Math.max(1, canvas[1] / border[1]); // 100% or fit width
+                       let crop = cropinput.value.split(/[^0-9.]/).map(pos => pos / 1000);
+                       let scale = 1 / (crop[2] - crop[0]) || minscale;
+                       crop.push(0); // defined y dimension
+                       crop.splice(2); // end coordinates applied to zoom
+                       crop = crop.map((rel, axis) => rel * border[axis % 2] * scale);
+
+                       let drag, pinch;
+                       function applydrag(e) {
+                               const touch = e.touches ? e.touches[0] : e;
+                               let pos = [touch.pageX, touch.pageY];
+                               if (e.type === 'touchmove' && e.touches.length > 1) {
+                                       // distance to second point
+                                       pos[0] -= e.touches[1].pageX;
+                                       pos[1] -= e.touches[1].pageY;
+                                       const span = Math.sqrt(pos[0]**2 + pos[1]**2);
+                                       if (pinch) {
+                                               cropzoom(.01 * (span - pinch));
+                                       }
+                                       pinch = span;
+                                       return;
+                               }
+                               if (drag) {
+                                       // apply drag delta to crop position
+                                       crop[0] += drag[0] - pos[0];
+                                       crop[1] += drag[1] - pos[1];
+                                       recrop();
+                               }
+                               drag = pos;
+                       }
+
+                       function recrop() {
+                               [0, 1].forEach(axis => {
+                                       if (crop[axis] > border[axis] * scale - canvas[axis]) {
+                                               crop[axis] = border[axis] * scale - canvas[axis]; // max bound
+                                       }
+                                       if (crop[axis] < 0) {
+                                               crop[axis] = 0; // min bound
+                                       }
+                               });
+                               imgselect.style.left = -crop[0]+'px';
+                               imgselect.style.top  = -crop[1]+'px';
+                               imgselect.style.width = (scale * 100)+'%';
+                               cropinput.value = [
+                                       crop[0] / border[0],
+                                       crop[1] / border[1],
+                                       (crop[0] + canvas[0]) / border[0],
+                                       (crop[1] + canvas[1]) / border[1],
+                               ].map(pos => Math.round(1000 * pos / scale));
+                       }
+
+                       function cropzoom(delta) {
+                               if (scale + delta < minscale) {
+                                       delta = minscale - scale; // scale = 1
+                               }
+                               [0, 1].forEach(axis => {
+                                       // same area center at altered scale
+                                       crop[axis] += (crop[axis] + border[axis] / 2) / scale * delta;
+                               });
+                               scale += delta;
+                               recrop();
+                       }
+
+                       imgselect.src = imgpreview.src;
+                       imgselect.style.cursor = 'grab';
+                       imgselect.style.position = 'absolute';
+                       imgselect.style.maxWidth = 'none';
+                       recrop();
+
+                       imgselect.ontouchstart =
+                       imgselect.onmousedown = e => {
+                               e.preventDefault();
+                               drag = pinch = false;
+                               applydrag(e);
+                               imgselect.style.cursor = 'grabbing';
+                               window.ontouchmove =
+                               window.onmousemove = e => {
+                                       e.preventDefault();
+                                       applydrag(e);
+                               };
+                               window.ontouchend =
+                               window.onmouseup = e => {
+                                       e.preventDefault();
+                                       imgselect.style.cursor = 'grab';
+                                       window.ontouchmove = window.ontouchend =
+                                       window.onmousemove = window.onmouseup = null;
+                               };
+                       };
+
+                       imgselect.onwheel = e => {
+                               e.preventDefault();
+                               let delta = (-e.deltaY || e.wheelDelta) * .001 * scale;
+                               if (e.deltaMode == 1) { // DOM_DELTA_LINE
+                                       delta *= 18; // convert number of lines to pixels
+                               }
+                               cropzoom(delta);
+                       };
+               };
+       }
+
+       let translist = document.getElementById('trans');
+       if (translist) {
+               let langoptions = Array.prototype.filter.call(document.getElementById('lang').options, opt => {
+                       if (document.getElementById('trans-' + opt.value)) return;
+                       if (document.getElementById('lang').value == opt.value) return;
+                       return true;
+               });
+               if (!langoptions.length) return;
+
+               let transadd = translist.appendChild(document.createElement('li'));
+               let transselect = transadd.appendChild(document.createElement('select'));
+               transselect.appendChild(document.createElement('option'));
+               for (let langoption of langoptions) {
+                       let transoption = document.createElement('option');
+                       transoption.value = langoption.value;
+                       transoption.append(langoption.label);
+                       transselect.appendChild(transoption);
+               }
+               transselect.onchange = e => {
+                       let inputlang = e.target.selectedOptions[0];
+                       let transadded = translist.insertBefore(document.createElement('li'), transadd);
+                       let translabel = transadded.appendChild(document.createElement('label'));
+                       translabel.append(inputlang.label.replace(/ (.+)/, ' ')); //TODO title = $1
+                       let transinput = transadded.appendChild(document.createElement('input'));
+                       transinput.name = 'trans-'+inputlang.value;
+                       translabel.setAttribute('for', transinput.id = transinput.name);
+                       inputlang.remove();
+                       if (e.target.length <= 1) e.target.remove();
+                       transinput.focus();
+               };
+       }
+});
diff --git a/word/finder.js b/word/finder.js
new file mode 100644 (file)
index 0000000..fffc552
--- /dev/null
@@ -0,0 +1,67 @@
+class WordFinder extends WordQuiz {
+       add(catitem, rows) {
+               rows.forEach(word => {
+                       if (!word) return;
+                       const worditem = put(catitem, 'li');
+                       const figitem = put(worditem, 'figure');
+                       if (word.imgid) {
+                               put(figitem, 'img[src=$]', word.thumb());
+                       }
+                       if (word.title) {
+                               put(figitem, 'figcaption', {
+                                       innerHTML: word.html,
+                               });
+                       }
+                       if (this.preset.debug) {
+                               put(figitem, '[title=$]', `id ${word.id} level ${word.level}`);
+                       }
+                       put(worditem, '.level' + word.level);
+                       if (!word.subs.length) {
+                               return;
+                       }
+                       if (word.level <= 1 && word.subs.length >= 4) {
+                               put(worditem, '.large');
+                       }
+                       if (true) {
+                               // delve into subcategory
+                               put(worditem, '.parent');
+                               const expansion = put(worditem, 'ul');
+                               this.add(expansion, word.subs);
+                       }
+
+                       // hide or reselect subcategories
+                       put(figitem, '[data-sup=$]', word.subs.length);
+                       figitem.onclick = () => {
+                               let expansion;
+                               if (expansion = worditem.querySelector('ul')) {
+                                       put(expansion, '!');
+                                       put(worditem, '.expand');
+                                       return;
+                               }
+                               expansion = put(worditem, 'ul');
+                               this.add(expansion, word.subs);
+                               put(worditem, '!expand');
+                       };
+               });
+       }
+
+       configure(input) {
+               this.preset.level = 1;
+               this.preset.images = false;
+               return super.configure(input);
+       }
+
+       setup() {
+               super.setup();
+               if (this.preset.debug) {
+                       put(document.head, 'link', {rel: 'stylesheet', href: '/word/debug.css'});
+               }
+               this.form.innerHTML = '';
+               put(this.form, 'p', 'Under construction.');
+               for (let cat of this.data.root()) {
+                       this.add(put(this.form, 'ul.gallery'), [cat]);
+               }
+       }
+
+       stop() {}
+};
diff --git a/word/memory.css b/word/memory.css
new file mode 100644 (file)
index 0000000..314e036
--- /dev/null
@@ -0,0 +1,62 @@
+/* cards */
+#quiz {
+       display: grid;
+       grid: auto / repeat(6, 1fr);
+       grid-gap: 1ex;
+}
+html {
+       overflow: hidden; /* rotation overflow on celebration */
+}
+
+figure {
+       background: #224;
+       border: 1px solid #888;
+       perspective: 100em;
+}
+figure:not(.turn):hover {
+       cursor: pointer;
+}
+figure, img {
+       transition: all .5s ease-in;
+}
+
+/* card faces */
+figure img {
+       backface-visibility: hidden;
+       -webkit-backface-visibility: hidden;
+       transform: rotateY(180deg); /* back */
+       transform-style: preserve-3d;
+       float: left; /* ff workaround to prevent click selection */
+       height: 100%;
+       object-fit: contain; /* center */
+}
+figure.mirror img {
+       transform: rotateY(180deg) scaleX(-1);
+}
+
+/* turn results */
+figure.turn img {
+       transform: rotateY(0deg);
+}
+figure.mirror.turn img {
+       transform: rotateY(0deg) scaleX(-1);
+}
+figure.bad img {
+       filter: sepia(.5) hue-rotate(-45deg) saturate(2); /* red tint */
+}
+figure.good {
+       opacity: .8;
+}
+
+.good figure {
+       animation: celebration 7s linear infinite;
+       background: none;
+       border: 0;
+       opacity: 1;
+}
+@keyframes celebration {
+       0% { filter: hue-rotate(0deg); transform: rotate(0deg) }
+       33% { filter: hue-rotate(180deg); transform: rotate(180deg) }
+       66% { filter: hue-rotate(360deg); transform: rotate(360deg) }
+       100% { filter: hue-rotate(360deg); transform: rotate(360deg) }
+}
diff --git a/word/memory.js b/word/memory.js
new file mode 100644 (file)
index 0000000..b539b2d
--- /dev/null
@@ -0,0 +1,92 @@
+class WordMemory extends WordQuiz {
+       turn(click) {
+               let target = click.currentTarget;
+               if (!target.classList.contains('turn')) {
+                       // show an open card
+                       this.turned.push(target);
+                       put(target, '.turn');
+                       this.log('pick', target.id, target.index);
+               }
+               else if (this.turned.length < 2) {
+                       return; // keep open
+               }
+
+               if (this.turned.length <= 1) {
+                       return; // first choice
+               }
+
+               // compare two cards
+               let match = !this.pairs ? this.turned[0].id == this.turned[1].id : (
+                       this.pairs[this.turned[0].id] == this.turned[1].id
+                       || this.pairs[this.turned[1].id] == this.turned[0].id
+               );
+               if (!match && !this.turned[0].classList.contains('bad')) {
+                       put(this.turned[0], '.bad'); // indicate failure on first card
+                       return;
+               }
+
+               if (match) {
+                       // lock both as correct
+                       this.turned.forEach(card => put(card, '.good![onclick]'));
+                       this.turned = [];
+                       if (Array.from(this.form.children).every(card => card.classList.contains('good'))) {
+                               put(this.form, '.good');
+                               this.stop('done');
+                       }
+                       return;
+               }
+
+               // fold back earlier cards
+               this.turned.splice(0, 2)
+               .forEach(card => put(card, '!.turn!.bad'));
+       }
+
+       load() {
+               this.configure();
+               if (this.preset.pairs) {
+                       this.dataurl = '/data/wordpairs.json';
+                       fetch(this.dataurl).then(res => res.json()).then(pairs => {
+                               this.pairs = pairs;
+                               this.setup();
+                       });
+               }
+               else {
+                       super.load();
+               }
+       }
+
+       setup() {
+               super.setup();
+               this.turned = [];
+               this.form.innerHTML = '';
+               this.form.className = '';
+
+               let cards;
+               if (this.words) {
+                       const aspect = this.form.clientWidth / window.innerHeight;
+                       //TODO image ratio
+                       let count = parseInt(this.preset.n) || 35;
+                       let cols = Math.round(Math.sqrt(count) * aspect**.5);
+                       count = cols * Math.ceil(count / cols);
+                       this.form.style['grid-template-columns'] = `repeat(${cols}, 1fr)`;
+                       cards = this.words.splice(0, count>>1).map(row => row.imgid);
+                       cards.push(...cards.map(val => -val));
+               }
+               else {
+                       cards = Object.entries(this.pairs).flat()
+                               .map(e => e.toString())
+               }
+
+               cards.shuffle().forEach((word, seq) => {
+                       let ref = Math.abs(word);
+                       put(this.form,
+                               'figure>img[src=$]<', `/data/word/32/${ref}.jpg`, {
+                                       onclick: e => this.turn(e),
+                                       id: ref,
+                                       className: word < 0 ? 'mirror' : '',
+                                       index: seq,
+                               }
+                       );
+               });
+       }
+};
diff --git a/word/multichoice.css b/word/multichoice.css
new file mode 100644 (file)
index 0000000..15c3023
--- /dev/null
@@ -0,0 +1,15 @@
+img {
+       width: 90vw;
+       max-width: 64em;
+       margin: 2em 0 1ex;
+}
+li {
+       font-size: 20pt;
+       padding: .2ex;
+}
+li:hover {
+       cursor: pointer;
+       background: #8888;
+}
+li.wrong {background: #F008}
+li.good {background: #0F08}
diff --git a/word/multichoice.js b/word/multichoice.js
new file mode 100644 (file)
index 0000000..300a32f
--- /dev/null
@@ -0,0 +1,30 @@
+class WordMultichoice extends WordQuiz {
+       next() {
+               if (this.words.length < 4) return;
+               let word = this.words.shift();
+               let form = put(this.form,
+                       '+img[src=$]+ul', word.thumb()
+               );
+
+               let answers = [word, this.words[0], this.words[1], this.words[2]]
+                       .shuffle()
+               this.log('ask', word.id, answers.map(w => w.id));
+               answers.forEach(suggest => {
+                       let option = put(form, 'li', suggest.label, {onclick: () => {
+                               this.log('pick', suggest.id, null, word.id);
+                               if (suggest.label != word.label) {
+                                       // incorrect
+                                       put(option, '.wrong');
+                                       return;
+                               }
+                               put(option, '.good');
+                               window.setTimeout(() => this.next(), 500);
+                       }});
+               });
+       }
+
+       setup() {
+               super.setup();
+               this.next();
+       }
+};
diff --git a/word/quiz.js b/word/quiz.js
new file mode 100644 (file)
index 0000000..9436f7a
--- /dev/null
@@ -0,0 +1,184 @@
+Array.prototype.shuffle = function () {
+       for (let i = this.length - 1; i > 0; i--) {
+               const j = Math.floor(Math.random() * (i + 1)); // random index 0..i
+               [this[i], this[j]] = [this[j], this[i]]; // swap elements
+       }
+       return this;
+};
+
+function hashparams() {
+       const encodedhash = window.location.href.split('#').slice(1) || '';
+       if (encodedhash.length == 1) {
+               // location.hash is not encoded in firefox
+               return decodeURIComponent(encodedhash).split('#');
+       }
+       return encodedhash;
+}
+
+class Words {
+       constructor(data, root = undefined) {
+               this.data = data;
+               this.selection = root || this.data[''][3];
+               this.visible = new Set(root || Object.keys(data).flatMap(id => id && parseInt(id)));
+               if (root) {
+                       let children = root;
+                       for (let loop = 0; children.length && loop < 20; loop++) {
+                               for (let child of children) this.visible.add(child);
+                               children = children.map(cat => data[cat][3]).filter(is => is).flat();
+                       }
+               }
+       }
+
+       filter(f) {
+               // keep only matching entries, and root selection regardless
+               this.visible = new Set([...this.visible].filter(f).concat(this.selection));
+       }
+
+       *root() {
+               for (let i of this.selection) {
+                       if (!this.has(i)) {
+                               continue;
+                       }
+                       yield this.get(i);
+               }
+       }
+
+       *random() {
+               let order = [...this.visible.keys()].shuffle();
+               for (let i of order) {
+                       if (!this.has(i)) {
+                               continue;
+                       }
+                       yield this.get(i);
+               }
+       }
+
+       has(id) {
+               return this.visible.has(id);
+       }
+
+       subs(id) {
+               let refs = this.data[id][3];
+               if (!refs) {
+                       return [];
+               }
+               for (let ref of refs) {
+                       // retain orphaned references in grandparent categories
+                       if (!this.has(ref)) {
+                               refs = refs.concat(this.subs(ref));
+                       }
+               }
+               return refs;
+       }
+
+       get(id) {
+               if (!this.has(id)) {
+                       return;
+               }
+               const p = this;
+               const row = this.data[id];
+               return row && {
+                       id: id,
+                       title: row[0],
+                       get label() {
+                               return row[0].replace(/\/.*/, ''); // primary form
+                       },
+                       get html() {
+                               let aliases = this.title.split('/');
+                               let html = aliases.shift();
+                               html = html.replace(/\((.+)\)/, '<small>$1</small>');
+                               for (let alias of aliases) {
+                                       html += ` <small>(${alias})</small>`;
+                               }
+                               return html;
+                       },
+                       level: row[1],
+                       imgid: row[2],
+                       thumb(size = 32) {
+                               return `/data/word/${size}/${row[2]}.jpg`;
+                       },
+                       get subs() {
+                               return p.subs(id).map(e => p.get(e));
+                       },
+               };
+       }
+}
+
+class WordQuiz {
+       dataselect(json) {
+               this.data = this.datafilter(json);
+               return [...this.data.random()];
+       }
+
+       datafilter(json) {
+               // find viable rows from json data
+               const selection = new Words(json, this.preset.cat);
+
+               if (this.preset.images) {
+                       selection.filter(id => json[id][2]);
+               }
+               if (this.preset.level !== undefined) {
+                       selection.filter(id => json[id][1] <= this.preset.level);
+               }
+
+               if (this.preset.distinct) {
+                       // remove referenced categories
+                       selection.filter(id => !selection.get(id).subs.length);
+               }
+
+               return selection;
+       }
+
+       configure(params = hashparams()) {
+               const opts = new Map(params.map(arg => arg.split(/[:=](.*)/)));
+               for (let [query, val] of opts) {
+                       if (query.match(/^\d+$/)) {
+                               this.preset.cat = [parseInt(query)];
+                       }
+                       else if (query === 'level') {
+                               this.preset.level = parseInt(val);
+                       }
+                       else if (query === 'debug') {
+                               this.preset.debug = true;
+                       }
+                       else {
+                               this.preset[query] = val;
+                       }
+               }
+               this.preset.dataurl = `/data/wordlist.${this.preset.lang}.json`
+       }
+
+       setup() {
+               this.form = document.getElementById('quiz');
+       }
+
+       load() {
+               this.configure();
+               fetch(this.preset.dataurl).then(res => res.json()).then(json => {
+                       this.words = this.dataselect(json)
+                       this.setup();
+               });
+       }
+
+       log(...args) {
+               this.history.push([new Date().toISOString(), ...args]);
+       }
+
+       stop(...args) {
+               this.log(...args);
+               window.onbeforeunload = null;
+               fetch('/word/report', {method: 'POST', body: JSON.stringify(this.history)});
+       }
+
+       constructor() {
+               this.preset = {images: true, lang: 'en'};
+               this.load();
+               this.history = [];
+               window.onbeforeunload = e => {
+                       this.stop('abort');
+               };
+               window.onhashchange = e => {
+                       this.load();
+               };
+       }
+}
diff --git a/word/report.plp b/word/report.plp
new file mode 100644 (file)
index 0000000..c40b0af
--- /dev/null
@@ -0,0 +1,12 @@
+<:
+my $db = eval {
+       require Shiar_Sheet::DB;
+       Shiar_Sheet::DB->connect;
+} or Abort('Database error', 501, $@);
+
+$db->insert(report => {
+       agent   => $ENV{HTTP_USER_AGENT},
+       page    => $ENV{HTTP_REFERER},
+       address => $ENV{REMOTE_ADDR},
+       history => (map {$_ ? $PLP::read->($_) : undef} $ENV{CONTENT_LENGTH}),
+});
diff --git a/word/wijzer.css b/word/wijzer.css
new file mode 100644 (file)
index 0000000..445b6f2
--- /dev/null
@@ -0,0 +1,40 @@
+#quiz {
+       display: grid;
+       grid: auto / 2fr 1fr;
+       margin: 2em 0 1ex;
+       align-items: center;
+       grid-gap: 0 1ex;
+}
+figure {
+       grid-column: 1;
+       grid-row-start: 1;
+       grid-row-end: span 9;
+}
+ul {
+       display: contents;
+}
+li {
+       grid-column: 2;
+       font-size: 20pt;
+       padding: 1ex .2ex;
+       transition: all .5s ease-in;
+}
+li.chosen {background: #FF08}
+.done li.wrong {background: #F008}
+.done li.good {background: #0F08}
+li[onclick]:hover {
+       cursor: pointer;
+       background: #8888;
+}
+
+@media (orientation: portrait) {
+       #quiz {
+               grid: auto / 1fr 1fr;
+       }
+       figure {
+               grid-column: span 2;
+       }
+       li {
+               grid-column: auto;
+       }
+}
diff --git a/word/wijzer.js b/word/wijzer.js
new file mode 100644 (file)
index 0000000..7ae3d3e
--- /dev/null
@@ -0,0 +1,49 @@
+class WordWijzer extends WordQuiz {
+       next() {
+               let word = this.words.shift();
+               if (!word) {
+                       put(this.form, '.done');
+                       this.form.querySelectorAll('li[onclick]').forEach(answer => {
+                               answer.removeAttribute('onclick');
+                       });
+                       this.log('done');
+                       return;
+               }
+
+               this.question.innerHTML = '';
+               put(this.question,
+                       '[data-id=$] img[src=$]', word.id,
+                       word.thumb()
+               );
+       }
+
+       verify(click) {
+               let answer = click.target;
+               put(answer, '.chosen');
+               console.log(this.question, answer);
+               let match = this.question.dataset.id == answer.dataset.id;
+               put(answer, match ? '.good' : '.wrong');
+               this.log('pick', answer.dataset.id, answer.index, this.question.dataset.id);
+               this.next();
+       }
+
+       setup() {
+               super.setup();
+               this.form.innerHTML = '';
+               this.question = put(this.form, 'figure');
+               this.words.splice(9)
+
+               let answers = put(this.form, 'ul');
+               this.words
+                       .forEach((answer, seq) => {
+                               put(answers, 'li[data-id=$][onclick=""]',
+                                       answer.id, answer.label, {
+                                               onclick: e => this.verify(e),
+                                               index: seq,
+                                       }
+                               )
+                       });
+               this.words.shuffle();
+               this.next();
+       }
+};
index ef5cd4ae8c5691c0df0070347a61ba9175ea0d46..463fa1700163235beadcac7fc3b63251c80e2bbf 100644 (file)
@@ -1,25 +1,45 @@
 use utf8;
-(
-iso      => [qw{ k  kh g  gh ṅ  c  ch j  jh > ñ  ṭ  ṭh ḍ  ḍh ṇ  t  th d  dh n ṉ  p  ph b  bh m  y  r ṟ  l  ḷ  ḻ  v  ś  ṣ  s  h }],
-ipa      => [qw{ k  kʰ ɡ  ɡʱ ŋ  c  cʰ ɟ  ɟʱ > ɲ  ʈ  ʈʰ ɖ  ɖʱ ɳ  t̪  t̪ʰ d̪  d̪ʱ n̪ n  p  pʰ b  bʱ m  j  r ɾ  l  ɭ  ɻ  ʋ  ʃ  ʂ  s  ɦ }],
-orya     => [qw{ କ  ଖ  ଗ  ଘ  ଙ  ଚ  ଛ  ଜ  ଝ  > ଞ  ଟ  ଠ  ଡ  ଢ  ଣ  ତ  ଥ  ଦ  ଧ  > ନ  ପ  ଫ  ବ  ଭ  ମ  ଯ  > ର  ଲ  >  ଳ  ଵ  ଶ  ଷ  ସ  ହ }], # <kalinga 10hCE
-beng     => [qw{ ক  খ  গ  ঘ  ঙ  চ  ছ  জ  ঝ  > ঞ  ট  ঠ  ড  ঢ  ণ  ত  থ  দ  ধ  > ন  প  ফ  ব  ভ  ম  য  > র/ৰ ল >  -  ৱ  শ  ষ  স  হ }], # <sidd 11hCE
-deva     => [qw{ क  ख  ग  घ  ङ  च  छ  ज  झ  > ञ  ट  ठ  ड  ढ  ण  त  थ  द  ध  न ऩ  प  फ  ब  भ  म  य  र ऱ  ल  ळ  ऴ  व  श  ष  स  ह }], # <nagari 13hCE <kalinga 8hCE <brah
-gujr     => [qw{ ક  ખ  ગ  ઘ  ઙ  ચ  છ  જ  ઝ  > ઞ  ટ  ઠ  ડ  ઢ  ણ  ત  થ  દ  ધ  > ન  પ  ફ  બ  ભ  મ  ય  > ર  લ  >  ળ  વ  શ  ષ  સ  હ }], # <deva 17hCE
-guru     => [qw{ ਕ  ਖ  ਗ  ਘ  ਙ  ਚ  ਛ  ਜ  ਝ  > ਞ  ਟ  ਠ  ਡ  ਢ  ਣ  ਤ  ਥ  ਦ  ਧ  > ਨ  ਪ  ਫ  ਬ  ਭ  ਮ  ਯ  > ਰ  ਲ  >  ਲ਼  ਵ  ਸ਼  -  ਸ  ਹ }], # <sharada 16hCE
-tibt     => [qw{ ཀ  ཁ  ག  -  ང  ཅ  ཆ  ཇ  -  > ཉ  ཊ  ཋ  ཌ  -  ཎ  ཏ  ཐ  ད  -  > ན  པ  ཕ  བ  -  མ  ཡ  > ར  ལ  >  -  ཝ  ཤ  ཥ  ས  ཧ }], # <sidd 7hCE
-sidd     => [qw{ 𑖎  𑖏  𑖐  𑖑  𑖒  𑖓  𑖔  𑖕  𑖖  > 𑖗  𑖘  𑖙  𑖚  𑖛  𑖜  𑖝  𑖞  𑖟  𑖠  > 𑖡  𑖢  𑖣  𑖤  𑖥  𑖦  𑖧  > 𑖨  𑖩  >  -  𑖪  𑖫  𑖬  𑖭  𑖮 }], # <brah 7hCE
+{
+title => 'brahmic',
+pagetitle => 'brahmic scripts comparison sheet',
+version => '1.0',
+keywords => [qw( brahmic brahmi abugida )],
+intro => <<'.',
+Comparison of writing systems derived from the ancient Brahmi abugida.
+Also see more distant relatives of <a href="/writing">Phoenician</a>,
+including the <a href="/latin">Latin</a> alphabet.
+See <a href="/charset">charsets</a> for other characters.
+.
+
+list => [qw(
+       iso ipa
+       orya beng deva gujr guru tibt sidd
+       brah
+       telu knda sinh mlym taml mymr khmr thai laoo tglg
+)],
+
+table => {
+iso  => [qw{ k  kh g  gh ṅ  c  ch j  jh > ñ  ṭ  ṭh ḍ  ḍh ṇ  t  th d  dh n ṉ  p  ph b  bh m  y  r ṟ  l  ḷ  ḻ  v  ś  ṣ  s  h }],
+ipa  => [qw{ k  kʰ ɡ  ɡʱ ŋ  c  cʰ ɟ  ɟʱ > ɲ  ʈ  ʈʰ ɖ  ɖʱ ɳ  t̪  t̪ʰ d̪  d̪ʱ n̪ n  p  pʰ b  bʱ m  j  r ɾ  l  ɭ  ɻ  ʋ  ʃ  ʂ  s  ɦ }],
+orya => [qw{ କ  ଖ  ଗ  ଘ  ଙ  ଚ  ଛ  ଜ  ଝ  > ଞ  ଟ  ଠ  ଡ  ଢ  ଣ  ତ  ଥ  ଦ  ଧ  > ନ  ପ  ଫ  ବ  ଭ  ମ  ଯ  > ର  ଲ  >  ଳ  ଵ  ଶ  ଷ  ସ  ହ }], # <kalinga 10hCE
+beng => [qw{ ক  খ  গ  ঘ  ঙ  চ  ছ  জ  ঝ  > ঞ  ট  ঠ  ড  ঢ  ণ  ত  থ  দ  ধ  > ন  প  ফ  ব  ভ  ম  য  > র/ৰ ল >  -  ৱ  শ  ষ  স  হ }], # <sidd 11hCE
+deva => [qw{ क  ख  ग  घ  ङ  च  छ  ज  झ  > ञ  ट  ठ  ड  ढ  ण  त  थ  द  ध  न ऩ  प  फ  ब  भ  म  य  र ऱ  ल  ळ  ऴ  व  श  ष  स  ह }], # <nagari 13hCE <kalinga 8hCE <brah
+gujr => [qw{ ક  ખ  ગ  ઘ  ઙ  ચ  છ  જ  ઝ  > ઞ  ટ  ઠ  ડ  ઢ  ણ  ત  થ  દ  ધ  > ન  પ  ફ  બ  ભ  મ  ય  > ર  લ  >  ળ  વ  શ  ષ  સ  હ }], # <deva 17hCE
+guru => [qw{ ਕ  ਖ  ਗ  ਘ  ਙ  ਚ  ਛ  ਜ  ਝ  > ਞ  ਟ  ਠ  ਡ  ਢ  ਣ  ਤ  ਥ  ਦ  ਧ  > ਨ  ਪ  ਫ  ਬ  ਭ  ਮ  ਯ  > ਰ  ਲ  >  ਲ਼  ਵ  ਸ਼  -  ਸ  ਹ }], # <sharada 16hCE
+tibt => [qw{ ཀ  ཁ  ག  -  ང  ཅ  ཆ  ཇ  -  > ཉ  ཊ  ཋ  ཌ  -  ཎ  ཏ  ཐ  ད  -  > ན  པ  ཕ  བ  -  མ  ཡ  > ར  ལ  >  -  ཝ  ཤ  ཥ  ས  ཧ }], # <sidd 7hCE
+sidd => [qw{ 𑖎  𑖏  𑖐  𑖑  𑖒  𑖓  𑖔  𑖕  𑖖  > 𑖗  𑖘  𑖙  𑖚  𑖛  𑖜  𑖝  𑖞  𑖟  𑖠  > 𑖡  𑖢  𑖣  𑖤  𑖥  𑖦  𑖧  > 𑖨  𑖩  >  -  𑖪  𑖫  𑖬  𑖭  𑖮 }], # <brah 7hCE
 # ↑ northern
-brah     => [qw{ 𑀓  𑀔  𑀕  𑀖  𑀗  𑀘  𑀙  𑀚  𑀛  > 𑀜  𑀝  𑀞  𑀟  𑀠  𑀡  𑀢  𑀣  𑀤  𑀥  𑀦 -𑀷 𑀧  𑀨  𑀩  𑀪  𑀫  𑀬  𑀭 -𑀶 𑀮  𑀴  -𑀵 𑀯  𑀰  𑀱  𑀲  𑀳 }],
+brah => [qw{ 𑀓  𑀔  𑀕  𑀖  𑀗  𑀘  𑀙  𑀚  𑀛  > 𑀜  𑀝  𑀞  𑀟  𑀠  𑀡  𑀢  𑀣  𑀤  𑀥  𑀦 -𑀷 𑀧  𑀨  𑀩  𑀪  𑀫  𑀬  𑀭 -𑀶 𑀮  𑀴  -𑀵 𑀯  𑀰  𑀱  𑀲  𑀳 }],
 # ↓ southern
-telu     => [qw{ క  ఖ  గ  ఘ  ఙ  చ  ఛ  జ  ఝ  > ఞ  ట  ఠ  డ  ఢ  ణ  త  థ  ద  ధ  > న  ప  ఫ  బ  భ  మ  య  ర ఱ  ల  >  ళ  వ  శ  ష  స  హ }], # 11hCE
-knda     => [qw{ ಕ  ಖ  ಗ  ಘ  ಙ  ಚ  ಛ  ಜ  ಝ  > ಞ  ಟ  ಠ  ಡ  ಢ  ಣ  ತ  ಥ  ದ  ಧ  > ನ  ಪ  ಫ  ಬ  ಭ  ಮ  ಯ  ರ ಱ  ಲ  ಳ  ೞ  ವ  ಶ  ಷ  ಸ  ಹ }], # 9hCE
-sinh     => [qw{ ක  ඛ  ග  ඝ  ඞ  ච  ඡ  ජ  ඣ  > ඤ  ට  ඨ  ඩ  ඪ  ණ  ත  ථ  ද  ධ  > න  ප  ඵ  බ  භ  ම  ය  > ර  ල  >  ළ  ව  ශ  ෂ  ස  හ }], # <gran 12hCE
-mlym     => [qw{ ക  ഖ  ഗ  ഘ  ങ  ച  ഛ  ജ  ഝ  > ഞ  ട  ഠ  ഡ  ഢ  ണ  ത  ഥ  ദ  ധ  ന -ഩ പ  ഫ  ബ  ഭ  മ  യ  ര റ  ല  ള  ഴ  വ  ശ  ഷ  സ  ഹ }], # <gran 12hCE
-taml     => [qw{ க  -  -  -  ங  ச  -  ஜ  -  > ஞ  ட  -  -  -  ண  த  -  -  -  ந ன  ப  -  -  -  ம  ய  ர ற  ல  ள  ழ  வ  ஶ  ஷ  ஸ  ஹ }], # 3hCE
-mymr     => [qw{ က  ခ  ဂ  ဃ  င  စ  ဆ  ဇ  ဈ  ဉ ည  ဋ  ဌ  ဍ  ဎ  ဏ  တ  ထ  ဒ  ဓ  > န  ပ  ဖ  ဗ  ဘ  မ  ယ  > ရ  လ  ဠ  ၔ  ဝ  ၐ  ၑ  သ  ဟ }], # <gran 11hCE
-khmr     => [qw{ ក  ខ  គ  ឃ  ង  ច  ឆ  ជ  ឈ  > ញ  ដ  ឋ  ឌ  ឍ  ណ  ត  ថ  ទ  ធ  > ន  ប  ផ  ព  ភ  ម  យ  > រ  ល  >  -  វ  ឝ  ឞ  ស  ហ }], # <gran 11hCE
-thai     => [qw{ ก  ข  ค  ฆ  ง  จ  ฉ  ช  ฌ  > ญ  ฏ  ฐ  ฑ  ฒ  ณ  ต  ถ  ท  ธ  > น  ป  ผ  พ  ภ  ม  ย  > ร  ล  >  -  ว  ศ  ษ  ส  ห }], # <khmr 13hCE
-laoo     => [qw{ ກ  ຂ  -  ຄ  ງ  ຈ  ສ  ຊ  -  > ຍ  -  -  -  -  -  ຕ  ຖ  ທ  -  > ນ  ປ  ຜ  ຟ  ພ  ມ  ຢ  > ຣ  ລ  >  -  ວ  -  -  -  ຫ }], # <khmr 14hCE
-tglg     => [qw{ ᜃ  -  ᜄ  -  ᜅ  -  -  -  -  > -  -  -  -  -  -  ᜆ  -  ᜇ  -  > ᜈ  ᜉ  -  ᜊ  -  ᜋ  ᜌ  > ᜇ  ᜎ  >  -  -  -  -  ᜐ  ᜑ }], # <kawi 14hCE
-);
+telu => [qw{ క  ఖ  గ  ఘ  ఙ  చ  ఛ  జ  ఝ  > ఞ  ట  ఠ  డ  ఢ  ణ  త  థ  ద  ధ  > న  ప  ఫ  బ  భ  మ  య  ర ఱ  ల  >  ళ  వ  శ  ష  స  హ }], # 11hCE
+knda => [qw{ ಕ  ಖ  ಗ  ಘ  ಙ  ಚ  ಛ  ಜ  ಝ  > ಞ  ಟ  ಠ  ಡ  ಢ  ಣ  ತ  ಥ  ದ  ಧ  > ನ  ಪ  ಫ  ಬ  ಭ  ಮ  ಯ  ರ ಱ  ಲ  ಳ  ೞ  ವ  ಶ  ಷ  ಸ  ಹ }], # 9hCE
+sinh => [qw{ ක  ඛ  ග  ඝ  ඞ  ච  ඡ  ජ  ඣ  > ඤ  ට  ඨ  ඩ  ඪ  ණ  ත  ථ  ද  ධ  > න  ප  ඵ  බ  භ  ම  ය  > ර  ල  >  ළ  ව  ශ  ෂ  ස  හ }], # <gran 12hCE
+mlym => [qw{ ക  ഖ  ഗ  ഘ  ങ  ച  ഛ  ജ  ഝ  > ഞ  ട  ഠ  ഡ  ഢ  ണ  ത  ഥ  ദ  ധ  ന -ഩ പ  ഫ  ബ  ഭ  മ  യ  ര റ  ല  ള  ഴ  വ  ശ  ഷ  സ  ഹ }], # <gran 12hCE
+taml => [qw{ க  -  -  -  ங  ச  -  ஜ  -  > ஞ  ட  -  -  -  ண  த  -  -  -  ந ன  ப  -  -  -  ம  ய  ர ற  ல  ள  ழ  வ  ஶ  ஷ  ஸ  ஹ }], # 3hCE
+mymr => [qw{ က  ခ  ဂ  ဃ  င  စ  ဆ  ဇ  ဈ  ဉ ည  ဋ  ဌ  ဍ  ဎ  ဏ  တ  ထ  ဒ  ဓ  > န  ပ  ဖ  ဗ  ဘ  မ  ယ  > ရ  လ  ဠ  ၔ  ဝ  ၐ  ၑ  သ  ဟ }], # <gran 11hCE
+khmr => [qw{ ក  ខ  គ  ឃ  ង  ច  ឆ  ជ  ឈ  > ញ  ដ  ឋ  ឌ  ឍ  ណ  ត  ថ  ទ  ធ  > ន  ប  ផ  ព  ភ  ម  យ  > រ  ល  >  -  វ  ឝ  ឞ  ស  ហ }], # <gran 11hCE
+thai => [qw{ ก  ข  ค  ฆ  ง  จ  ฉ  ช  ฌ  > ญ  ฏ  ฐ  ฑ  ฒ  ณ  ต  ถ  ท  ธ  > น  ป  ผ  พ  ภ  ม  ย  > ร  ล  >  -  ว  ศ  ษ  ส  ห }], # <khmr 13hCE
+laoo => [qw{ ກ  ຂ  -  ຄ  ງ  ຈ  ສ  ຊ  -  > ຍ  -  -  -  -  -  ຕ  ຖ  ທ  -  > ນ  ປ  ຜ  ຟ  ພ  ມ  ຢ  > ຣ  ລ  >  -  ວ  -  -  -  ຫ }], # <khmr 14hCE
+tglg => [qw{ ᜃ  -  ᜄ  -  ᜅ  -  -  -  -  > -  -  -  -  -  -  ᜆ  -  ᜇ  -  > ᜈ  ᜉ  -  ᜊ  -  ᜋ  ᜌ  > ᜇ  ᜎ  >  -  -  -  -  ᜐ  ᜑ }], # <kawi 14hCE
+},
+}
index 57193c8c8376d96f14be33b60a24b625537d5404..4ad57fff76110e15849bd48444e6dc73b6bc4169 100644 (file)
@@ -1,7 +1,7 @@
 use 5.014;
 use utf8;
 use warnings;
-use List::Util qw( pairs pairmap sum );
+use List::Util qw( pairs pairmap sum min max );
 
 my %C = (
        red    => '#EC1C24',
@@ -15,10 +15,10 @@ my @wrapstyle = (
        'td { white-space: normal; word-spacing: 10em }',
                # force line break between words
        '.sample { word-spacing: 0 }',
-       '.sample span { margin-right: 1ex; white-space: nowrap; display: inline-block }',
+       '.sample svg { margin-right: 1ex; white-space: nowrap; display: inline-block }',
                # larger space between letters
 );
-my $spacestyle = '.sample span { margin-right: 0.5ex }';  # separate letters
+my $spacestyle = '.sample svg { margin-right: 0.5ex }';  # separate letters
 my @tapstyle = (
        @wrapstyle,
        '{ line-height: 1ex }',
@@ -53,6 +53,66 @@ sub disptap {
        return $prefix . join(' ', map { '·' x $_ } @dots);
 }
 
+sub dispdomino {
+       my $code = shift;
+       my ($prefix, @dots) = $code =~ m/\A(-?)(\d)(\d)/ or return $code;
+       # unicode glyph alternative as DOMINO TILE HORIZONTAL-0a-0b
+       return $prefix . chr(0x1F031 + ($dots[0] * 7) + $dots[1]);
+}
+
+sub dispdash {
+       my $code = shift;
+       my ($prefix, @dots) = $code =~ m/\A(-?)(\d)(\d)/ or return $code;
+       my ($w, $h) = (max(6, 4 * max(@dots)), 9);
+       my ($w0, $w1) = ($w / $dots[0], $dots[1] ? $w / $dots[1] : 1);
+       return sprintf(
+               '<svg height="20"%s viewBox="-.5 -.5 %s %s">'
+               . '<path d="%s" /></svg>',
+               $prefix && ' style="opacity:.5"',
+               $w + 1, $h + 1, join(' ',
+                       "m0,$h l+$w0,-$h" x $dots[0], # slashes
+                       "m0,$h l-$w1,-$h" x $dots[1], # backslashes
+               )
+       );
+}
+
+sub dispblock {
+       my $code = shift;
+       my ($prefix, $shape, $rotate) = $code =~ m/\A(-?)(\w)(\d?)/
+               or return $code;
+       my %path = (
+               S => 'm0,1h1v-1h1',
+               Z => 'm0,0h1v1h1',
+               L => 'm0,0v2h1',
+               v => 'm0,0v1h1',
+               J => 'm1,0v2h-1',
+               T => 'm0,0h2.5h-1.5v1',
+               O => 'm0,0h1v1h-1',
+               I => 'm0,1h3',
+               i => 'm0,1h2',
+               o => 'm0,1h1',
+       );
+       my %col = (
+               S => 120, Z => 0, J => 240, L => 30, T => 300, O => 60, I => 180,
+               v => '45,50%', i => '165,50%', o => '165,0%',
+       );
+       s/\z(?<!%)/,100%/ for values %col;
+       my @gaps = (grep $_,
+               $code =~ /[Ii]$|T[23]|L3?$|S1|Z$|J1|v3?$/ ? 'gapl1' : (),
+               $code =~ /T$|L2|Z$/ ? 'gapr1' : (),
+       );
+       return sprintf(
+               '<svg height="24" viewBox="-.5 -.5 %s 4"%s>'
+               . '<path d="%s" stroke="%s" fill="none"%s /></svg>',
+               $code eq 'I' ? 4 : $code =~ /T3|[LJO]$|[Iio]1/ ? 2 : 3,
+               @gaps ? qq( class="@gaps") : '',
+               $path{$shape}, "hsl($col{$shape},50%)", join('',
+                       $rotate && sprintf(' transform="rotate(%s 1 1)"', $rotate * 90),
+                       $prefix && ' style="opacity:.5"',
+               ),
+       );
+}
+
 sub dispbar {
        my $code = shift or return '';
 
@@ -92,7 +152,23 @@ sub disphues {
        );
 }
 
-(
++{
+default => [qw( written sign digital touch tactile sound games semaphore barcode personal )],
+written => [qw( uppercase lowercase suetterlin roman )],
+digital => [qw( stroke ita2 )],
+stroke => [qw( graffiti unistrokes edgewrite )],
+touch => [qw( moon braille )],
+sign => ['sutton'],
+sound => [qw( morse tap shorttap )],
+games => [qw( domino tetromino cards )],
+semaphore => [qw( maritime flag chappe prussian )],
+barcode => [qw( rm4scc code39 code93 code128 )],
+personal => [qw( rgbmap cmymap dni pigpen nyctographs chromacons )],
+
+order => {
+       name => '#',
+       list => [1 .. 26],
+},
 uppercase => {
        list => [qw{ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z }],
 },
@@ -107,6 +183,7 @@ suetterlin => {
                        src: url("/suetterlin.ttf");
                }',
                'td { font-family: Suetterlin }',
+               'td:hover::first-letter { text-transform: uppercase }',
        ],
        list => [qw{ a b c d e f g h i j k l m n o p q r ſ s t u v w x y z }],
 },
@@ -114,7 +191,7 @@ roman => {
        name => 'Old Roman Cursive',
        style => [
                'svg path { stroke-linecap: round; stroke-linejoin: round }',
-               '.sample span { margin-right: -10px }',
+               '.sample svg { margin-right: -10px }',
        ],
        list => [
                map {
@@ -164,6 +241,17 @@ sutton => {
                0         965aa6
        }],
 },
+graffiti => {
+       name => 'Palm Graffiti',
+       style => [
+               '@font-face {
+                       font-family: Graffiti;
+                       src: url("/graffiti.ttf");
+               }',
+               'td { font-family: Graffiti }',
+       ],
+       list => [qw{ a b c d e f g h i j k l m n o p q r s t u v w * y z }],
+},
 unistrokes => {
        name => 'Unistrokes',
        url   => 'https://www.google.com/patents/US5596656', # by Xerox
@@ -171,8 +259,9 @@ unistrokes => {
        list => [
                map { '<svg width="14" height="16" viewBox="-1 -1 8 10">'.$_.'</svg>' }
                map {
-                       sprintf('<circle cx="%s" cy="%s" r="1"/>', m/\AM(\d+),(\d+)(.?)/) . # start point
-                       (!!$3 && qq(<path d="$_"/>))
+                       my ($x, $y, $next) = m/\AM(\d+),(\d+)(.)?/;
+                       sprintf('<circle cx="%s" cy="%s" r="1"/>', $x, $y) . # start point
+                       (defined $next && qq(<path d="$_"/>))
                }
                'M3,8 V0',
                'M0,0 6,4 0,8',
@@ -228,9 +317,6 @@ edgewrite => {
                )
        ],
 },
-#graffiti => {
-#      name => 'Palm Graffiti',
-#},
 ita2 => {
        name => '<abbr title="International Telegraph Alphabet">ITA</abbr>2',
        style => [@wrapstyle, 'td { font-size: 50% }'],
@@ -311,10 +397,39 @@ tap => {
 shorttap => {
        name => 'Short Tap',
        style => \@tapstyle,
-       list => [map { disptap($_) } qw{
+       altlist => [map { disptap($_) } qw{
                11 12 13 14 21 22 23 20 > 31 -13 32 33
                30 41 42 -13 43 40 10 51 52 53 50 -31 -40
        }],
+       list => [map { dispdash($_) } qw{
+               10 14 -24 12 20 23 22 21 30 -34 13 33 32
+               31 40 43 -13 42 41 11 50 -53 -44 -52 -51 -54
+       }],
+},
+domino => {
+       name => 'Domino tiles',
+       style => [
+               # enlarge single tile height to span full vertical combinations
+               'td { font-size: 200%; line-height: .6; padding: 0 0 .3ex }',
+       ],
+       list => [map { dispdomino($_) } qw{
+               10 11 20 21 22 30 31 32 33 40 41 42 43
+               44 50 51 52 53 54 55 60 61 62 63 64 65
+       }],
+},
+tetromino => {
+       style => [
+               'svg path { stroke-linecap: square }',
+               '.sample .gapl1 + .gapr1 { margin-left: -6.3px }',
+       ],
+       name => 'Tetrominos',
+       list => [map { dispblock($_) } qw{
+               T2 T1 I  T3
+               i  L1 J  L3
+               o1 I1 L2 L  -S1 Z1
+               O  J2 v2 -J3 S  T
+               J1 v1 v  v3 i1 Z
+       }],
 },
 cards => {
        style => 'td { font-family: Symbola, "DejaVu Sans", serif, sans }',
@@ -580,7 +695,7 @@ dni => {
        name => "D'ni",
        style => [
                'svg { border: 1px solid currentColor }',
-               '.sample span + span svg { border-left: 0 }',
+               '.sample svg + svg { border-left: 0 }',
        ],
        list => [
                map {
@@ -663,7 +778,10 @@ nyctographs => {
 },
 chromacons => {
        title => 'Colour Alphabet by Paul Green-Armytage (2010)',
-#      style => '.sample { word-break: break-all }',
+       style => [
+               #'.sample { word-break: break-all }',
+               '.sample { background: white }',
+       ],
        list => [
                map {
                        sprintf('<span%s>%s</span>',
@@ -682,4 +800,4 @@ chromacons => {
                }
        ],
 },
-);
+};
index a7d6ae1bca78ad28316d4d6b24ef9d3bc288f158..f44eadd5d7ffe0872867948ae688cfe6160668a1 100644 (file)
@@ -1,35 +1,69 @@
 use utf8;
-(
- latn     => [qw{ A >  B C  G  D  > E  F  Y U V  W Z H -Þ -  I  J K L M N >  X O  P  > >  -  Q  R >  S T }],
-#runr_ys  => [qw{ ᚭ -  ᛓ ᚴ  ᛆ  -  > -  ᚠ  > > >  - ᛧ ᚽ  ᚦ -  >  ᛁ - ᛚ ᛙ ᚿ >  - -  -  > >  -  -  ᚱ >  ᛌ ᛐ }], # short-twig fuþark
-#runr_m   => [qw{ ᚮ -  ᛒ ᚲ  ᛆ  -  > -  ᚠ  > > >  - ᛧ ᚼ  ᚦ -  ᛁ  ᛂ - ᛚ ᛘ ᚿ >  - -  -  > >  -  -  ᚱ >  ᛋ ᛐ }], # fuþork (medieval)
--runr     => [qw{ ᚨ ᛈ  ᛒ ᚲ  ᛃ  ᛞ  > ᛖ  ᚠ  ᛉ > ᚢ  ᚣ ᛇ ᚺ  ᚦ -  >  ᛁ - ᛚ ᛗ ᚾ >  ᚷ ᛟ  -  > >  -  ᛜ  ᚱ >  ᛊ ᛏ }], # elder fuþark (ᚹ from Y/P/Q?)
--ital     => [qw{ 𐌀 >  𐌁 >  𐌂  𐌃  > 𐌄  𐌅  > > 𐌖 -𐌞 𐌆 𐌇  𐌈 𐌘  𐌉 -𐌝 𐌊 𐌋 𐌌 𐌍 𐌗  𐌎 𐌏  𐌐  > >  𐌑  𐌒  𐌓 >  𐌔 𐌕 }],
--goth     => [qw{ 𐌰 >  𐌱 𐌲  𐌾  𐌳  > 𐌴  𐍆  > > >  𐍅 𐌶 𐌷  𐌸 𐍈  >  𐌹 𐌺 𐌻 𐌼 𐌽 > -𐍇 𐌿 𐍀𐌵 -𐍊 >  - -𐍁  𐍂 >  𐍃 𐍄 }],
--copt     => [qw{ Ⲁ >  Ⲃ >  Ⲅ  Ⲇ  > Ⲉ  >  > > >  Ⲩ Ⲍ Ⲏ  Ⲑ Ⲫ  >  Ⲓ Ⲕ Ⲗ Ⲙ Ⲛ Ⲭ  Ⲝ Ⲟ  Ⲡ -Ⳁ >  Ⲋ  -  Ⲣ >  Ⲥ Ⲧ }],
- cyrl     => [qw{ А Б  В >  Г  Д  Е Э  > -Ѵ > У -Ү З И -Ѳ Ф  І -Ј К Л М Н Х -Ѯ О  П  Ц Ч -Џ -Ҁ  Р Ш  С Т }],
--lyci     => [qw{ 𐊀 𐊂  𐊃 >  𐊄  𐊅  > 𐊆  >  > > >  𐊇 𐊈 -  > 𐊉  >  𐊊 𐊋 𐊍 𐊎 𐊏 𐊐  𐊑 𐊒  𐊓  > >  -  𐊔  𐊕 >  𐊖 𐊗 }],
- grek     => [qw{ Α >  Β >  Γ  Δ  > Ε -Ϝ  > > >  Υ Ζ Η  Θ Φ  Ι -Ϳ Κ Λ Μ Ν Χ  Ξ Ο  Π -Ϡ > -Ϻ -Ϟ  Ρ Σ -Ϲ Τ }], # <phnx -8hCE
--cari     => [qw{ 𐊠 𐊩  𐊷 > -𐊱  𐊢  > 𐊹  𐊤  𐊿 > >  𐊲 𐋂 𐊺  > -  >  - 𐊼 𐊣 𐊪 𐊵 >  𐊴 𐊫  -  𐊸 >  𐊰  𐊨  𐊥 >  𐊮 𐊭 }], # <grek -7hCE
--lydi     => [qw{ 𐤠 >  𐤡 𐤢  𐤹  𐤣  > 𐤤  𐤥  𐤧 > >  𐤰 - -  > 𐤱  >  𐤦 𐤨 𐤩 𐤪 𐤫 >  - 𐤬  -  > >  𐤳  𐤲  𐤭 >  𐤮 𐤯 }], # <E-grek -7hCE
--qaai     => [qw{ - -  - >  𐗒  𐗢  > 𐗃  >  > > >  𐗉 - 𐗇  > 𐗜  >  𐗅 - 𐗣 𐗥 𐗦 >  𐗖 -  𐗌  > >  𐗯  𐗘  𐗩 >  𐗭 𐗚 }], # <phnx -7hCE
- phnx     => [qw{ 𐤀 >  𐤁 >  𐤂  𐤃  > 𐤄  >  > > >  𐤅 𐤆 𐤇  > 𐤈  >  𐤉 𐤊 𐤋 𐤌 𐤍 >  𐤎 𐤏  𐤐  > >  𐤑  𐤒  𐤓 >  𐤔 𐤕 }],
--samr     => [qw{ ࠀ >  ࠁ >  ࠂ  ࠃ  > ࠄ  >  > > >  ࠅ ࠆ ࠇ  > ࠈ  >  ࠉ ࠊ ࠋ ࠌ ࠍ >  ࠎ ࠏ  ࠐ  > >  ࠑ  ࠒ  ࠓ >  ࠔ ࠕ }], # <phnx -6hCE
--armi     => [qw{ 𐡀 >  𐡁 >  𐡂  𐡃  > 𐡄  >  > > >  𐡅 𐡆 𐡇  > 𐡈  𐡉  ꜜ 𐡊 𐡋 𐡌 𐡍 >  𐡎 𐡏  𐡐  > >  𐡑  𐡒  𐡓 >  𐡔 𐡕 }], # <phnx -8hCE
- hebr     => [qw{ א >  ב >  ג  ד  > ה  >  > > ו  װ ז ח  > ט  י  ײ כ ל מ נ >  ס ע  פ  > צ  ץ  ק  ר >  ש ת }], # <armi -3hCE
--sarb     => [qw{ 𐩱 >  𐩨 >  𐩴  𐩵  𐩠 𐩭  >  > > >  𐩥 𐩸 𐩢  > 𐩷  >  𐩺 𐩫 𐩡 𐩣 𐩬 >  𐩯 𐩲  𐩰  > 𐩮  𐩪  𐩤  𐩧 >  𐩦 𐩩 }], # unmatched: 𐩹 𐩳 𐩶 𐩻 𐩼
- ethi     => [qw{ አ በ -ቨ ገ -ጐ  ደ  ኀ ሀ  >  > > >  ወ - ሐ  > ጠ  >  የ ከ ለ መ ነ >  - ዐ  ፈ  ጸ ጰ  ሰ  ቀ  ረ >  ሠ ተ }], # unmatched: ዘ ፀ; new: ፐ
--narb     => [qw{ 𐪑 >  𐪈 >  𐪔  𐪕  𐪀 𐪍  >  > > >  𐪅 𐪘 𐪂  > 𐪗  >  𐪚 𐪋 𐪁 𐪃 𐪌 >  𐪏 𐪒  𐪐  > 𐪎  𐪊  𐪄  𐪇 >  𐪆 𐪉 }], # unmatched: 𐪙 𐪓 𐪖 𐪛 𐪜
--nbat     => [qw{ 𐢁 >  𐢃 >  𐢄  𐢅  > 𐢇  >  > > >  𐢈 𐢉 𐢊  > 𐢋  >  𐢍 𐢏 𐢑 𐢓 𐢕 >  𐢖 𐢗  𐢘  > >  𐢙  𐢚  𐢛 >  𐢝 𐢞 }], # <syrc -2hCE
- arab     => [qw{ ا >  ب >  ج  د  > ه  >  > > >  و ز ح  > ط  >  ي ك ل م ن >  - ع  ف  > >  ص  ق  ر >  س ت }], # <nbat  4hCE
- syrc     => [qw{ ܐ >  ܒ >  ܓ  ܕ  > ܗ  >  > > >  ܘ ܙ ܚ  > ܛ  >  ܝ ܟ ܠ ܡ ܢ >  ܣ ܥ  ܦ  > >  ܨ  ܩ  ܪ >  ܫ ܬ }], # <armi -2hCE
-#tfng     => [qw{ ⴰ >  ⴱ >  ⴳ  ⴷ  > ⴻ  >  > > >  ⵓ ⵣ ⵃ  > ⵟ  ⵉ  ⵢ ⴽ ⵍ ⵎ ⵏ >  ⵙ ⵄ  ⵒ  > >  ⵚ  ⵇ  ⵔ >  ⵛ ⵜ }], # <phnx? -3hCE
--hatr     => [qw{ 𐣠 >  𐣡 >  𐣢  𐣣  > 𐣤  >  > > >  𐣥 𐣦 𐣧  > 𐣨  >  𐣩 𐣪 𐣫 𐣬 𐣭 >  𐣮 𐣯  𐣰  > >  𐣱  𐣲 -𐣣 >  𐣴 𐣵 }], # <armi -1hCE
--prti     => [qw{ 𐭀 >  𐭁 >  𐭂  𐭃  > 𐭄  >  > > >  𐭅 𐭆 𐭇  > 𐭈  >  𐭉 𐭊 𐭋 𐭌 𐭍 >  𐭎 𐭏  𐭐  > >  𐭑  𐭒  𐭓 >  𐭔 𐭕 }], # <armi -1hCE
--phli     => [qw{ 𐭠 >  𐭡 >  𐭢  𐭣  > 𐭤  >  > > >  𐭥 𐭦 𐭧  > 𐭨  >  𐭩 𐭪 𐭫 𐭬 𐭭 >  𐭮 -  𐭯  > >  𐭰  -  - >  𐭱 𐭲 }], # <armi  3hCE
--phlp     => [qw{ 𐮀 >  𐮁 >  𐮂  𐮃  > 𐮄  >  > > >  𐮅 𐮆 𐮇  > -  >  𐮈 𐮉 𐮊 𐮋 𐮌 >  𐮍 -  𐮎  > >  𐮏  -  - >  𐮐 𐮑 }], # <phli  5hCE
--phlv     => [qw{-𐮳𐮳 > 𐮰 >  𐮳 -𐮶  > -  >  > > >  𐮷 𐮸 -  > -  >  𐮲 𐮹 𐮼 𐮾 -𐮷 > 𐯀 -  𐯂  > >  𐯁  - -𐮷 >  𐮿 𐯃 }], # <phli  6hCE
--avst     => [qw{ 𐬀 >  𐬠 >  𐬔  𐬛  > 𐬵  >  > > >  𐬎 𐬰 𐬑  > 𐬚  >  𐬌 𐬐 𐬮 𐬨 𐬥 >  𐬯 -  𐬞  > 𐬗  𐬘  -  𐬭 >  𐬱 𐬙 }], # <phlv  7hCE
- mand     => [qw{ ࡀ >  ࡁ >  ࡂ  ࡃ  > ࡄ  >  > > >  ࡅ ࡆ ࡇ  > ࡈ  >  ࡉ ࡊ ࡋ ࡌ ࡍ >  ࡎ ࡏ  ࡐ  > >  ࡑ  ࡒ  ࡓ >  ࡔ ࡕ }], # <prti
- brah     => [qw{ 𑀅 >  𑀩 >  𑀕  𑀥  > -  >  > > >  𑀯 𑀤 -  𑀣 𑀞  >  𑀬 𑀓 𑀮 𑀫 𑀦 >  𑀰 -  𑀧  > >  𑀲  𑀔  𑀭 >  𑀱 𑀢 }],
-);
+{
+title => 'phoenician-derived',
+pagetitle => 'writing system inheritance sheet',
+version => '1.3',
+keywords => [qw'
+       phoenician proto-canaanite hebrew latin greek aramaic arabic
+       abjad related derived descendant
+'],
+description => [
+       "Character comparison,",
+       "tracking letters as they evolve from Phoenician to modern scripts.",
+       "Good Unicode test sample.",
+],
+intro => <<'.',
+Comparison of Unicode letters in related alphabets.
+There are more detailed pages of <a href="/latin">Latin</a>
+and <a href="/writing/brahmi">Brahmic</a> scripts.
+Also see <a href="/charset">charsets</a>
+and <a href="/unicode">common characters</a>.
+.
+
+list => [qw(
+       latn -runr -ital -goth -copt cyrl -perm -lyci grek -cari -lydi
+       phnx -egyp -samr -armi hebr -sarb ethi -narb -nbat arab
+       syrc -sogo -sogd mong -hatr -prti -phli -phlp -avst mand brah
+)],
+
+table => {
+latn   => [qw{ A >  B C  G  D  > E  F  Y U V  W Z H -Þ -  I  J K L M N >  X O  P  > >  -  Q  R >  S T }],
+runr_ys=> [qw{ ᚭ -  ᛓ ᚴ  ᛆ  -  > -  ᚠ  > > >  - ᛧ ᚽ  ᚦ -  >  ᛁ - ᛚ ᛙ ᚿ >  - -  -  > >  -  -  ᚱ >  ᛌ ᛐ }], # short-twig fuþark
+runr_m => [qw{ ᚮ -  ᛒ ᚲ  ᛆ  -  > -  ᚠ  > > >  - ᛧ ᚼ  ᚦ -  ᛁ  ᛂ - ᛚ ᛘ ᚿ >  - -  -  > >  -  -  ᚱ >  ᛋ ᛐ }], # fuþork (medieval)
+runr   => [qw{ ᚨ ᛈ  ᛒ ᚲ  ᛃ  ᛞ  > ᛖ  ᚠ  ᛉ > ᚢ  ᚣ ᛇ ᚺ  ᚦ -  >  ᛁ - ᛚ ᛗ ᚾ >  ᚷ ᛟ  ᚹ  > >  -  ᛜ  ᚱ >  ᛊ ᛏ }], # <ital 2hCE (elder fuþark)
+ital   => [qw{ 𐌀 >  𐌁 >  𐌂  𐌃  > 𐌄  𐌅  > > 𐌖 -𐌞 𐌆 𐌇  𐌈 𐌘  𐌉 -𐌝 𐌊 𐌋 𐌌 𐌍 𐌗  𐌎 𐌏  𐌐  > >  𐌑  𐌒  𐌓 𐌯  𐌔 𐌕 }], # <grek -7hCE; new: 𐌚
+goth   => [qw{ 𐌰 >  𐌱 𐌲  𐌾  𐌳  > 𐌴  𐍆  > > >  𐍅 𐌶 𐌷  𐌸 𐍈  >  𐌹 𐌺 𐌻 𐌼 𐌽 > -𐍇 𐌿 𐍀𐌵 -𐍊 >  - -𐍁  𐍂 >  𐍃 𐍄 }],
+copt   => [qw{ Ⲁ >  Ⲃ >  Ⲅ  Ⲇ  > Ⲉ  >  > > >  Ⲩ Ⲍ Ⲏ  Ⲑ Ⲫ  >  Ⲓ Ⲕ Ⲗ Ⲙ Ⲛ Ⲭ  Ⲝ Ⲟ  Ⲡ -Ⳁ >  Ⲋ  -  Ⲣ >  Ⲥ Ⲧ }],
+cyrl   => [qw{ А Б  В >  Г  Д  Е Э  > -Ѵ > У -Ү З И -Ѳ Ф  І -Ј К Л М Н Х -Ѯ О  П  Ц Ч -Џ -Ҁ  Р Ш  С Т }],
+perm   => [qw{ 𐍐 𐍣  𐍮 >  𐍒  𐍓  𐍔 -  >  > > >  - 𐍘 𐍞  𐍤 𐍑  >  𐍙 𐍚 𐍛 𐍜 𐍝 -  𐍥 -  𐍟  > >  -  -  𐍠 𐍦  𐍡 𐍢 }], # <cyrl 14hCE
+lyci   => [qw{ 𐊀 𐊂  𐊃 >  𐊄  𐊅  > 𐊆  >  > > >  𐊇 𐊈 -  > 𐊉  >  𐊊 𐊋 𐊍 𐊎 𐊏 𐊐  𐊑 𐊒  𐊓  > >  -  𐊔  𐊕 >  𐊖 𐊗 }],
+grek   => [qw{ Α >  Β >  Γ  Δ  > Ε -Ϝ  > > >  Υ Ζ Η  Θ Φ  Ι -Ϳ Κ Λ Μ Ν Χ  Ξ Ο  Π -Ϡ > -Ϻ -Ϟ  Ρ Σ -Ϲ Τ }], # <phnx -8hCE
+cari   => [qw{ 𐊠 𐊩  𐊷 > -𐊱  𐊢  > 𐊹  𐊤  𐊿 > >  𐊲 𐋂 𐊺  > -  >  - 𐊼 𐊣 𐊪 𐊵 >  𐊴 𐊫  -  𐊸 >  𐊰  𐊨  𐊥 >  𐊮 𐊭 }], # <grek -7hCE
+lydi   => [qw{ 𐤠 >  𐤡 𐤢  𐤹  𐤣  > 𐤤  𐤥  𐤧 > >  𐤰 - -  > 𐤱  >  𐤦 𐤨 𐤩 𐤪 𐤫 >  - 𐤬  -  > >  𐤳  𐤲  𐤭 >  𐤮 𐤯 }], # <E-grek -7hCE
+qaai   => [qw{ - -  - >  𐈒  𐈢  > 𐈃  >  > > >  𐈉 - 𐈇  > 𐈜  >  𐈅 - 𐈣 𐈥 𐈧 >  𐈖 -  𐈌  > >  𐈲  𐈘  𐈫 >  𐈯 𐈚 }], # <phnx -7hCE TODO
+phnx   => [qw{ 𐤀 >  𐤁 >  𐤂  𐤃  > 𐤄  >  > > >  𐤅 𐤆 𐤇  > 𐤈  >  𐤉 𐤊 𐤋 𐤌 𐤍 >  𐤎 𐤏  𐤐  > >  𐤑  𐤒  𐤓 >  𐤔 𐤕 }],
+egyp   => [qw{ 𓃾 >  𓉐 >  𓌙  𓉿  > 𓀠  >  > > >  𓌉 𓍿 𓉗  > 𓄤  >  𓂝 𓂧 𓋿 𓈖 𓆓 >  𓊽 𓁹  𓂋  > >  𓎤  𓎗  𓁶 >  𓐮 𓏴 }], #      -33hCE
+samr   => [qw{ ࠀ >  ࠁ >  ࠂ  ࠃ  > ࠄ  >  > > >  ࠅ ࠆ ࠇ  > ࠈ  >  ࠉ ࠊ ࠋ ࠌ ࠍ >  ࠎ ࠏ  ࠐ  > >  ࠑ  ࠒ  ࠓ >  ࠔ ࠕ }], # <phnx -6hCE
+armi   => [qw{ 𐡀 >  𐡁 >  𐡂  𐡃  > 𐡄  >  > > >  𐡅 𐡆 𐡇  > 𐡈  𐡉  ꜜ 𐡊 𐡋 𐡌 𐡍 >  𐡎 𐡏  𐡐  > >  𐡑  𐡒  𐡓 >  𐡔 𐡕 }], # <phnx -8hCE
+hebr   => [qw{ א >  ב >  ג  ד  > ה  >  > > ו  װ ז ח  > ט  י  ײ כ ל מ נ >  ס ע  פ  > צ  ץ  ק  ר >  ש ת }], # <armi -3hCE
+sarb   => [qw{ 𐩱 >  𐩨 >  𐩴  𐩵  𐩠 𐩭  >  > > >  𐩥 𐩸 𐩢  > 𐩷  >  𐩺 𐩫 𐩡 𐩣 𐩬 >  𐩯 𐩲  𐩰  > 𐩮  𐩪  𐩤  𐩧 >  𐩦 𐩩 }], # unmatched: 𐩹 𐩳 𐩶 𐩻 𐩼
+ethi   => [qw{ አ በ -ቨ ገ -ጐ  ደ  ኀ ሀ  >  > > >  ወ - ሐ  > ጠ  >  የ ከ ለ መ ነ >  - ዐ  ፈ  ጸ ጰ  ሰ  ቀ  ረ >  ሠ ተ }], # unmatched: ዘ ፀ; new: ፐ
+narb   => [qw{ 𐪑 >  𐪈 >  𐪔  𐪕  𐪀 𐪍  >  > > >  𐪅 𐪘 𐪂  > 𐪗  >  𐪚 𐪋 𐪁 𐪃 𐪌 >  𐪏 𐪒  𐪐  > 𐪎  𐪊  𐪄  𐪇 >  𐪆 𐪉 }], # unmatched: 𐪙 𐪓 𐪖 𐪛 𐪜
+nbat   => [qw{ 𐢁 >  𐢃 >  𐢄  𐢅  > 𐢇  >  > > >  𐢈 𐢉 𐢊  > 𐢋  >  𐢍 𐢏 𐢑 𐢓 𐢕 >  𐢖 𐢗  𐢘  > >  𐢙  𐢚  𐢛 >  𐢝 𐢞 }], # <syrc -2hCE
+arab   => [qw{ ا >  ب >  ج  د  > ه  >  > > >  و ز ح  > ط  >  ي ك ل م ن >  - ع  ف  > >  ص  ق  ر >  س ت }], # <nbat  4hCE
+syrc   => [qw{ ܐ >  ܒ >  ܓ  ܕ  > ܗ  >  > > >  ܘ ܙ ܚ  > ܛ  >  ܝ ܟ ܠ ܡ ܢ >  ܣ ܥ  ܦ  > >  ܨ  ܩ  ܪ >  ܫ ܬ }], # <armi -2hCE
+sogo   => [qw{ 𐼀 >  𐼂 >  𐼄  -  > 𐼅  >  > > >  𐼇 𐼈 𐼉  > -  >  𐼊 𐼋 𐼌 𐼍 𐼎 >  𐼑 𐼒  𐼔  > >  𐼕  -  𐼘 >  𐼙 𐼚 }], # <syrc  3hCE
+sogd   => [qw{ 𐼰 >  𐼱 >  𐼲  -  > 𐼳  >  > > >  𐼴 𐼵 𐼶  > -  >  𐼷 𐼸 𐼹 𐼺 𐼻 >  𐼼 𐼽  𐼾  > >  𐼿  -  𐽀 >  𐽁 𐽂 }], # <sogo  6hCE
+ougr   => [qw{                                                                                        }], # <sogd  7hCE TODO
+mong   => [qw{ ᠠᠡ ᠧ ᠸ ᠬ  ᠭ  -  > -  >  > > > ᠣᠤᠥᠦ ᠰ᠋ ᠬᠭ > - > ᠢᠵᠶ ᠬᠭ ᠲ ᠮ ᠨ ᠰ ᠱ - ᠪ ᠴ ᠵ   -  - ᠯᠷ ᠰ ᠱ ᠲᠳ }], # <ougr 12hCE
+tfng   => [qw{ ⴰ >  ⴱ >  ⴳ  ⴷ  > ⴻ  >  > > >  ⵓ ⵣ ⵃ  > ⵟ  ⵉ  ⵢ ⴽ ⵍ ⵎ ⵏ >  ⵙ ⵄ  ⵒ  > >  ⵚ  ⵇ  ⵔ >  ⵛ ⵜ }], # <phnx? -3hCE
+hatr   => [qw{ 𐣠 >  𐣡 >  𐣢  𐣣  > 𐣤  >  > > >  𐣥 𐣦 𐣧  > 𐣨  >  𐣩 𐣪 𐣫 𐣬 𐣭 >  𐣮 𐣯  𐣰  > >  𐣱  𐣲 -𐣣 >  𐣴 𐣵 }], # <armi -1hCE
+prti   => [qw{ 𐭀 >  𐭁 >  𐭂  𐭃  > 𐭄  >  > > >  𐭅 𐭆 𐭇  > 𐭈  >  𐭉 𐭊 𐭋 𐭌 𐭍 >  𐭎 𐭏  𐭐  > >  𐭑  𐭒  𐭓 >  𐭔 𐭕 }], # <armi -1hCE
+phli   => [qw{ 𐭠 >  𐭡 >  𐭢  𐭣  > 𐭤  >  > > >  𐭥 𐭦 𐭧  > 𐭨  >  𐭩 𐭪 𐭫 𐭬 𐭭 >  𐭮 -  𐭯  > >  𐭰  -  - >  𐭱 𐭲 }], # <armi  3hCE
+phlp   => [qw{ 𐮀 >  𐮁 >  𐮂  𐮃  > 𐮄  >  > > >  𐮅 𐮆 𐮇  > -  >  𐮈 𐮉 𐮊 𐮋 𐮌 >  𐮍 -  𐮎  > >  𐮏  -  - >  𐮐 𐮑 }], # <phli  5hCE
+phlv   => [qw{-𐮳𐮳 > 𐮰 >  𐮳 -𐮶  > -  >  > > >  𐮷 𐮸 -  > -  >  𐮲 𐮹 𐮼 𐮾 -𐮷 > 𐯀 -  𐯂  > >  𐯁  - -𐮷 >  𐮿 𐯃 }], # <phli  6hCE TODO
+avst   => [qw{ 𐬀 >  𐬠 >  𐬔  𐬛  > 𐬵  >  > > >  𐬎 𐬰 𐬑  > 𐬚  >  𐬌 𐬐 𐬮 𐬨 𐬥 >  𐬯 -  𐬞  > 𐬗  𐬘  -  𐬭 >  𐬱 𐬙 }], # <phlv  7hCE
+mand   => [qw{ ࡀ >  ࡁ >  ࡂ  ࡃ  > ࡄ  >  > > >  ࡅ ࡆ ࡇ  > ࡈ  >  ࡉ ࡊ ࡋ ࡌ ࡍ >  ࡎ ࡏ  ࡐ  > >  ࡑ  ࡒ  ࡓ >  ࡔ ࡕ }], # <prti
+brah   => [qw{ 𑀅 >  𑀩 >  𑀕  𑀥  > -  >  > > >  𑀯 𑀤 -  𑀣 𑀞  >  𑀬 𑀓 𑀮 𑀫 𑀦 >  𑀰 -  𑀧  > >  𑀲  𑀔  𑀭 >  𑀱 𑀢 }],
+},
+}
index 25182ede25530eb074fe82d46752e91cbb3760be..136b03215e32f38d588934f9e508788a08d1ea86 100644 (file)
@@ -12,7 +12,7 @@ cham     => 'Cham',
 copt     => 'Coptic',
 cyrl     => 'Cyrillic',
 deva     => 'Devanagari',
-egyp     => 'Egyptian Hieroglyphs',
+egyp     => 'Egyptian',
 ethi     => "Ge'ez",
 goth     => 'Gothic',
 grek     => 'Greek',
@@ -38,12 +38,14 @@ lydi     => 'Lydian',
 mand     => 'Mandaic',
 maya     => 'Mayan',
 mlym     => 'Malayalam', # മലയാളം   ൧
+mong     => 'Mongolian',
 mymr     => 'Burmese',
 narb     => 'North Arabian',
 nbat     => 'Nabataean',
 olck     => 'Ol_Chiki',
 orya     => 'Oriya', # ଓଡ଼ିଆ
 osma     => 'Osmanya',
+perm     => 'Old Permic',
 phli     => 'Pahlavi',
 phlp     => 'Psalter Pahlavi',
 phlv     => 'Book Pahlavi',
@@ -56,6 +58,8 @@ sarb     => 'South Arabian',
 saur     => 'Saurashtra',
 sidd     => 'Siddham',
 sinh     => 'Sinhala',
+sogd     => 'Sogdian',
+sogo     => 'Old Sogdian',
 sund     => 'Sundanese',
 syrc     => 'Syriac',
 talu     => 'New_Tai_Lue',
index 2f08da63a3cd18d486fd7ccd97cc24eb24c85251..dfd78a4a77835f5943dd039b9a5abcc23dbc709d 100644 (file)
@@ -1,32 +1,32 @@
 <(common.inc.plp)><:
 
+my $source = lc $Request || 'phnx';
+$source =~ s/^brah\Kmi$//;
+my $include = "writing-$source";
+
+my $info = eval { Data($include) } || {};
+
 Html({
-       title => 'writing system inheritance sheet',
-       version => '1.2',
-       description => [
-               "Character comparison,",
-               "tracking letters as they evolve from Phoenician to modern scripts.",
-               "Good Unicode test sample.",
-       ],
-       keywords => [qw'
-               script glyph unicode writing comparison character alphabet letter
-               history phoenician latin sample test language multilingual
-       '],
+       title => $info->{pagetitle} || "$info->{title} scripts comparison sheet",
+       version => $info->{version} || '0.1',
+       description => $info->{description},
+       keywords => [@{ $info->{keywords} // []}, qw(
+               writing script glyph unicode character letter comparison history
+               alphabet sample test language multilingual
+       )],
        stylesheet => [qw'light circus dark red mono'],
-       data => [qw'writing-phnx.inc.pl'],
+       data => ["$include.inc.pl"],
 });
 
-:>
-<h1>Writing systems</h1>
-
-<p>
-Comparison of Unicode letters in related alphabets.
-Also see <a href="/charset">charsets</a>
-and <a href="/unicode">common chars</a>.</p>
+my $rows = $info->{list} or Abort(
+       "Requested script parent <q>$source</q> not available",
+       '404 request not found',
+);
 
-<div class="section">
+say "<h1>\u$info->{title} scripts</h1>";
+say "<p>$_</p>" for $info->{intro} || ();
+say '<div class="section">';
 
-<:
 use Shiar_Sheet::FormatChar;
 my $glyphs = Shiar_Sheet::FormatChar->new;
 unless (exists $get{v}) {
@@ -35,26 +35,17 @@ unless (exists $get{v}) {
        $glyphs->{style} = 'univer';
 }
 
-my $scriptname = do 'writing-script.inc.pl';
+my $scriptname = eval { Data('writing-script') }; # optional translations
 $_ = showlink($_, "/latin") for $scriptname->{latn} || ();
-
-for (
-       [phnx => 'Phoenician'],
-       [brah => 'Brahmi'],
-) {
-       my ($source, $title) = @$_;
-       my @table = do "writing-$source.inc.pl";
-       if ($! or $@) {
-               say "<h2>$title</h2>";
-               printf "<p>Table data not found: <em>%s</em>.</p>\n", $@ || $!;
-               next;
-       }
-       $glyphs->print($title => [map {
-               my $lead = s/^(-)// && $1;
-               ref $_ eq 'ARRAY' ? @$_ : map { ".>$lead$_" }
-                       $scriptname->{$source.'_'.$_} || $scriptname->{$_} || $_
-       } @table]);
-}
+$_ = showlink($_, "/writing/brahmi") for $scriptname->{brah} || ();
+
+say $glyphs->table([map {
+       my $lead = s/^(-)// && $1;
+       (map { ".>$lead$_" }
+               $scriptname->{$source.'_'.$_} || $scriptname->{$_} || $_
+       ),
+       @{ $info->{table}->{$_} || [] }
+} @{$rows}]);
 
 say "</div>\n";