sábado, 21 de noviembre de 2009

Procesando POD con Pod::Simple

[English translation]
En el artículo anterior logramos convertir POD a HTML con relativa facilidad para un minúsculo servidor de documentos hecho con CGI, hoy voy a expandir el programa, permitiendo la visualización de documentos POD en una docena de formas diferentes.


Una opción útil cuando veo la documentación del CPAN, es la capacidad de mostrar las fuentes de los módulos, así que voy a agregar un enlace que permita visualizar la fuente de un documente, voy a poner el enlace al final del documento, configurando el footer de la conversión HTML, también debo agregar lógica para reconocer el nuevo tipo de enlaces.

Agregaré un parámetro de formato (format) al query, y lo interpretaré con el given de la línea 12, además para ser compatible con la versión anterior, permitiré que el formato sea opcional, y su valor por omisión será HTML (línea 11):

 1 #!/usr/bin/perl
 2 
 3 use Modern::Perl;
 4 use CGI;
 5 use CGI::Carp 'fatalsToBrowser';
 6 use Pod::Simple::Search;
 7 use Pod::Simple::HTML;
 8 
 9 my $q        = new CGI;
10 my $filename = Pod::Simple::Search->new->inc(1)->find( $q->param("pod") );
11 my $format   = $q->param("format") || "HTML";
12 given ($format) {
13     when ("source") {
14         print $q->header("text/plain");
15         open POD, $filename;
16         print $_ while (<POD>);
17     }
18     when ('HTML') {
19         my $parser = Pod::Simple::HTML->new;
20         print $q->header("text/html");
21         $parser->perldoc_url_prefix( $q->url( -path_info => 1 ) . "?pod=" );
22         my $footer = "<hr>" . make_link("source");
23         $parser->html_footer(qq[\n<!-- end doc -->\n\n$footer</body></html>\n]);
24         $parser->output_fh(*STDOUT);
25         $parser->parse_file($filename);
26     }
27     default {
28         die("Formato desconocido '$format'");
29     }
30 }
31 
32 sub make_link {
33     my $fmt = shift;
34     $q->a( { href => $q->url( -path_info => 1, -query => 1 ) . "\&format=$fmt" }, $fmt );
35 }

Más del doble de las líneas de la versión anterior, sin embargo esta arquitectura muy pronto mostrará ser flexible en combinación con Pod::Simple y sus amigos. Mostrar las fuentes, en realidad es una tontería, solamente se envía un encabezado (línea 14) y luego el resto del archivo sin transformar.

La rutina make_link, facilita la creación de enlaces con el nuevo parámetro (format), a partir de la dirección que estamos visitando (ahora incluye también el query), y aunque solo se usa una vez (línea 22), la usaremos más a medida que agreguemos formatos de conversión a la aplicación.

Otro módulo que usé fue CGI::Carp con la opción "fatalsToBrowser", esto sirve para que los errores del programa se puedan ver en el navegador, si quieres probarlo, solo debes poner un parámetro format con algo desconocido para el programa y verás el mensaje de error en el navegador.

Una vez dicho esto voy a transformar POD a Wiki, para ello usaré "Pod::Simple::Wiki" que tiene conversores para al menos 9 formatos de wiki diferentes, así que sin importar si usas Mediawiki o Twiki, siempre puedes escribir tus artículos en POD :-)

Como Perl es dinámico, versatil y fácil voy a agregar todos los formatos de una vez, para lo cual requiero un arreglo con los formatos que voy a soportar (línea 9) y un mapa que me permita asociar cada formato con algún traductor de POD (línea 10):

 1 #!/usr/bin/perl
 2
 3 use Modern::Perl;
 4 use CGI;
 5 use CGI::Carp 'fatalsToBrowser';
 6 use Pod::Simple::Search;
 7 use Pod::Simple::HTML;
 8 
 9 my @wikis   = qw(Usemod Twiki Template Kwiki Confluence Moinmoin Tiddlywiki Mediawiki Textile);
10 my %formats = (
11     ( map { $_ => "Pod::Simple::Wiki::$_" } @wikis )
12 );
13 
14 my $q        = new CGI;
15 my $filename = Pod::Simple::Search->new->inc(1)->find( $q->param("pod") );
16 my $format   = $q->param("format") || "HTML";
17 given ($format) {
18     when ("source") {
19         print $q->header("text/plain");
20         open POD, $filename;
21         print $_ while (<POD>);
22     }
23     when ('HTML') {
24         my $parser = Pod::Simple::HTML->new;
25         print $q->header("text/html");
26         $parser->perldoc_url_prefix( $q->url( -path_info => 1 ) . "?pod=" );
27         my $footer = "<hr>" . make_link("source")
28             . " | Wiki formats: "
29             . join( " ", map { make_link($_) } @wikis );
30         $parser->html_footer(qq[\n<!-- end doc -->\n\n$footer</body></html>\n]);
31         $parser->output_fh(*STDOUT);
32         $parser->parse_file($filename);
33     }
34     when (%formats) {
35         my $class = $formats{$format};
36         eval "require $class";
37         print $q->header( "text/plain" );
38         $class->filter($filename);
39     }
40     default {
41         die("Formato desconocido '$format'");
42     }
43 }
44 
45 sub make_link {
46     my $fmt = shift;
47     $q->a( { href => $q->url( -path_info => 1, -query => 1 ) . "\&format=$fmt" }, $fmt );
48 }

Todo el trabajo se hace al reconocer alguno de los nuevos formatos en la línea 34, allí obtenemos la clase de "Pod::Simple::Wiki" que implementa la conversión y la cargamos dinámicamente con require dentro de un eval (línea 36), de esta manera no tendremos que cargar todas las clases al principio de nuestro programa, utilizando sólo lo necesario para la conversión deseada, luego se envía el tipo de contenidos al navegador y se hace la conversión según clase de wiki deseado.

Ahora solo queda incluir los enlaces a los diferentes formatos en el footer, lo que se hace durante la generación de la página en HTML (líneas 27 a 29).

Si quisieras incluir parte de la documentación en un manual impreso probablemente querrás convertir POD, para las herramientas más adaptadas a este trabajo, vamos a generar RTF y LaTeX a partir de POD, no debe ser muy difícil porque ya existen las clases en el CPAN, lo primero es generalizar  el tipo de contenido enviado al navegador, permitiendo usar el mismo código para diversos formatos:

37         print $q->header( $content_types{$format} || "text/plain" );

Esto asume que existe un hash que tiene los formatos que manejaremos asociados a sus tipos de contenido, utilizaremos también este mapa para  generar los enlaces a los nuevos tipos de contenido:

27         my $footer = "<hr>" . make_link("source")
28             . join( " | ", map { make_link($_) } keys %content_types )
29             . " Wiki formats: "
30             . join( " | ", map { make_link($_) } @wikis );

El mapa de formatos y tipos de contenido se puede agregar al principio:

 9 my %content_types = (
10     RTF    => "application/rtf",
11     LaTeX  => "application/x-latex",
12 );

y no olvidemos que todo formato debe estar asociado a la clase que hace la conversión en %formats, de lo contrario el formato no será reconocido en el given:

14 my %formats = (
15     ( map { $_ => "Pod::Simple::$_" } keys %content_types ),
16     ( map { $_ => "Pod::Simple::Wiki::$_" } @wikis )
17 );

Ahora podemos convertir a RTF, lo que seguramente arrancará tu suite de oficina favorita, y en el caso de LaTeX probablemente se descargará el archivo.

Para cerrar, voy a incluir un último formato: PDF, este será más complejo porque no hay un módulo en el CPAN que transforme POD a PDF, por ello voy a hacerme uno, basado en Pod::Simple (línea 5), que use LaTeX como formato intermedio para crear los PDF.

 1 package Pod::Simple::PDF;
 2 
 3 use Modern::Perl;
 4 use Pod::Simple::LaTeX;
 5 use base "Pod::Simple";
 6 
 7 use File::Temp;
 8 use File::Spec::Functions;
 9 use IO::File;
10 use IO::Handle;
11 
12 sub new {
13     my $class = shift;
14     return bless { output_fh => \*STDOUT }, ref $class || $class;
15 }
16 
17 sub parse_file {
18     my $self = shift;
19     my $file = shift;
20 
21     my $dir      = File::Temp->newdir();
22     my $tex_name = catfile( $dir, "pod.tex" );
23     my $texf     = IO::File->new( $tex_name, "w" );
24     my $parser   = Pod::Simple::LaTeX->new;
25     $parser->output_fh($texf);
26     $parser->parse_file($file);
27     $texf->close;
28     `cd '$dir'; pdflatex '$tex_name'; pdflatex '$tex_name'`;
29     my $in = IO::File->new( catfile( $dir, "pod.pdf" ), "r" );
30     $self->{'output_fh'}->print($_) while readline($in);
31 }
32 
33 1;

Tal vez la razón por la cual no hay conversor a PDF es porque no hay una manera muy portable de hacerlo, yo voy usar la herramienta pdflatex que es parte de TeX Live, porque supongo que se puede instalar tanto en Unix como en Windows, sin embargo cualquier distribución moderna de TeX en unix debería incluir esta herramienta.

El método parse_file crea un directorio temporal usando File::Temp->newdir
para luego crear el archivo pod.tex dentro del directorio, que se usa para almacenar el resultado de la conversión realizada con Pod::Simple::LaTeX, este archivo ahora se procesa con el comando 'pdflatex' (línea 27) que produce el archivo 'pod.pdf' (y uno que otro archivo inútil) en el directorio temporal.

Muchas cosas pueden salir mal durante en la línea 27 porque el método simple que estoy ofrece muy poco control sobre lo que allí sucede, en una implementación mejorada habría que usar módulos como IPC::Run3 para controlar la ejecución de las herramientas y actuar correctamente ante las diversas fallas que pudieran ocurrir, sin embargo una de las caracteristicas interesantes de Perl es que se pueden hacer prototipos como este rápidamente y después se pueden refinar.

En las líneas 28 y 29 se transmite 'pod.pdf' al navegador, y al terminar el método parse file, la variable $dir sale de contexto y el objeto File::Temp se destruye, eliminando el directorio temporal y todo lo que este dentro del mismo.

Una vez que este módulo se guarda en el lugar apropiado, lo que hace automáticamente el CPAN si el módulo esta empaquetado según las instrucciones de perlmodlib (aunque ahora solo para probar puedes poner el archivo PDF.pm en el mismo directorio donde se encuentra el archivo HTML.pm que contiene Pod::Simple::HTML).

Finalmente hay que agregar el nuevo tipo de contenido (PDF) a la aplicación lo cual es tan simple como agregar una sola línea al hash %content_types (línea 12) sin necesidad de tocar mas nada, ya tenemos un servidor capaz de mostrar POD en mas de una docena de formatos:

 1 #!/usr/bin/perl
 2 
 3 use Modern::Perl;
 4 use CGI;
 5 use CGI::Carp 'fatalsToBrowser';
 6 use Pod::Simple::Search;
 7 use Pod::Simple::HTML;
 8 
 9 my %content_types = (
10     RTF    => "application/rtf",
11     LaTeX  => "application/x-latex",
12     PDF    => "application/pdf",
13 );
14 my @wikis   = qw(Usemod Twiki Template Kwiki Confluence Moinmoin Tiddlywiki Mediawiki Textile);
15 my %formats = (
16     ( map { $_ => "Pod::Simple::$_" } keys %content_types ),
17     ( map { $_ => "Pod::Simple::Wiki::$_" } @wikis )
18 );
19 
20 my $q        = new CGI;
21 my $filename = Pod::Simple::Search->new->inc(1)->find( $q->param("pod") );
22 my $format   = $q->param("format") || "HTML";
23 given ($format) {
24     when ("source") {
25         print $q->header("text/plain");
26         open POD, $filename;
27         print $_ while (<POD>);
28     }
29     when ('HTML') {
30         my $parser = Pod::Simple::HTML->new;
31         print $q->header("text/html");
32         $parser->perldoc_url_prefix( $q->url( -path_info => 1 ) . "?pod=" );
33         my $footer = "<hr>"
34             . join( " ", map { make_link($_) } "source", keys %content_types )
35             . " | Wiki formats: "
36             . join( " ", map { make_link($_) } @wikis );
37         $parser->html_footer(qq[\n<!-- end doc -->\n\n$footer</body></html>\n]);
38         $parser->output_fh(*STDOUT);
39         $parser->parse_file($filename);
40     }
41     when (%formats) {
42         my $class = $formats{$format};
43         eval "require $class";
44         print $q->header( $content_types{$format} || "text/plain" );
45         $class->filter($filename);
46     }
47     default {
48         die("Formato desconocido '$format'");
49     }
50 }
51 
52 sub make_link {
53     my $fmt = shift;
54     $q->a( { href => $q->url( -path_info => 1, -query => 1 ) . "\&format=$fmt" }, $fmt );
55 }

No hay comentarios:

Publicar un comentario