File Coverage

blib/lib/XML/UM.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             #
2             # TO DO:
3             #
4             # - Implement SlowMappers for expat builtin encodings (for which there
5             # are no .enc files), e.g. UTF-16, US-ASCII, ISO-8859-1.
6             # - Instead of parsing the .xml file with XML::Encoding, we should use XS.
7             # If this will not be implemented for a while, we could try reading the
8             # .enc file directly, instead of the .xml file.
9             # I started writing XML::UM::EncParser to do this (see EOF), but got stuck.
10             #
11              
12 1     1   22813 use strict;
  1         3  
  1         57  
13              
14             package XML::UM::SlowMapper;
15 1     1   7 use Carp;
  1         2  
  1         84  
16 1     1   24068 use XML::Encoding;
  0            
  0            
17              
18             use vars qw{ $VERSION $ENCDIR %DEFAULT_ASCII_MAPPINGS };
19             $VERSION = '0.01';
20              
21             my $UTFCHAR = '[\\x00-\\xBF]|[\\xC0-\\xDF].|[\\xE0-\\xEF]..|[\\xF0-\\xFF]...';
22              
23             #
24             # The directory that contains the .xml files that come with XML::Encoding.
25             # Include the terminating '\' or '/' !!
26             #
27             $ENCDIR = "/home1/enno/perlModules/XML-Encoding-1.01/maps/";
28             #$ENCDIR = "c:\\src\\perl\\xml\\XML-Encoding-1.01\\maps\\";
29              
30             #
31             # From xmlparse.h in expat distribution:
32             #
33             # Expat places certain restrictions on the encodings that are supported
34             # using this mechanism.
35             #
36             # 1. Every ASCII character that can appear in a well-formed XML document,
37             # other than the characters
38             #
39             # $@\^`{}~
40             #
41             # must be represented by a single byte, and that byte must be the
42             # same byte that represents that character in ASCII.
43             #
44             # [end of excerpt]
45              
46             #?? Which 'ASCII characters can appear in a well-formed XML document ??
47              
48             # All ASCII codes 0 - 127, excl. 36,64,92,94,96,123,125,126 i.e. $@\^`{}~
49             %DEFAULT_ASCII_MAPPINGS = map { (chr($_), chr($_)) } (0 .. 35, 37 .. 63,
50             65 .. 91, 93, 95,
51             97 .. 122, 124, 127);
52              
53             sub new
54             {
55             my ($class, %hash) = @_;
56             my $self = bless \%hash, $class;
57            
58             $self->read_encoding_file;
59              
60             $self;
61             }
62              
63             sub dispose
64             {
65             my $self = shift;
66             $self->{Factory}->dispose_mapper ($self);
67             delete $self->{Encode};
68             }
69              
70             # Reads the XML file that contains the encoding definition.
71             # These files come with XML::Encoding.
72             sub read_encoding_file
73             {
74             #?? This should parse the .enc files (the .xml files are not installed) !!
75              
76             my ($self) = @_;
77             my $encoding = $self->{Encoding};
78              
79             # There is no .enc (or .xml) file for US-ASCII, but the mapping is simple
80             # so here it goes...
81             if ($encoding eq 'US-ASCII')
82             {
83             $self->{EncMapName} = 'US-ASCII';
84             $self->{Map} = \%DEFAULT_ASCII_MAPPINGS; # I hope this is right
85             return;
86             }
87              
88             my $file = $self->find_encoding_file ($encoding);
89            
90             my %uni = %DEFAULT_ASCII_MAPPINGS;
91             my $prefix = "";
92             my $DIR = "file:$ENCDIR";
93              
94             my $enc = new XML::Encoding (Handlers => {
95             Init =>
96             sub {
97             my $base = shift->base ($DIR);
98             }
99             },
100              
101             PushPrefixFcn =>
102             sub {
103             $prefix .= chr (shift);
104             undef;
105             },
106              
107             PopPrefixFcn =>
108             sub {
109             chop $prefix;
110             undef;
111             },
112              
113             RangeSetFcn =>
114             sub {
115             my ($byte, $uni, $len) = @_;
116             for (my $i = $uni; $len--; $uni++)
117             {
118             $uni{XML::UM::unicode_to_utf8($uni)} = $prefix . chr ($byte++);
119             }
120             undef;
121             });
122              
123             $self->{EncMapName} = $enc->parsefile ($file);
124              
125             #print "Parsed Encoding " . $self->{Encoding} . " MapName=" . $self->{EncMapName} . "\n";
126              
127             $self->{Map} = \%uni;
128             }
129              
130             sub find_encoding_file
131             {
132             my ($self, $enc) = @_;
133              
134             return "$ENCDIR\L$enc\E.xml"; # .xml filename is lower case
135             }
136              
137             # Returns a closure (method) that converts a UTF-8 encoded string to an
138             # encoded byte sequence.
139             sub get_encode
140             {
141             my ($self, %hash) = @_;
142             my $MAP = $self->{Map};
143             my $ENCODE_UNMAPPED = $hash{EncodeUnmapped} || \&XML::UM::encode_unmapped_dec;
144              
145             my $code = "sub {\n my \$str = shift;\n \$str =~ s/";
146              
147             $code .= "($UTFCHAR)/\n";
148             $code .= "defined \$MAP->{\$1} ? \$MAP->{\$1} : ";
149             $code .= "\&\$ENCODE_UNMAPPED(\$1) /egs;\n";
150              
151             $code .= "\$str }\n";
152             # print $code;
153              
154             my $func = eval $code;
155             croak "could not eval generated code=[$code]: $@" if $@;
156              
157             $func;
158             }
159              
160             #
161             # Optimized version for when the encoding is UTF-8.
162             # (In that case no conversion takes place.)
163             #
164             package XML::UM::SlowMapper::UTF8;
165             use vars qw{ @ISA };
166             @ISA = qw{ XML::UM::SlowMapper };
167              
168             sub read_encoding_file
169             {
170             # ignore it
171             }
172              
173             sub get_encode
174             {
175             \&dont_convert;
176             }
177              
178             sub dont_convert # static
179             {
180             shift # return argument unchanged
181             }
182              
183             package XML::UM::SlowMapperFactory;
184              
185             sub new
186             {
187             my ($class, %hash) = @_;
188             bless \%hash, $class;
189             }
190              
191             sub get_encode
192             {
193             my ($self, %options) = @_;
194             my $encoding = $options{Encoding};
195              
196             my $mapper = $self->get_mapper ($encoding);
197             return $mapper->get_encode (%options);
198             }
199              
200             sub get_mapper
201             {
202             my ($self, $encoding) = @_;
203             $self->{Mapper}->{$encoding} ||=
204             ($encoding eq "UTF-8" ?
205             new XML::UM::SlowMapper::UTF8 (Encoding => $encoding,
206             Factory => $self) :
207             new XML::UM::SlowMapper (Encoding => $encoding,
208             Factory => $self));
209             }
210              
211             #
212             # Prepare for garbage collection (remove circular refs)
213             #
214             sub dispose_encoding
215             {
216             my ($self, $encoding) = @_;
217             my $mapper = $self->{Mapper}->{$encoding};
218             return unless defined $mapper;
219              
220             delete $mapper->{Factory};
221             delete $self->{Mapper}->{$encoding};
222             }
223              
224             package XML::UM;
225             use Carp;
226              
227             use vars qw{ $FACTORY %XML_MAPPING_CRITERIA };
228             $FACTORY = XML::UM::SlowMapperFactory->new;
229              
230             sub get_encode # static
231             {
232             $FACTORY->get_encode (@_);
233             }
234              
235             sub dispose_encoding # static
236             {
237             $FACTORY->dispose_encoding (@_);
238             }
239              
240             # Convert UTF-8 byte sequence to Unicode index; then to '&#xNN;' string
241             sub encode_unmapped_hex # static
242             {
243             my $n = utf8_to_unicode (shift);
244             sprintf ("&#x%X;", $n);
245             }
246              
247             sub encode_unmapped_dec # static
248             {
249             my $n = utf8_to_unicode (shift);
250             "&#$n;"
251             }
252              
253             # Converts a UTF-8 byte sequence that represents one character,
254             # to its Unicode index.
255             sub utf8_to_unicode # static
256             {
257             my $str = shift;
258             my $len = length ($str);
259              
260             if ($len == 1)
261             {
262             return ord ($str);
263             }
264             if ($len == 2)
265             {
266             my @n = unpack "C2", $str;
267             return (($n[0] & 0x3f) << 6) + ($n[1] & 0x3f);
268             }
269             elsif ($len == 3)
270             {
271             my @n = unpack "C3", $str;
272             return (($n[0] & 0x1f) << 12) + (($n[1] & 0x3f) << 6) +
273             ($n[2] & 0x3f);
274             }
275             elsif ($len == 4)
276             {
277             my @n = unpack "C4", $str;
278             return (($n[0] & 0x0f) << 18) + (($n[1] & 0x3f) << 12) +
279             (($n[2] & 0x3f) << 6) + ($n[3] & 0x3f);
280             }
281             else
282             {
283             croak "bad UTF8 sequence [$str] hex=" . hb($str);
284             }
285             }
286              
287             # Converts a Unicode character index to the byte sequence
288             # that represents that character in UTF-8.
289             sub unicode_to_utf8 # static
290             {
291             my $n = shift;
292             if ($n < 0x80)
293             {
294             return chr ($n);
295             }
296             elsif ($n < 0x800)
297             {
298             return pack ("CC", (($n >> 6) | 0xc0), (($n & 0x3f) | 0x80));
299             }
300             elsif ($n < 0x10000)
301             {
302             return pack ("CCC", (($n >> 12) | 0xe0), ((($n >> 6) & 0x3f) | 0x80),
303             (($n & 0x3f) | 0x80));
304             }
305             elsif ($n < 0x110000)
306             {
307             return pack ("CCCC", (($n >> 18) | 0xf0), ((($n >> 12) & 0x3f) | 0x80),
308             ((($n >> 6) & 0x3f) | 0x80), (($n & 0x3f) | 0x80));
309             }
310             croak "number [$n] is too large for Unicode in \&unicode_to_utf8";
311             }
312              
313             #?? The following package is unfinished.
314             #?? It should parse the .enc file and create an array that maps
315             #?? Unicode-index to encoded-str. I got stuck...
316              
317             # package XML::UM::EncParser;
318             #
319             # sub new
320             # {
321             # my ($class, %hash) = @_;
322             # my $self = bless \%hash, $class;
323             # $self;
324             # }
325             #
326             # sub parse
327             # {
328             # my ($self, $filename) = @_;
329             # open (FILE, $filename) || die "can't open .enc file $filename";
330             # binmode (FILE);
331             #
332             # my $buf;
333             # read (FILE, $buf, 4 + 40 + 2 + 2 + 1024);
334             #
335             # my ($magic, $name, $pfsize, $bmsize, @map) = unpack ("NA40nnN256", $buf);
336             # printf "magic=%04x name=$name pfsize=$pfsize bmsize=$bmsize\n", $magic;
337             #
338             # if ($magic != 0xFEEBFACE)
339             # {
340             # close FILE;
341             # die sprintf ("bad magic number [0x%08X] in $filename, expected 0xFEEBFACE", $magic);
342             # }
343             #
344             # for (my $i = 0; $i < 256; $i++)
345             # {
346             # printf "[%d]=%d ", $i, $map[$i];
347             # print "\n" if ($i % 8 == 7);
348             # }
349             #
350             # for (my $i = 0; $i < $pfsize; $i++)
351             # {
352             # print "----- PrefixMap $i ----\n";
353             # read (FILE, $buf, 2 + 2 + 32 + 32);
354             # my ($min, $len, $bmap_start, @ispfx) = unpack ("CCnC64", $buf);
355             # my (@ischar) = splice @ispfx, 32, 32, ();
356             # #?? could use b256 instead of C32 for bitvector a la vec()
357             #
358             # print "ispfx=@ispfx\n";
359             # print "ischar=@ischar\n";
360             # $len = 256 if $len == 0;
361             #
362             # print " min=$min len=$len bmap_start=$bmap_start\n";
363             # }
364             #
365             # close FILE;
366             # }
367              
368             1; # package return code
369              
370             __END__