File Coverage

blib/lib/Text/Amuse/Preprocessor.pm
Criterion Covered Total %
statement 174 185 94.0
branch 59 80 73.7
condition 10 15 66.6
subroutine 35 38 92.1
pod 15 15 100.0
total 293 333 87.9


line stmt bran cond sub pod time code
1             package Text::Amuse::Preprocessor;
2              
3 10     10   554189 use strict;
  10         92  
  10         296  
4 10     10   67 use warnings;
  10         22  
  10         319  
5              
6 10     10   4552 use Text::Amuse::Preprocessor::HTML;
  10         31  
  10         566  
7 10     10   3973 use Text::Amuse::Preprocessor::Parser;
  10         30  
  10         376  
8 10     10   4152 use Text::Amuse::Preprocessor::Typography qw/get_typography_filter/;
  10         62  
  10         729  
9 10     10   4520 use Text::Amuse::Preprocessor::Footnotes;
  10         36  
  10         397  
10 10     10   4745 use Text::Amuse::Functions;
  10         394236  
  10         696  
11 10     10   93 use File::Spec;
  10         27  
  10         231  
12 10     10   58 use File::Temp qw();
  10         26  
  10         161  
13 10     10   57 use File::Copy qw();
  10         24  
  10         148  
14 10     10   49 use Data::Dumper;
  10         25  
  10         21577  
15              
16             =head1 NAME
17              
18             Text::Amuse::Preprocessor - Helpers for Text::Amuse document formatting.
19              
20             =head1 VERSION
21              
22             Version 0.67
23              
24             =cut
25              
26             our $VERSION = '0.67';
27              
28              
29             =head1 SYNOPSIS
30              
31             use Text::Amuse::Preprocessor;
32             my $pp = Text::Amuse::Preprocessor->new(
33             input => $infile,
34             output => $outfile,
35             html => 1,
36             fix_links => 1,
37             fix_typography => 1,
38             fix_nbsp => 1,
39             fix_footnotes => 1
40             );
41             $pp->process;
42              
43             =head1 DESCRIPTION
44              
45             This module provides a solution to apply some common fixes to muse
46             files.
47              
48             Without any option save for C and C (which are
49             mandatory), the only things the module does is to remove carriage
50             returns, replace character ligatures or characters which shouldn't
51             enter at all and expand the tabs to 4 spaces (no smart expanding).
52              
53             =head1 LANGUAGE SUPPORT
54              
55             The following languages are supported
56              
57             =over 4
58              
59             =item english
60              
61             smart quotes, dashes, and the common superscripts (like 11th)
62              
63             =item russian
64              
65             smart quotes, dashes and non-breaking spaces
66              
67             =item spanish
68              
69             smart quotes and dashes
70              
71             =item finnish
72              
73             smart quotes and dashes
74              
75             =item swedish
76              
77             smart quotes and dashes
78              
79             =item serbian
80              
81             smart quotes and dashes
82              
83             =item croatian
84              
85             smart quotes and dashes
86              
87             =item italian
88              
89             smart quotes and dashes
90              
91             =item macedonian
92              
93             smart quotes and dashes
94              
95             =item german
96              
97             smart quotes and dashes
98              
99             =back
100              
101             =head1 ACCESSORS
102              
103             The following values are read-only and must be passed to the constructor.
104              
105             =head2 Mandatory
106              
107             =head3 input
108              
109             Can be a string (with the input file path) or a reference to a scalar
110             with the text to process).
111              
112             =head3 output
113              
114             Can be a string (with the output file path) or a reference to a scalar
115             with the processed text.
116              
117             =head2 Optional
118              
119             =head3 html
120              
121             Before doing anything, convert the HTML input into a muse file. Even
122             if possible, you're discouraged to do the html import and the fixing
123             in the same processing. Instead, create two objects, then first do the
124             HTML to muse convert, save the result somewhere, add the headers, then
125             reprocess it with the required fixes above.
126              
127             Notably, the output will be without an header, so the language will
128             not be detected.
129              
130             Default to false.
131              
132             =head3 fix_links
133              
134             Find the links and add the markup if needed. Default to false.
135              
136             =head3 fix_typography
137              
138             Apply the typographical fixes. Default to false. This add the "smart
139             quotes" feature.
140              
141             =head3 remove_nbsp
142              
143             Remove all the non-break spaces in the document, unconditionally. This
144             options does not conflict with the following. If both are provided,
145             first the non-break spaces are removed, then reinserted.
146              
147             =head3 fix_nbsp
148              
149             Add non-break spaces where appropriate (whatever this means).
150              
151             =head3 show_nbsp
152              
153             Make the non-break spaces visible and explicit as ~~ (available on
154             Text::Amuse since version 0.94).
155              
156             =head3 fix_footnotes
157              
158             Rearrange the footnotes if needed. Default to false.
159              
160             =head3 debug
161              
162             Don't unlink the temporary files and be verbose
163              
164             =head1 METHODS
165              
166             =head2 new(%options)
167              
168             Constructor. Accepts the above options.
169              
170             =cut
171              
172             sub new {
173 83     83 1 49615 my ($class, %options) = @_;
174 83         517 my $self = {
175             html => 0,
176             fix_links => 0,
177             fix_typography => 0,
178             fix_footnotes => 0,
179             remove_nbsp => 0,
180             show_nbsp => 0,
181             fix_nbsp => 0,
182             debug => 0,
183             input => undef,
184             output => undef,
185             };
186 83         384 foreach my $k (keys %$self) {
187 830 100       1553 if (exists $options{$k}) {
188 522         961 $self->{$k} = delete $options{$k};
189             }
190             }
191 83         238 $self->{_error} = '';
192 83         214 $self->{_verbatim_pieces} = {};
193 83         162 $self->{_unique_counter} = 0;
194 83 50       191 die "Unrecognized option: " . join(' ', keys %options) . "\n" if %options;
195 83 50       198 die "Missing input" unless defined $self->{input};
196 83 50       181 die "Missing output" unless defined $self->{output};
197 83         326 bless $self, $class;
198             }
199              
200             sub _get_unique_counter {
201 0     0   0 my $self = shift;
202 0         0 my $counter = ++$self->{_unique_counter};
203 0         0 return $counter;
204             }
205              
206             sub _verbatim_pieces {
207 0     0   0 return shift->{_verbatim_pieces};
208             }
209              
210             sub html {
211 83     83 1 251 return shift->{html};
212             }
213              
214             sub fix_links {
215 83     83 1 156 return shift->{fix_links};
216             }
217              
218             sub fix_typography {
219 83     83 1 158 return shift->{fix_typography};
220             }
221              
222             sub remove_nbsp {
223 83     83 1 200 return shift->{remove_nbsp};
224             }
225              
226             sub show_nbsp {
227 83     83 1 162 return shift->{show_nbsp};
228             }
229              
230             sub fix_nbsp {
231 58     58 1 215 return shift->{fix_nbsp};
232             }
233              
234             sub fix_footnotes {
235 83     83 1 269 return shift->{fix_footnotes};
236             }
237              
238             sub debug {
239 166     166 1 673 return shift->{debug};
240             }
241              
242             sub input {
243 83     83 1 164 return shift->{input};
244             }
245              
246             sub output {
247 78     78 1 186 return shift->{output};
248             }
249              
250             =head2 process
251              
252             Process C according to the options passed and write into
253             C. Return C on success, false otherwise.
254              
255             =cut
256              
257             sub _infile {
258 252     252   1420 my ($self, $arg) = @_;
259 252 100       527 if ($arg) {
260 83 50       231 die "Infile already set" if $self->{_infile};
261 83         200 $self->{_infile} = $arg;
262             }
263 252         647 return $self->{_infile};
264             }
265              
266             # temporary file for output
267             sub _outfile {
268 83     83   138 my $self = shift;
269 83         190 return File::Spec->catfile($self->tmpdir, 'output.muse');
270             }
271              
272             sub _fn_outfile {
273 24     24   47 my $self = shift;
274 24         59 return File::Spec->catfile($self->tmpdir, 'fn-out.muse');
275             }
276              
277             sub process {
278 83     83 1 1369 my $self = shift;
279 83         178 my $debug = $self->debug;
280              
281 83         188 my $wd = $self->tmpdir;
282 83 100       1140 print "# Using $wd to store temporary files\n" if $debug;
283 83         216 my $infile = $self->_set_infile;
284 83 50       1126 die "Something went wrong" unless -f $infile;
285              
286 83 50       327 if ($self->html) {
287 0         0 $self->_process_html;
288             }
289              
290             # then try to get the language
291 83         203 my ($filter, $specific_filter, $nbsp_filter);
292 83         182 my $fixlinks = $self->fix_links;
293 83         186 my $fixtypo = $self->fix_typography;
294 83         161 my $remove_nbsp = $self->remove_nbsp;
295 83         186 my $show_nbsp = $self->show_nbsp;
296 83         180 my $lang = $self->_get_lang;
297              
298 83 100 100     288 if ($lang && $fixtypo) {
299 55         195 $filter =
300             Text::Amuse::Preprocessor::TypographyFilters::filter($lang);
301 55         167 $specific_filter =
302             Text::Amuse::Preprocessor::TypographyFilters::specific_filter($lang);
303             }
304              
305 83 100 100     289 if ($lang && $self->fix_nbsp) {
306 14         48 $nbsp_filter =
307             Text::Amuse::Preprocessor::TypographyFilters::nbsp_filter($lang);
308             }
309              
310 83         208 my $outfile = $self->_outfile;
311 83         1335 my $line;
312 83         214 my @body = Text::Amuse::Preprocessor::Parser::parse_text($self->_read_file($infile));
313             # print Dumper(\@body);
314             CHUNK:
315 83         247 foreach my $piece (@body) {
316 5451 100       10347 next CHUNK if $piece->{type} ne 'text';
317             # print "Processing $piece->{type} $piece->{string}\n";
318              
319             # do the job
320 3726         5679 $line = $piece->{string};
321              
322             # some bad things we want to filter anyway
323             # $line =~ s/─/—/g; # they look the same, but they are not
324 3726         6341 $line =~ s/\x{2500}/\x{2014}/g;
325             # ligatures, totally lame to have in input file
326 3726         5678 $line =~ s/\x{fb00}/ff/g;
327 3726         5621 $line =~ s/\x{fb01}/fi/g;
328 3726         5536 $line =~ s/\x{fb02}/fl/g;
329 3726         5543 $line =~ s/\x{fb03}/ffi/g;
330 3726         5569 $line =~ s/\x{fb04}/ffl/g;
331             # remove soft-hyphens + space. They are invisible in browsers
332             # and sometimes even on the console
333 3726         5711 $line =~ s/\x{ad}\s*//g;
334 3726 100       6260 if ($remove_nbsp) {
335 226         455 $line =~ s/\x{a0}/ /g;
336 226         376 $line =~ s/~~/ /g;
337             }
338 3726 100       6161 if ($fixtypo) {
339 2152         5625 $line =~ s/(?<=\.) (?=\.)//g; # collapse the dots
340             }
341 3726 100       6296 if ($fixlinks) {
342 2030         4451 $line = Text::Amuse::Preprocessor::TypographyFilters::linkify($line);
343             }
344 3726 100       6607 if ($filter) {
345 2148         4119 $line = $filter->($line);
346             }
347 3726 100       7121 if ($specific_filter) {
348 226         448 $line = $specific_filter->($line);
349             }
350 3726 100       6059 if ($nbsp_filter) {
351 172         336 $line = $nbsp_filter->($line);
352             }
353 3726 100       6059 if ($show_nbsp) {
354 87         319 $line =~ s/\x{a0}/~~/g;
355             }
356 3726         6498 $piece->{string} = $line;
357             }
358             # write out
359 83         212 $self->_write_file($outfile, join('', map { $_->{string} } @body));
  5451         9339  
360              
361 83 100       671 if ($self->fix_footnotes) {
362 24         62 my $fn_auxfile = $self->_fn_outfile;
363 24         565 my $fnfixer = Text::Amuse::Preprocessor::Footnotes
364             ->new(input => $outfile,
365             output => $fn_auxfile);
366             # print "$outfile $fn_auxfile\n";
367 24 100       74 if ($fnfixer->process) {
368             # replace the outfile
369 19         104 $outfile = $fn_auxfile;
370             }
371             else {
372             # set the error
373 5         11 $self->_set_error({ %{ $fnfixer->error } });
  5         26  
374 5         60 return;
375             }
376             }
377              
378 78         645 my $output = $self->output;
379 78 100       242 if (my $ref = ref($output)) {
380 39 50       109 if ($ref eq 'SCALAR') {
381 39         85 $$output = $self->_read_file($outfile);
382             }
383             else {
384 0         0 die "Output is not a scalar ref!";
385             }
386             }
387             else {
388 39 50       163 File::Copy::move($outfile, $output)
389             or die "Cannot move $outfile to $output, $!";
390             }
391 78         5573 return $output;
392             }
393              
394             sub _process_html {
395 0     0   0 my $self = shift;
396             # read the infile, process, overwrite. Doc states that it's just lame.
397 0         0 my $body = $self->_read_file($self->_infile);
398 0         0 my $html = Text::Amuse::Preprocessor::HTML::html_to_muse($body);
399 0         0 $self->_write_file($self->_infile, $html);
400             }
401              
402             sub _write_file {
403 113     113   50288 my ($self, $file, $body) = @_;
404 113 50 33     514 die unless $file && $body;
405 113 50       8864 open (my $fh, '>:encoding(UTF-8)', $file) or die "opening $file $!";
406 113         12283 print $fh $body;
407 113 50       4751 close $fh or die "closing $file: $!";
408              
409             }
410              
411             sub _read_file {
412 210     210   49194 my ($self, $file) = @_;
413 210 50       536 die unless $file;
414 210 50       7562 open (my $fh, '<:encoding(UTF-8)', $file) or die "$file: $!";
415 210         15079 local $/ = undef;
416 210         6388 my $body = <$fh>;
417 210         7943 close $fh;
418 210         1981 return $body;
419             }
420              
421              
422              
423             sub _set_infile {
424 83     83   159 my $self = shift;
425 83         193 my $input = $self->input;
426 83         203 my $infile = File::Spec->catfile($self->tmpdir, 'input.txt');
427 83 100       1194 if (my $ref = ref($input)) {
428 40 50       122 if ($ref eq 'SCALAR') {
429 40 50   3   4581 open (my $fh, '>:encoding(UTF-8)', $infile) or die "$infile: $!";
  3         23  
  3         6  
  3         36  
430 40         7690 print $fh $$input;
431 40 50       1655 close $fh or die "closing $infile $!";
432 40         196 $self->_infile($infile);
433             }
434             else {
435 0         0 die Dumper($ref) . " is not a scalar ref!";
436             }
437             }
438             else {
439 43 50       161 File::Copy::copy($input, $infile) or die "Couldn't copy $input to $infile $!";
440 43         13542 $self->_infile($infile);
441             }
442 83         203 return $self->_infile;
443             }
444              
445              
446             =head2 html_to_muse
447              
448             Can be called on the class and will invoke the
449             L's C function on the
450             argument returning the converted chunk.
451              
452             =cut
453              
454             sub html_to_muse {
455 2     2 1 898 my ($self, $text) = @_;
456 2 50       8 return unless defined $text;
457 2         8 return Text::Amuse::Preprocessor::HTML::html_to_muse($text);
458             }
459              
460             =head2 error
461              
462             This is set only when processing footnotes. See
463             L documentation for the hashref
464             returned when an error has been detected.
465              
466             =cut
467              
468             sub error {
469 17     17 1 10657 return shift->{_error};
470             }
471              
472             sub _set_error {
473 5     5   13 my ($self, $error) = @_;
474 5 50       17 $self->{_error} = $error if $error;
475             }
476              
477             =head2 tmpdir
478              
479             Return the directory name used internally to hold the temporary files.
480              
481             =cut
482              
483             sub tmpdir {
484 273     273 1 416 my $self = shift;
485 273 100       915 unless ($self->{_tmpdir}) {
486 83         209 $self->{_tmpdir} = File::Temp->newdir(CLEANUP => !$self->debug);
487             }
488 273         32877 return $self->{_tmpdir}->dirname;
489             }
490              
491             sub _get_lang {
492 83     83   133 my $self = shift;
493 83         157 my $infile = $self->_infile;
494             # shouldn't happen
495 83 50 33     1051 die unless $infile && -f $infile;
496 83         227 my $info;
497 83         156 eval {
498 83         275 $info = Text::Amuse::Functions::muse_fast_scan_header($infile);
499             };
500 83 100 66     28347 if ($info && $info->{lang}) {
501 58 50       306 if ($info->{lang} =~ m/^\s*([a-z]{2,3})\s*$/s) {
502 58         308 return $1;
503             }
504             }
505 25         128 return;
506             }
507              
508              
509             =head1 AUTHOR
510              
511             Marco Pessotto, C<< >>
512              
513             =head1 BUGS
514              
515             Please report any bugs or feature requests to the author's email. If
516             you find a bug, please provide a minimal muse file which reproduces
517             the problem (so I can add it to the test suite).
518              
519             =head1 SUPPORT
520              
521             You can find documentation for this module with the perldoc command.
522              
523             perldoc Text::Amuse::Preprocessor
524              
525             Repository available at GitHub:
526             L
527              
528             =head1 SEE ALSO
529              
530             The original documentation for the Emacs Muse markup can be found at:
531             L
532              
533             The parser itself is L.
534              
535             This distribution ships the following executables
536              
537             =over 4
538              
539             =item * html-to-muse.pl (HTML to muse converter)
540              
541             =item * muse-check-footnotes.pl (footnote checker)
542              
543             =item * muse-rearrange-footnotes.pl (fix footnote numbering)
544              
545             =item * pod-to-muse.pl (POD to muse converter)
546              
547             =item * muse-preprocessor.pl (script which uses this module)
548              
549             =back
550              
551             See the manpage or pass --help to the scripts for usage.
552              
553             =head1 LICENSE
554              
555             This program is free software; you can redistribute it and/or modify it
556             under the terms of either: the GNU General Public License as published
557             by the Free Software Foundation; or the Artistic License.
558              
559             See L for more information.
560              
561              
562             =cut
563              
564             1; # End of Text::Amuse::Preprocessor