d8cb66f52f71f43b93e9778efb20f23edcee2f85
[project-aon.git] / common / scripts / gbtoepub.pl
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5
6 use File::Path qw(mkpath);
7
8 my $PROGRAM_NAME    = 'gbtoepub';
9 my $USAGE           = "$PROGRAM_NAME [options] book-code\n\t--meta=[metadata file]\n\t--xml=[book XML]\n\t--epub-xsl=[XSL transformation]\n\t--language=[language area of input data (output determined by meta file)]\n\t--font-files=[font-files]\n\t--no-validate\n\t--verbose\n";
10
11 my $FILENAME_SEPARATOR = '/';
12
13 my $RXP        = qx{which rxp};
14 my $CP         = qx{which cp};
15 my $MV         = qx{which mv};
16 my $TAR        = qx{which tar};
17 my $ZIP        = qx{which zip};
18 my $BZIP2      = qx{which bzip2};
19 my $JAVA       = qx{which java};
20 #my $XALAN_JAR  = '/usr/share/java/xalan2.jar';
21 my $XALAN_JAR  = '/usr/share/ant/lib/xalan.jar';
22 my $RM         = qx{which rm};
23 my $CHMOD      = qx{which chmod};
24
25 chomp $RXP;
26 chomp $CP;
27 chomp $MV;
28 chomp $TAR;
29 chomp $ZIP;
30 chomp $BZIP2;
31 chomp $JAVA;
32 chomp $RM;
33 chomp $CHMOD;
34
35 # Check that all the binaries are were want them
36
37 my @BINARIES;
38 push @BINARIES, ($RXP, $CP, $MV, $TAR, $ZIP, $BZIP2, $JAVA, $XALAN_JAR, $RM, $CHMOD);
39
40 foreach (@BINARIES) {
41     if ( ! -e $_ ) {
42         die "$PROGRAM_NAME: Cannot find binary '".$_."'. Please install it.\n";
43     }
44 }
45
46 ###
47
48 my $EPUB_MIMETYPE  = 'application/epub+zip';
49 my $MIMETYPE_FILE  = 'mimetype';
50 my $CONTAINER_FILE = 'container.xml';
51 my $OEBPS_DIR      = 'OEBPS';
52 my $META_INF_DIR   = 'META-INF';
53 my $NCX_FILE       = 'toc.ncx';
54 my $XHTML_EXT      = 'html';
55
56 my $PROJECT_AON_URI = 'http://www.projectaon.org';
57
58
59 ###
60
61 my $bookCode     = '';
62 my $metaFile     = '';
63 my $bookXML      = '';
64 my $ncxXSL       = 'common/xsl/epub-ncx.xsl';
65 my $epubXSL      = 'common/xsl/epub-xhtml.xsl';
66 my $metadataXSL  = 'common/xsl/epub-opf-metadata.xsl';
67 my $spineXSL     = 'common/xsl/epub-opf-spine.xsl';
68 my $fontFiles    = "common/fonts";
69 my $language     = 'en';
70
71 my $verbose = 0;
72 my $noValidate = 0;
73
74 ### read command line options
75
76 while( $#ARGV > -1 ) {
77     my $cmdLineItem = shift @ARGV;
78     if( $cmdLineItem =~ /^--meta=(.+)$/ ) {
79         $metaFile = $1;
80     }
81     elsif( $cmdLineItem =~ /^--xml=(.+)$/ ) {
82         $bookXML = $1;
83     }
84     elsif( $cmdLineItem =~ /^--epub-xsl=(.+)$/ ) {
85         $epubXSL = $1;
86     }
87     elsif( $cmdLineItem =~ /^--language=(.+)$/ ) {
88         $language = $1;
89     }
90     elsif( $cmdLineItem =~ /^--verbose/ ) {
91         $verbose = 1;
92     }
93     elsif( $cmdLineItem =~ /^--no-validate/ ) {
94         $noValidate = 1;
95     }
96     elsif( $cmdLineItem =~ /^--font-files=(.+)$/ ) {
97         $fontFiles = $1;
98     }
99     else { 
100         $bookCode = $cmdLineItem;
101     }
102 }
103
104 if( $bookCode eq '' ) { 
105     die "$PROGRAM_NAME: Unspecified book code\n$USAGE";
106 }
107 if( $metaFile eq '' ) { $metaFile = "$language/.publisher/rules/epub"; }
108 if( $bookXML eq '' ) { $bookXML = "$language/xml/$bookCode.xml"; }
109 if( $epubXSL eq '' ) {
110     die "$PROGRAM_NAME: Unspecified XSL transformation file\n$USAGE";
111 }
112
113 ### validate book XML
114
115 if( (not $noValidate) && -e $RXP ) {
116     system( $RXP, '-Vs', $bookXML ) == 0
117         or die "$PROGRAM_NAME: XML validation failed\n";
118 }
119 elsif( $noValidate ) {
120     warn "$PROGRAM_NAME: XML validation skipped - validate before publication\n";
121 }
122 else {
123     warn "$PROGRAM_NAME: XML validator not installed - validate before publication\n";
124 }
125
126 ### read in metadata file
127
128 unless( -e $metaFile && -f $metaFile && -r $metaFile ) {
129     die qq{$PROGRAM_NAME: Improper metadata file "$metaFile"\n};
130 }
131
132 open( META, '<', $metaFile ) or 
133     die qq{$PROGRAM_NAME: Unable to open metadata file "$metaFile": $!\n};
134
135 my $meta = '';
136 while( my $line = <META> ) {
137     $meta .= $line if $line !~ /^[[:space:]]*#/;
138 }
139 close META;
140
141 ### interpret rules from metadata
142 my $rulesString = '';
143 if( $meta =~ /^[[:space:]]*$bookCode[[:space:]]*{([^}]*)}/sm ) {
144     $rulesString = $1;
145 }
146 else {
147     die "$PROGRAM_NAME: Book code ($bookCode) not found in metadata file or invalid file syntax\n";
148 }
149
150 my @rules = split( /[[:space:]\n]*;[[:space:]\n]*/, $rulesString );
151 my %rulesHash;
152 foreach my $rule (@rules) {
153     if( $rule =~ /[[:space:]]*([^:]+)[[:space:]]*:[[:space:]]*(.+)$/s ) {
154         $rulesHash{ $1 } = $2;
155     }
156     else {
157         die "$PROGRAM_NAME: Unrecognized rule syntax:\n$rule\n";
158     }
159 }
160
161 unless( defined $rulesHash{'book-series'} ) {
162     die "$PROGRAM_NAME: no book series set\n";
163 }
164 unless( defined $rulesHash{'csst'} ) {
165     die "$PROGRAM_NAME: metadata file leaves CSS templates unspecified\n";
166 }
167
168 my $SERIES = get_series($rulesHash{'book-series'}) ;
169 my $SERIES_NUMBER = get_series_number($bookCode);
170
171
172 ### create output directories
173
174 my %outPath;
175 $outPath{'top'} = $rulesHash{'language'} . $FILENAME_SEPARATOR .
176                      'epub' . $FILENAME_SEPARATOR .
177                      $rulesHash{'book-series'} . $FILENAME_SEPARATOR .
178                      $bookCode;
179 # clear old files
180 if( -e "$outPath{'top'}$FILENAME_SEPARATOR$MIMETYPE_FILE" ) {
181     print qx{$RM -r $outPath{'top'}$FILENAME_SEPARATOR*};
182 }
183
184 $outPath{'meta-inf'} = $outPath{'top'} . $FILENAME_SEPARATOR . $META_INF_DIR;
185 $outPath{'oebps'} = $outPath{'top'} . $FILENAME_SEPARATOR . $OEBPS_DIR;
186
187 foreach my $directory (keys(%outPath)) {
188     unless( -e $outPath{$directory} && -d $outPath{$directory} ) {
189         mkpath $outPath{$directory}
190             or die "$PROGRAM_NAME: Unknown error creating output directory " .
191                    "\"$outPath{$directory}\"\n";
192     }
193 }
194
195 ### create content files
196
197 # the location of this tempfile also controls where the xhtml will go
198 my $tempFile = "$outPath{'oebps'}${FILENAME_SEPARATOR}foo.xml";
199 print qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$epubXSL" -OUT "$tempFile" -PARAM xhtml-ext ".$XHTML_EXT" -PARAM use-illustrators "$rulesHash{'use-illustrators'}" -PARAM language "$rulesHash{'language'}"}; #" <- comment to unconfuse VIM syntax hilighting (ugh)
200 print qx{$RM $tempFile};
201
202 foreach my $imagePath (split( /:/, $rulesHash{'images'} )) {
203     unless( -e $imagePath && -d $imagePath ) {
204     die "$PROGRAM_NAME: Image path ($imagePath) does not exist or is not a directory\n";
205 }
206     print qx{$CP $imagePath${FILENAME_SEPARATOR}* $outPath{'oebps'}};
207 }
208
209 ### create the CSS stylsheet
210
211 foreach my $cssTemplate (split( /:/, $rulesHash{'csst'} )) {
212     $cssTemplate =~ m/([^${FILENAME_SEPARATOR}]+)t$/;
213     my $templateFile = $1;
214     open( TEMPLATE, '<', $cssTemplate ) 
215         or die "$PROGRAM_NAME: Unable to open CSS template file ($cssTemplate): $!\n";
216
217     my $stylesFile = "$outPath{'oebps'}$FILENAME_SEPARATOR$templateFile";
218     open( STYLESHEET, '>', $stylesFile ) 
219         or die "$PROGRAM_NAME: Unable to open stylesheet file ($stylesFile) for writing: $!\n";
220
221     while( my $templateLine = <TEMPLATE> ) {
222         while( $templateLine =~ /%%([^%[:space:]]+)%%/ ) {
223             my $name = $1;
224             $templateLine =~ s/%%${name}%%/$rulesHash{$name}/g;
225         }
226         print STYLESHEET $templateLine;
227     }
228     close STYLESHEET;
229     close TEMPLATE;
230 }
231
232 ### copy the font files
233
234 unless( -e $fontFiles && -d $fontFiles ) {
235     die "$PROGRAM_NAME: font files directory does not exist or is not a directory \"$fontFiles\": $!\n";
236 }
237 print qx{$CP $fontFiles${FILENAME_SEPARATOR}*tf $outPath{'oebps'}};
238
239 ### write NCX file
240
241 my $uniqueID = "opf-$bookCode";
242 my $bookUniqueURI = "$PROJECT_AON_URI/$language/epub/" .
243                     "$rulesHash{'book-series'}/$bookCode/";
244
245 my $ncxFile = $outPath{'oebps'} . $FILENAME_SEPARATOR . $NCX_FILE;
246 open( NCXFILE, '>', $ncxFile ) or
247     die "$PROGRAM_NAME: unable to open NCX file for writing " .
248         "\"$ncxFile\"\n";
249 print NCXFILE qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$ncxXSL" -PARAM xhtml-ext ".$XHTML_EXT" -PARAM unique-identifier "$bookUniqueURI" -PARAM language "$rulesHash{'language'}"}; #" comment to unconfuse VIM syntax highlighting
250 close NCXFILE;
251
252 ### write mimetype file
253
254 my $mimeFile = $outPath{'top'} . $FILENAME_SEPARATOR . $MIMETYPE_FILE;
255 open( MIMETYPE, '>', $mimeFile ) or
256     die "$PROGRAM_NAME: unable to open mimetype file for writing " .
257         "\"$mimeFile\"\n";
258 print MIMETYPE $EPUB_MIMETYPE;
259 close MIMETYPE;
260
261
262 ### write OPF Root file
263 # All content files must be created prior to creating the OPF root file
264 # with its manifest of content files.
265
266 my $opfFileName = "$bookCode.opf";
267 my $opfFile = "$outPath{'oebps'}$FILENAME_SEPARATOR$opfFileName";
268 open( OPF, '>', $opfFile ) or
269     die "$PROGRAM_NAME: unable to open OPF file for writing " .
270         "\"$opfFile\"\n";
271
272 print OPF <<END_OPF_HEADER;
273 <?xml version="1.0"?>
274 <package version="2.0" 
275          xmlns="http://www.idpf.org/2007/opf" 
276          unique-identifier="$uniqueID">
277
278 END_OPF_HEADER
279
280 ## write metadata
281
282 my $metadata = qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$metadataXSL" -PARAM opf-id "$uniqueID" -PARAM unique-identifier "$bookUniqueURI" -PARAM language "$rulesHash{'language'}" -PARAM book_series "$SERIES" -PARAM book_series_index "$SERIES_NUMBER"}; #" comment to unconfuse VIM syntax hilighting
283 $metadata = " $metadata";
284 $metadata =~ s|(<dc:)|\n  $1|g;
285 $metadata =~ s|(</metadata>)|\n $1|g;
286
287 print OPF "$metadata\n\n";
288
289 ## write manifest data
290 # assuming a flat directory structure within the OEBPS directory
291
292 print OPF " <manifest>\n";
293
294 opendir( my $content_dir, $outPath{'oebps'} )
295     or die "$PROGRAM_NAME: unable to read OEBPS directory " .
296            "\"$outPath{'oebps'}\": $!\n";
297
298 while( my $content_file = readdir $content_dir ) {
299     next if $content_file eq '.';
300     next if $content_file eq '..';
301     next if $content_file =~ m/\.opf$/i;
302     print OPF qq{  <item id="};
303     print OPF make_id( $content_file );
304     print OPF qq{" href="$content_file" media-type="};
305     print OPF get_mime_type( $content_file );
306     print OPF qq{"/>\n};
307 }
308
309 closedir $content_dir;
310
311 print OPF " </manifest>\n\n";
312
313 ## write spine data
314
315 my $ncxID = make_id( $NCX_FILE );
316 my $spine = qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$spineXSL" -PARAM toc-id "$ncxID"};
317
318 $spine =~ s/idref="([^"]*)"/idref="$1.$XHTML_EXT"/g;
319 $spine = " $spine";
320 $spine =~ s|(<itemref)|\n  $1|g;
321 $spine =~ s|(</spine>)|\n $1|g;
322
323 print OPF "$spine\n\n";
324
325 # TODO: write (optional) guide data here
326 #print OPF " <guide>\n";
327 #print OPF " </guide>\n</package>";
328
329 print OPF "</package>";
330 close OPF;
331
332
333 ### write container.xml
334
335 my $containerFile = "$outPath{'meta-inf'}$FILENAME_SEPARATOR$CONTAINER_FILE";
336 open( CONTAINER, '>', $containerFile ) or
337     die "$PROGRAM_NAME: unable to open container file for writing " .
338         "\"$containerFile\"\n";
339 print CONTAINER <<END_CONTAINER;
340 <?xml version="1.0"?>
341 <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
342   <rootfiles>
343     <rootfile full-path="$OEBPS_DIR/$opfFileName"
344      media-type="application/oebps-package+xml" />
345   </rootfiles>
346 </container>
347 END_CONTAINER
348 close CONTAINER;
349
350 ### compress epub contents
351 # complies with Open Container Format 2.0.1
352 # http://idpf.org/epub/20/spec/OCF_2.0.1_draft.doc
353
354 chdir $outPath{'top'};
355
356 system( $ZIP, '-0Xq', "$bookCode.epub", $MIMETYPE_FILE );
357 system( $ZIP, '-rq', "$bookCode.epub", $META_INF_DIR );
358 system( $ZIP, '-rq', "$bookCode.epub", $OEBPS_DIR );
359
360 exit 0;
361
362 ################################################################################
363 # Subroutines
364 ################################################################################
365
366 sub make_id {
367     my ( $name ) = ( @_ );
368     $name = "_$name" if( $name =~ m/^[-.0-9]/ );
369     $name =~ tr/\x80-\xff/_/;
370     return $name
371 }
372
373 sub get_mime_type {
374     # relies on valid file name extensions
375
376     my ( $file ) = ( @_ );
377     if( $file =~ m/\.x?html?$/i ) {
378         return 'application/xhtml+xml';
379     }
380     elsif( $file =~ m/\.css$/i ) {
381         return 'text/css';
382     }
383     elsif( $file =~ m/\.png$/i ) {
384         return 'image/png';
385     }
386     elsif( $file =~ m/\.jpe?g$/i ) {
387         return 'image/jpeg';
388     }
389     elsif( $file =~ m/\.svg$/i ) {
390         return 'image/svg+xml';
391     }
392     elsif( $file =~ m/\.gif$/i ) {
393         return 'image/gif';
394     }
395     elsif( $file =~ m/\.ncx$/i ) {
396         return 'application/x-dtbncx+xml';
397     }
398     elsif( $file =~ m/\.otf$/i ) {
399         return 'application/x-font-opentype';
400     }
401     else {
402         return 'application/x-unrecognized-mime';
403     }
404 }
405
406 sub check_file {
407 # Check if a file is empty, if it is, abort as this is an indication
408 # that the previous processing step went wrong
409     my ($file) = @_;
410
411     if ( -z $file ) {
412         print  STDERR "There was an error generating $file (empty file produced)";
413         exit 1;
414     }
415
416     return 0;
417 }
418
419 #unless( $bookXML =~ m{^([-\w\@./]+)$} ) {
420 #    die "$PROGRAM_NAME: bad book XML filename \"$bookXML\"\n";
421 #}
422 #$bookXML = $1;
423 #
424 #unless( -e $bookXML && -f $bookXML && -r $bookXML ) {
425 #    die "$PROGRAM_NAME: XML does not exist or is not readable \"$bookXML\"\n";
426 #}
427 #
428 #if( -e $RXP ) {
429 #    system( $RXP, '-Vs', $bookXML ) == 0
430 #        or die "$PROGRAM_NAME: XML validation failed\n";
431 #}
432 #else {
433 #    warn "$PROGRAM_NAME: XML Validator not installed - validate before publication\n";
434 #}
435 #
436 #unless( defined $rulesHash{'language'} ) { die "$PROGRAM_NAME: Metadata file leaves language unspecified\n"; }
437 #unless( defined $rulesHash{'book-series'} ) { die "$PROGRAM_NAME: Metadata file leaves book series unspecified\n"; }
438 #unless( defined $rulesHash{'images'} ) { die "$PROGRAM_NAME: Metadata file leaves image directories unspecified\n"; }
439 #unless( defined $rulesHash{'csst'} ) { die "$PROGRAM_NAME: Metadata file leaves CSS templates unspecified\n"; }
440 #
441 #
442 #my $bookPath = "$outPath${FILENAME_SEPARATOR}";
443 #print qx{$RM ${bookPath}*} if -e $bookPath."/toc.htm";
444 #print qx{$JAVA -classpath "$XALAN_JAR" org.apache.xalan.xslt.Process -IN "$bookXML" -XSL "$xhtmlXSL" -OUT "${bookPath}foo.xml" -PARAM background-color "$rulesHash{'background-color'}" -PARAM text-color "$rulesHash{'text-color'}" -PARAM link-color "$rulesHash{'link-color'}" -PARAM use-illustrators "$rulesHash{'use-illustrators'}" -PARAM language "$rulesHash{'language'}"};
445 #print qx{$RM ${bookPath}foo.xml};
446 #
447 #
448 #
449 #print qx{$ZIP -8 -q ${bookCode}.zip ${bookPath}*};
450 #print qx{$MV ${bookCode}* $bookPath};
451 #
452 #print "Success\n" if $verbose;
453
454 # Determine series long name by the series acronym
455 sub get_series {
456     my ($series) = @_;
457     my $series_name = "";
458     if ($series eq "lw" ) {
459         $series_name = "Lone Wolf";
460     } elsif ($series eq "ls" ) {
461         $series_name = "Lobo Solitario";
462     } elsif ($series eq "gs" ) {
463         $series_name = "Grey Star the Wizard";
464     } elsif ($series eq "fw" ) {
465         $series_name = "Freeway Warrior";
466     } else {
467         print STDERR "WARN: Undefined series. Short name given: '$series'\n";
468         $series_name = "[undefined]";
469     }
470     return $series_name;
471 }
472
473 # Determine the series number based on book code
474 sub get_series_number {
475     my ($bookCode) = @_;
476     my $series_number = "";
477     if ( $bookCode =~ /^(\d\d)/ ) {
478         $series_number = $1;
479     } else {
480         print STDERR "WARN: Undefined series number. Book code is '$bookCode'.\n";
481         $series_number = "xx";
482     }
483     return $series_number;
484 }
485
486 # Determine the book title by reading the book meta information
487 sub find_title {
488     my ($book) = @_;
489     my $title = ""; my $line = "";
490     open (BOOK, "head -100 $book | ") || die ("Could not read $book: $!");
491     while ($title eq "" && ( $line = <BOOK> ) ) {
492         chomp $line;
493         if ( $line =~ /<title>(.*?)<\/title>/ ) {
494             $title = $1;
495         }
496     }
497     close BOOK;
498
499     if ( $title eq "" ) {
500         print STDERR "WARN: Cannot find title for book '$book'\n";
501         $title = "[Undefined]";
502     }
503
504     return convert_entities($title);
505 }
506
507 # Determine the book author by reading the book meta information
508 sub find_author {
509     my ($book) = @_;
510     my $author = ""; 
511     my $line = "";
512     open (BOOK, "head -100 $book |") || die ("Could not read $book: $!");
513
514     my $find_line = 0;
515     while ($author eq "" && ( $line = <BOOK> ) ) {
516         chomp $line;
517         if ( $find_line == 1 && $line =~ /<line>(.*?)<\/line>/ ) {
518             $author = $1;
519         }
520         $find_line = 1 if ( $line =~ /<creator class="medium">/ );
521         $find_line = 0 if ( $line =~ /<\/creator>/ );
522         if ( $line =~ /<creator class="author">(.*?)<\/title>/ ) {
523             $author = $1;
524         }
525     }
526     close BOOK;
527
528     if ( $author eq "" ) {
529         print STDERR "WARN: Cannot find author for book '$book'\n";
530         $author = "[Undefined]";
531     }
532
533
534     return $author;
535 }
536
537 # Determine the book illustrator by reading the book meta information
538 sub find_illustrator {
539     my ($book) = @_;
540     my $illustrator = "";
541     my $line = "";
542     open (BOOK, "head -100 $book | ") || die ("Could not read $book: $!");
543
544     my $find_line = 0;
545     while ($illustrator eq "" && ( $line = <BOOK> ) ) {
546         chomp $line;
547         if ( $find_line == 1 && $line =~ /<line>Illustrated by (.*?)<\/line>/ ) {
548             $illustrator = $1;
549         }
550         $find_line = 1 if ( $line =~ /<creator class="medium">/ );
551         $find_line = 0 if ( $line =~ /<\/creator>/ );
552         if ( $line =~ /<creator class="illustrator">(.*?)<\/title>/ ) {
553             $illustrator = $1;
554         }
555     }
556     close BOOK;
557
558     if ( $illustrator eq "" ) {
559         print STDERR "WARN: Cannot find illustrator for book '$book'\n";
560         $illustrator = "[Undefined]";
561     }
562     if ( $language eq "en" ) {
563         $illustrator = "Illustrated by ".$illustrator;
564     } elsif ( $language eq "es" ) {
565         $illustrator = "Illustrado por ".$illustrator;
566     }
567
568     return $illustrator;
569 }
570
571 sub convert_entities {
572 # Convert character entities to their correspondent values
573     my ($text) = @_;
574
575     $text =~ s/\<ch.apos\/\>/'/g; 
576     $text =~ s/\<ch.nbsp\/\>/ /g;
577     $text =~ s/\<ch.plusmn\/\>/+-/g;
578     $text =~ s/\<ch.aacute\/\>/á/g;
579     $text =~ s/\<ch.eacute\/\>/é/g;
580     $text =~ s/\<ch.iacute\/\>/í/g;
581     $text =~ s/\<ch.oacute\/\>/ó/g;
582     $text =~ s/\<ch.uacute\/\>/ú/g;
583     $text =~ s/\<ch.ntilde\/\>/ñ/g;
584     $text =~ s/\<ch.Aacute\/\>/Á/g;
585     $text =~ s/\<ch.Eacute\/\>/É/g;
586     $text =~ s/\<ch.Iacute\/\>/Í/g;
587     $text =~ s/\<ch.Oacute\/\>/Ó/g;
588     $text =~ s/\<ch.Uacute\/\>/Ú/g;
589     $text =~ s/\<ch.auml\/\>/ä/g;
590     $text =~ s/\<ch.euml\/\>/ë/g;
591     $text =~ s/\<ch.iuml\/\>/ï/g;
592     $text =~ s/\<ch.ouml\/\>/ö/g;
593     $text =~ s/\<ch.uuml\/\>/ü/g;
594     $text =~ s/\<ch.Ntilde\/\>/Ñ/g;
595     $text =~ s/\<ch.acute\/\>/´/g;
596     $text =~ s/\<ch.iexcl\/\>/¡/g;
597     $text =~ s/\<ch.iquest\/\>/¿/g;
598     $text =~ s/\<ch.laquo\/\>/«/g;
599     $text =~ s/\<ch.raquo\/\>/»/g;
600     $text =~ s/\<ch.ampersand\/\>/&/g;
601
602     return $text;
603 }
604
605 # Quote metacaracters for shell use
606 sub quote_shell {
607     my ($text) = @_;
608     $text =~ s/'/\\'/g; 
609     return $text;
610 }