update personal url domains to shiar.nl
[sheet.git] / Shiar_Sheet / Keyboard.pm
1 package Shiar_Sheet::Keyboard;
2
3 use strict;
4 use warnings;
5 no  warnings 'uninitialized';  # save some useless checks for more legible code
6 use Carp;
7
8 our $VERSION = '1.01';
9
10 my @casedesc = qw(ctrl shift);
11 my @rowdesc = qw(numeric top home bottom);
12 my %keyrows = do 'keys.inc.pl';
13
14 my %keytrans = qw(
15         ^@ NUL ^a SOH ^b STX ^c ETX  ^d EOT ^e ENQ ^f ACK ^g BEL
16         ^h BS  ^i tab ^j LF  ^k VT   ^l FF  ^m CR  ^n SO  ^o SI
17         ^p DLE ^q DC1 ^r DC2 ^s DC3  ^t DC4 ^u NAK ^v SYN ^w ETB
18         ^x CAN ^y EM  ^z SUB ^[ ESC  ^\ FS  ^] GS  ^^ RS  ^_ US
19         ^? DEL
20 );
21
22 sub new {
23         my $self = shift;
24         my ($keys) = @_;
25
26         croak 'Invalid key table specified' unless ref $keys eq 'HASH';
27         my $parent = (caller)[0];  # calling module
28         my $sign = do {
29                 no strict 'refs';  # temporarily allow variable references
30                 \%{ $parent.'::sign' };  # return %sign from parent
31         };
32         croak "%${parent}::sign not found" unless %$sign;
33
34         bless {sign => $sign, keys => $keys, map => 'qwerty'}, $self;
35 }
36
37 sub map {
38         my $self = shift;
39         my ($mapname) = @_;
40
41         return $self->{map} = $mapname if defined $keyrows{$mapname};
42         return undef;
43 }
44
45 sub escapeclass {
46         local $_ = shift;
47         s/\^/_c/g;
48         s/\[/_sbo/g;
49         s/\]/_sbc/g;
50         s/^$/_/;
51         return $_;
52 }
53
54 sub escapehtml {
55         local $_ = shift;
56         s/</&lt;/g;
57         s/>/&gt;/g;
58         s/  / &nbsp;/g;
59         return $_;
60 }
61
62 sub keyunalias {
63         my $self = shift;
64         my ($key, $ancestry) = @_;
65
66         $key =~ s/(\S*?)(\^?\S)($|\s.*)/$2/;
67         my $mode = $1;
68         my $keyinfo = $self->{keys}->{$mode}->{$key};
69
70         return [] unless defined $keyinfo;
71         return $keyinfo if ref $keyinfo;
72         return if $ancestry->{$key}++;  # endless loop failsafe
73         return $self->keyunalias($keyinfo, $ancestry);
74 }
75
76 sub print_key {
77         my $self = shift;
78         my ($mode, $key, $keyinfo) = @_;
79
80         $keyinfo = [ $self->{sign}->{alias}.$keyinfo, $self->keyunalias($keyinfo)->[1] . ' alias' ]
81                 if defined $keyinfo and not ref $keyinfo;  # alias
82         my ($desc, $flags, $mnem) = @$keyinfo if defined $keyinfo;
83         defined $desc or $flags = $key eq '^0' ? 'ni' : 'no';
84
85 #       $key = $keytrans{$key} if defined $keytrans{$key};
86         my $keytxt = $mode . escapehtml($key) if $key ne '^0';
87            $keytxt .= $self->{sign}->{$1} while $flags =~ s/(?:^| )(arg[a-ln-z]?)\b//;  # arguments
88            $keytxt .= "<small>$self->{sign}->{motion}</small>" if $flags =~ s/ ?\bargm\b//;  # motion argument
89            $keytxt =~ s{\^(?=.)}{<small>^</small>};  # element around ctrl-identifier
90         my $onclick = $flags =~ s/ ?\bmode(\S*)// && defined $self->{keys}{$1} && sprintf(
91                 ' onclick="setmode(%s)"',
92                 $1 eq '' ? '' : sprintf(q{'mode%s'}, escapeclass($1))
93         );
94         $onclick .= sprintf(q{ onclick="document.location='%s'"}, $1)
95                 if $flags =~ s/ ?\blink(\S*)//;
96         my $keyhint = defined($mnem) && qq{ title="$mnem"};
97
98         print qq{\t\t<li class="$flags"$onclick><b$keyhint>$keytxt</b>};
99         print ' ', $desc if defined $desc;
100         print "\n";
101 }
102
103 sub print_rows {
104         my $self = shift;
105         my $static = shift;
106         my @moderows = $static ? split(/\s+/, $static) : sort keys %{ $self->{keys} };
107
108         for (my $row = 0; $row <= $#{ $keyrows{$self->{map}} }; $row++) {
109                 my $keyrow = $keyrows{$self->{map}}->[$row];
110                 my @caserows = 0 .. $#$keyrow;
111
112                 print qq{<li class="row row$row"><ul>\n};
113                 for my $modefull (@moderows) {
114                         my $mode = $modefull;
115                         my @showcase = $mode =~ s/(\d+)(?:-(\d+))?$//
116                                 ? (map {3 - $_} split //, $row == 0 && $2 || $1) : @caserows;
117                         my $modekeys = $self->{keys}{$mode};
118
119                         for my $case (@showcase) {
120                                 my $keycase = $keyrow->[$case] or next;
121                                   @$keycase or next;
122
123                                 printf "\t<li%s>", $mode ne '' && sprintf(
124                                         ' class="%s"', ($static ? '' : 'mode ') . 'mode' . escapeclass($mode)
125                                 );
126                                 printf("<h3>%s<small>: %s</small></h3>\n", # XXX insert &nbsp; here to fix msie<=6
127                                                 $modekeys->{desc} || "mode $mode",
128                                                 "$rowdesc[$row] row $casedesc[$case]"
129                                 );
130                                 my $caseclass = 'keys';
131                                    $caseclass .= ' lead' if defined $modekeys->{lead};  # leading command key shown
132                                    $caseclass .= " $casedesc[$case]" if defined $casedesc[$case];
133                                 print qq{\t\t<ul class="$caseclass">\n};
134                                 $self->print_key($modekeys->{lead}, $_, $modekeys->{$_}) for @$keycase;
135                                 print qq{\t\t</ul>\n};
136                         } # case
137
138                 } # mode
139                 print qq{\t</ul>\n};
140         } # row
141 }
142
143 1;
144
145 =head1 NAME
146
147 Shiar_Sheet::Keyboard - Output HTML for key sheets
148
149 =head1 SYNOPSIS
150
151         our %sign = (alias => 'see: ');
152         
153         my $keys = Shiar_Sheet::Keyboard({
154                 'mode' => {
155                         desc => 'mode description',
156                         'A' => 'a', # alias
157                         'a' => ['description', 'classes', 'comments (on hover)'],
158                 },
159         });
160         $keys->map('dvorak') or die "Keyboard map not found";
161         
162         $keys->print_rows;
163
164 =head1 DESCRIPTION
165
166 Used by http://sheet.shiar.nl to display keyboard sheets.
167 Assumes specific stylesheets and javascript from this site,
168 so probably not of much use elsewhere.
169
170 =head1 AUTHOR
171
172 Mischa POSLAWSKY <perl@shiar.nl>
173
174 =head1 LICENSE
175
176 Licensed under the GNU Affero General Public License version 3.
177