stas 02/01/05 10:51:59 Added: lib DocSet.pm lib/DocSet 5005compat.pm Cache.pm Config.pm Doc.pm DocSet.pm NavigateCache.pm RunTime.pm Util.pm lib/DocSet/Doc HTML2HTML.pm POD2HTML.pm Text2HTML.pm lib/DocSet/DocSet HTML.pm PSPDF.pm lib/DocSet/Source HTML.pm POD.pm Text.pm lib/DocSet/Template/Plugin NavigateCache.pm Log: - add the DocSet package locally until it gets released on CPAN Revision Changes Path 1.1 modperl-docs/lib/DocSet.pm Index: DocSet.pm =================================================================== package DocSet; $VERSION = '0.08'; =head1 NAME DocSet - documentation projects builder in HTML, PS and PDF formats =head1 SYNOPSIS pod2hpp [options] base_full_path relative_to_base_configuration_file_location Options: -h this help -v verbose -i podify pseudo-pod items (s/^* /=item */) -s create the splitted html version (not implemented) -t create tar.gz (not implemented) -p generate PS file -d generate PDF file -f force a complete rebuild -a print available hypertext anchors (not implemented) -l do hypertext links validation (not implemented) -e slides mode (for presentations) (not implemented) -m executed from Makefile (forces rebuild, no PS/PDF file, no tgz archive!) =head1 DESCRIPTION This package builds a docset from sources in different formats. The generated documents can be all nicely interlinked and to have the same look and feel. Currently it knows to handle input formats: * POD * HTML and knows to generate: * HTML * PS * PDF =head2 Modification control Each output mode maintains its own cache (per docset) which is used when certain source documents weren't modified since last build and the build is running in a non-force-rebuild mode. =head2 Definitions: * Chapter is a single document (file). * Link is an URL * Docset is a collection of docsets, chapters and links. =head2 Application Specific Features =over =item 1 META: not ported yet! Generate a split version HTML, creating html file for each pod section, and having everything interlinked of course. This version is used best for the search. =item 1 Complete the POD on the fly from the files in POD format. This is used to ease the generating of the presentations slides, so one can use C<*> instead of a long =over/=item/.../=item/=back strings. The rest is done as before. Take a look at the special version of the html2ps format to generate nice slides in I<conf/html2ps-slides.conf>. =item 1 META: not ported yet! If you turn the slides mode on, it automatically turns the C<-i> (C<*> preprocessing) mode and does a page break before each =head tag. =back =head2 Look-n-Feel Customization You can customise the look and feel of the ouput by adjusting the templates in the directory I<example/tmpl/custom>. You can change look and feel of the PS (PDF) versions by modifying I<example/conf/html2ps.conf>. Be careful that if your documentation that you want to put in one PS or PDF file is very big and you tell html2ps to put the TOC at the beginning you will need lots of memory because it won't write a single byte to the disk before it gets all the HTML markup converted to PS. =head1 CONFIGURATION All you have to prepare is a single config file that you then pass as an argument to C<pod2hpp>: pod2hpp [options] /abs/project/root/path /full/path/to/config/file Every directory in the source tree may have a configuration file, which designates a docset's root. See the I<config> files for examples. Usually the file in the root (I<example/src>) sets operational directories and other arguments, which you don't have to repeat in sub-docsets. Modify these files to suit your documentation project layout. Note that I<example/bin/build> script automatically locates your project's directory, so you can move your project around filesystem without changing anything. I<example/README> explains the layout of the directories. C<DocSet::Config> manpage explains the layout of the configuration file. =head1 PREREQUISITES All these are not required if all you want is to generate only the html version. =over 4 =item * ps2pdf Needed to generate the PDF version =item * Storable Perl module available from CPAN (http://cpan.org/) Allows source modification control, so if you modify only one file you will not have to rebuild everything to get the updated HTML/PS/PDF files. =back =head1 SUPPORT Notice that this tool relies on two tools (ps2pdf and html2ps) which I don't support. So if you have any problem first make sure that it's not a problem of these tools. Note that while C<html2ps> is included in this distribution, it's written in the old style Perl, so if you have patches send them along, but I won't try to fix/modify this code otherwise. I didn't write this utility. =head1 BUGS Huh? Probably many... =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =head1 SEE ALSO perl(1), Pod::HTML(3), html2ps(1), ps2pod(1), Storable(3) =head1 COPYRIGHT This program is distributed under the Artistic License, like the Perl itself. =cut 1.1 modperl-docs/lib/DocSet/5005compat.pm Index: 5005compat.pm =================================================================== package DocSet::5005compat; use strict; use Symbol (); use File::Basename; use File::Path; use Symbol (); my %compat_files = ( 'lib/warnings.pm' => \&warnings_pm, ); sub import { if ($] >= 5.006) { #make sure old compat stubs dont wipe out installed versions unlink for keys %compat_files; return; } eval { require File::Spec::Functions; } or die "this is only Perl $], you need to install File-Spec from CPAN"; my $min_version = 0.82; unless ($File::Spec::VERSION >= $min_version) { die "you need to install File-Spec-$min_version or higher from CPAN"; } while (my($file, $sub) = each %compat_files) { $sub->($file); } } sub open_file { my $file = shift; unless (-d 'lib') { $file = "Apache-Test/$file"; } my $dir = dirname $file; unless (-d $dir) { mkpath([$dir], 0, 0755); } my $fh = Symbol::gensym(); print "creating $file\n"; open $fh, ">$file" or die "open $file: $!"; return $fh; } sub warnings_pm { return if eval { require warnings }; my $fh = open_file(shift); print $fh <<'EOF'; package warnings; sub import {} 1; EOF close $fh; } 1; 1.1 modperl-docs/lib/DocSet/Cache.pm Index: Cache.pm =================================================================== package DocSet::Cache; use strict; use warnings; use DocSet::RunTime; use DocSet::Util; use Storable; use Carp; my %attrs = map {$_ => 1} qw(toc meta order); sub new { my($class, $path) = @_; die "no cache path specified" unless defined $path; my $self = bless { path => $path, dirty => 0, }, ref($class)||$class; $self->read(); return $self; } sub path { my($self) = @_; $self->{path}; } sub read { my($self) = @_; if (-w $self->{path} && DocSet::RunTime::has_storable_module()) { note "+++ Reading cache from $self->{path}"; $self->{cache} = Storable::retrieve($self->{path}); } else { note "+++ Initializing a new cache for $self->{path}"; $self->{cache} = {}; } } sub write { my($self) = @_; if (DocSet::RunTime::has_storable_module()) { note "+++ Storing the docset's cache to $self->{path}"; Storable::store($self->{cache}, $self->{path}); $self->{dirty} = 0; # mark as synced (clean) } } # set a cache entry (overrides a prev entry if any exists) sub set { my($self, $id, $attr, $data, $hidden) = @_; croak "must specify a unique id" unless defined $id; croak "must specify an attribute" unless defined $attr; croak "unknown attribute $attr" unless exists $attrs{$attr}; # remember the addition order (unless it's an update) unless (exists $self->{cache}{$id}) { push @{ $self->{cache}{_ordered_ids} }, $id; $self->{cache}{$id}{seq} = $#{ $self->{cache}{_ordered_ids} }; } $self->{cache}{$id}{$attr} = $data; $self->{cache}{$id}{_hidden} = $hidden; $self->{dirty} = 1; } # get a cache entry sub get { my($self, $id, $attr) = @_; croak "must specify a unique id" unless defined $id; croak "must specify an attribute" unless defined $attr; croak "unknown attribute $attr" unless exists $attrs{$attr}; if (exists $self->{cache}{$id} && exists $self->{cache}{$id}{$attr}) { return $self->{cache}{$id}{$attr}; } } # check whether a cached entry exists sub is_cached { my($self, $id, $attr) = @_; croak "must specify a unique id" unless defined $id; croak "must specify an attribute" unless defined $attr; croak "unknown attribute $attr" unless exists $attrs{$attr}; exists $self->{cache}{$id}{$attr}; } # invalidate cache (i.e. when a complete rebuild is forced) sub invalidate { my($self) = @_; $self->{cache} = {}; } # delete an entry in the cache sub unset { my($self, $id, $attr) = @_; croak "must specify a unique id" unless defined $id; croak "must specify an attribute" unless defined $attr; croak "unknown attribute $attr" unless exists $attrs{$attr}; if (exists $self->{cache}{$id}{$attr}) { delete $self->{cache}{$id}{$attr}; $self->{dirty} = 1; } } sub is_hidden { my($self, $id) = @_; #print "$id is hidden\n" if $self->{cache}{$id}{_hidden}; return $self->{cache}{$id}{_hidden}; } # return the sequence number of $id in the list of linked objects (0..N) sub id2seq { my($self, $id) = @_; croak "must specify a unique id" unless defined $id; if (exists $self->{cache}{$id}) { return $self->{cache}{$id}{seq}; } else { # this shouldn't happen! die "Cannot find $id in $self->{path} cache", dumper $self; } } # return the $id at the place $seq in the list of linked objects (0..N) sub seq2id { my($self, $seq) = @_; croak "must specify a seq number" unless defined $seq; if ($self->{cache}{_ordered_ids}) { return $self->{cache}{_ordered_ids}->[$seq]; } else { die "Cannot find $seq in $self->{path} cache", dumper $self; } } sub ordered_ids { my($self) = @_; return @{ $self->{cache}{_ordered_ids}||[] }; } sub total_ids { my($self) = @_; return scalar @{ $self->{cache}{_ordered_ids}||[] }; } # remember the meta data of the index node sub index_node { my($self) = shift; if (@_) { # set my($id, $title, $abstract) = @_; croak "must specify the index_node's id" unless defined $id; croak "must specify the index_node's title" unless defined $title; $self->{cache}{_index}{id} = $id; $self->{cache}{_index}{title} = $title; $self->{cache}{_index}{abstract} = $abstract; } else { # get return exists $self->{cache}{_index} ? $self->{cache}{_index} : undef; } } # set/get the path to the parent cache sub parent_node { my($self) = shift; if (@_) { # set my($cache_path, $id, $rel_path) = @_; croak "must specify a path to the parent cache" unless defined $cache_path; croak "must specify a relative to parent path" unless defined $rel_path; croak "must specify a parent id" unless defined $id; $self->{cache}{_parent}{cache_path} = $cache_path; $self->{cache}{_parent}{id} = $id; $self->{cache}{_parent}{rel_path} = $rel_path; } else { # get return exists $self->{cache}{_parent} ? ($self->{cache}{_parent}{cache_path}, $self->{cache}{_parent}{id}, $self->{cache}{_parent}{rel_path}) : (undef, undef, undef); } } # set/get the path to the node_groups cache sub node_groups { my($self) = shift; if (@_) { # set $self->{cache}{_node_groups} = shift; } else { # get return $self->{cache}{_node_groups}; } } sub is_dirty { shift->{dirty};} sub DESTROY { my($self) = @_; # flush the cache if destroyed before having a chance to sync to the disk $self->write if $self->is_dirty; } 1; __END__ =head1 NAME C<DocSet::Cache> - Maintain a Non-Volatile Cache of DocSet's Data =head1 SYNOPSIS use DocSet::Cache (); my $cache = DocSet::Cache->new($cache_path); $cache->read; $cache->write; $cache->set($id, $attr, $data); my $data = $cache->get($id, $attr); print "$id is cached" if $cache->is_cached($id); $cache->invalidate(); $cache->unset($id, $attr) my $seq = $cache->id2seq($id); my $id = $cache->seq2id($seq); my @ids = $cache->ordered_ids; my $total_ids = $cache->total_ids; $cache->index_node($id, $title, $abstract); my %index_node = $cache->index_node(); $cache->parent_node($cache_path, $id, $rel_path); my($cache_path, $id, $rel_path) = $cache->parent_node(); =head1 DESCRIPTION C<DocSet::Cache> maintains a non-volatile cache of docset's data. The cache is initialized either from the freezed file at the provided path. When the file is empty or doesn't exists, a new cache is initialized. When the cache is modified it should be saved, but if for some reason it doesn't get saved, the C<DESTROY> method will check whether the cache wasn't synced to the disk yet and will perform the sync itself. Each docset's node can create an entry in the cache, and store its data in it. The creator has to ensure that it supplies a unique id for each node that is added. Cache's internal representation is a hash, with internal data keys starting with _ (underscore), therefore the only restriction on node's id value is that it shouldn't not start with underscore. =head2 METHODS META: to be written (see SYNOPSIS meanwhile) =over =item * =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/Config.pm Index: Config.pm =================================================================== package DocSet::Config; use strict; use warnings; use Carp; use File::Find; use File::Basename (); use File::Spec::Functions; use DocSet::Util; use constant TRACE => 1; # uri extension to MIME type mapping my %ext2mime = ( map({$_ => 'text/html' } qw(htm html)), map({$_ => 'text/plain'} qw(txt text)), map({$_ => 'text/pod' } qw(pod pm)), ); my %conv_class= ( 'text/pod' => { 'text/html' => 'DocSet::Doc::POD2HTML', 'text/ps' => 'DocSet::Doc::POD2PS', }, 'text/html' => { 'text/html' => 'DocSet::Doc::HTML2HTML', 'text/ps' => 'DocSet::Doc::HTML2PS', }, 'text/plain' => { 'text/html' => 'DocSet::Doc::Text2HTML', 'text/pdf' => 'DocSet::Doc::Text2PDF', }, ); sub ext2mime { my($self, $ext) = @_; exists $ext2mime{$ext} ? $ext2mime{$ext} : undef; } sub conv_class { my($self, $src_mime, $dst_mime) = @_; # convert die "src_mime is not defined" unless defined $src_mime; die "dst_mime is not defined" unless defined $dst_mime; my $conv_class = $conv_class{$src_mime}{$dst_mime} or die "unknown input/output MIME mapping: $src_mime => $dst_mime"; return $conv_class; } my %attr = map {$_ => 1} qw(chapters docsets links); sub read_config { my($self, $config_file) = @_; die "Configuration file is not specified" unless $config_file; my $package = path2package($config_file); $self->{package} = $package; my $content; read_file($config_file, \$content); eval join '', "package $package;", $content, ";1;"; die "failed to eval config file at $config_file:\n$@" if $@; # parse the attributes of the docset's config file no strict 'refs'; use vars qw(@c); *c = [EMAIL PROTECTED]"$package\::c"}; my @groups = (); my $current_group = ''; my $group_size; for ( my $i=0; $i < @c; $i +=2 ) { my($key, $val) = @c[$i, $i+1]; if ($key eq 'group') { # close the previous group by storing the key of its last node if ($current_group) { push @{ $self->{node_groups} }, $current_group, $group_size; } # start the new group $current_group = $val; $group_size = 0; } elsif ($key eq 'hidden') { die "hidden's value must be an ARRAY reference" unless ref $val eq 'ARRAY'; my @h = @$val; for ( my $j=0; $j < @h; $j +=2 ) { my($key1, $val1) = @h[$j, $j+1]; die "hidden's can include only 'chapters' and 'docsets', " . "$key1 is invalid" unless $key1 =~ /^(docsets|chapters)$/; $self->add_node($key1, $val1, 1); } } elsif (exists $attr{$key}) { $group_size += $self->add_node($key, $val, 0); } else { $self->{$key} = $val; #print "$key = $val\n"; } } if ($current_group) { push @{ $self->{node_groups} }, $current_group, $group_size; } # merge_config will adjust this value, for nested docsets # so this value is relevant only for the real top parent node $self->{dir}{abs_doc_root} = '.'; $self->{dir}{src_root} = File::Basename::dirname $config_file; # dumper $self; } # # 1. put chapters together, docsets together, links together # 2. store the normal nodes in the order they were listed in 'ordered_nodes' # 2. store the hidden nodes in the order they were listed in 'hidden_nodes' # # return the number of added items sub add_node { my($self, $key, $value, $hidden) = @_; my @values = ref $value eq 'ARRAY' ? @$value : $value; if ($hidden) { push @{ $self->{hidden_nodes} }, $key, $_ for @values; } else { push @{ $self->{ordered_nodes} }, $key, $_ for @values; } return scalar @values; } # child config inherits from the parent config # and adjusts its paths sub merge_config { my($self, $src_rel_dir) = @_; my $parent_o = $self->{parent_o}; my $files = $self->{file} || {}; while ( my($k, $v) = each %{ $parent_o->{file}||{} }) { $self->{file}{$k} = $v unless $files->{$k}; } my $dirs = $self->{dir} || {}; while ( my($k, $v) = each %{ $parent_o->{dir}||{} }) { $self->{dir}{$k} = $v unless $dirs->{$k}; } # a chapter object won't set this one if ($src_rel_dir) { $self->{dir}{src_rel_dir} = $src_rel_dir; # append the relative to parent_o's src dir segments # META: hardcoded paths! for my $k ( qw(dst_html dst_ps dst_split_html) ) { $self->{dir}{$k} .= "/$src_rel_dir"; } # set path to the abs_doc_root # META: hardcoded paths! (but in this case it doesn't matter, # as long as it's set in the config file $self->{dir}{abs_doc_root} = join '/', ("..") x ($self->{dir}{dst_html} =~ tr|/|/|); } } # return a list of files to be copied # # due to a potentially huge list of files to be copied (e.g. the # splash library) currently it's assumed that this function is called # only once. Therefore no caching is done to save memory. # # The following conventions are used for $self->{copy_glob} # 1. Explicitly specified files and directories are copied as is # (directories aren't descended into) # 2. Shell metachars (*?[]) can be used. e.g. if you want to grab # directory foo and its contents, make sure to specify foo/*. sub files_to_copy { my($self) = @_; my $copy_skip_patterns = $self->{copy_skip} || []; # build one sub that will match many regex at once. my $rsub_filter_out = build_matchmany_sub($copy_skip_patterns); my $src_root = $self->get_dir('src_root'); # expand $self->{copy_glob}, applying the filter to skip unwanted # files my @files = grep !$rsub_filter_out->($_), # skip unwanted # grep s|^(?:\./)?||, # strip the leading ./ grep !-d $_, # skip empty dirs map { -d $_ ? @{ expand_dir($_) } : $_ } # expand dirs map { $_ =~ /[\*\?\[\]]/ ? glob($_) : $_ } # expand globs map { "$src_root/$_" } # prefix with src_root @{ $self->{copy_glob}||[] }; return [EMAIL PROTECTED]; } sub expand_dir { my @files = (); if ($] >= 5.006) { find(sub {push @files, $File::Find::name}, $_[0]); } else { # perl 5.005.03 on FreeBSD doesn't set the dir it chdir'ed to # need to move this to compat level? require Cwd; my $cwd; find(sub {$cwd = Cwd::cwd(); push @files, catfile $cwd, $_}, $_[0]); } return [EMAIL PROTECTED]; } sub set { my($self, %args) = @_; @{$self}{keys %args} = values %args; } sub set_dir { my($self, %args) = @_; @{ $self->{dir} }{keys %args} = values %args; } sub get { my $self = shift; return () unless @_; my @values = map {exists $self->{$_} ? $self->{$_} : ''} @_; return wantarray ? @values : $values[0]; } sub get_file { my $self = shift; return () unless @_; my @values = map {exists $self->{file}{$_} ? $self->{file}{$_} : ''} @_; return wantarray ? @values : $values[0]; } sub get_dir { my $self = shift; return () unless @_; my @values = map {exists $self->{dir}{$_} ? $self->{dir}{$_} : ''} @_; return wantarray ? @values : $values[0]; } sub nodes_by_type { my $self = shift; return $self->{ordered_nodes} || []; } sub hidden_nodes_by_type { my $self = shift; return $self->{hidden_nodes} || []; } sub node_groups { my $self = shift; return $self->{node_groups} || []; } sub docsets { my $self = shift; return exists $self->{docsets} ? @{ $self->{docsets} } : (); } sub links { my $self = shift; return exists $self->{links} ? @{ $self->{links} } : (); } sub src_chapters { my $self = shift; return exists $self->{chapters} ? @{ $self->{chapters} } : (); } # chapter paths as they go into production # $self->trg_chapters(@paths) : push a chapter(s) # $self->trg_chapters : retrieve the list sub trg_chapters { my $self = shift; if (@_) { push @{ $self->{chapters_prod} }, @_; } else { return exists $self->{chapters_prod} ? @{ $self->{chapters_prod} } : (); } } # set/get cache sub cache { my $self = shift; if (@_) { $self->{cache} = shift; } $self->{cache}; } sub path2package { my $path = shift; $path =~ s|[\W\.]|_|g; return "MyDocSet::X$path"; } sub object_store { my($self, $object) = @_; croak "no object passed" unless defined $object and ref $object; push @{ $self->{_objects_store} }, $object; } sub stored_objects { my($self) = @_; return @{ $self->{_objects_store}||[] }; } #sub chapter_data { # my $self = shift; # my $id = shift; # if (@_) { # $self->{chapter_data}{$id} = shift; # } # else { # $self->{chapter_data}{$id}; # } #} 1; __END__ =head1 NAME C<DocSet::Config> - A superclass that handles object's configuration and data =head1 SYNOPSIS use DocSet::Config (); my $mime = $self->ext2mime($ext); my $class = $self->conv_class($src_mime, $dst_mime); $self->read_config($config_file); $self->merge_config($src_rel_dir); my @files = $self->files_to_copy(files_to_copy); my @files = $self->expand_dir(); $self->set($key => $val); $self->set_dir($dir_name => $val); $val = $self->get($key); $self->get_file($key); $self->get_dir($dir_name); my @docsets = $self->docsets(); my @links = $self->links(); my @chapters = $self->src_chapters(); my @chapters = $self->trg_chapters(); $self->cache($cache); my $cache = $self->cache(); $package = $self->path2package($path); $self->object_store($object); my @objects = $self->stored_objects(); =head1 DESCRIPTION This objects lays in the base of the DocSet class and provides configuration and internal data storage/retrieval methods. At the end of this document the generic configuration file is explained. =head2 METHODS META: to be completed (see SYNOPSIS meanwhile) =over =item * ext2mime =item * conv_class =item * read_config =item * merge_config =item * files_to_copy =item * expand_dir =item * set =item * set_dir =item * get =item * get_file =item * get_dir =item * docsets =item * links =item * src_chapters =item * trg_chapters =item * cache =item * path2package =item * object_store =item * stored_objects =back =back =head1 CONFIGURATION FILE Each DocSet has its own configuration file. =head2 Structure Currently the configuration file is a simple perl script that is expected to declare an array C<@c> with all the docset properties in it. Later on more configuration formats will be supported. We use the C<@c> array because some of the configuration attributes may be repeated, so the hash datatype is not suitable here. Otherwise this array looks exactly like a hash: key1 => val1, key2 => val2, ... keyN => valN Of course you can declare any other perl variables and do whatevery you want, but after the config file is run, it should have C<@c> set. Don't forget to end the file with C<1;>. =head2 Declare once attributes The following attributes must be declared at least in the top-level I<config.cfg> file: =over =item * dir dir => { # the resulting html files directory dst_html => "dst_html", # the resulting ps and pdf files directory (and special # set of html files used for creating the ps and pdf # versions.) dst_ps => "dst_ps", # the resulting split version html files directory dst_split_html => "dst_split_html", # location of the templates relative to the root dir # (searched left to right) tmpl => [qw(tmpl/custom tmpl/std tmpl)], }, =item * file file => { # the html2ps configuration file html2ps_conf => "conf/html2ps.conf", }, =back Generally you should specify these only in the top-level config file, and only specify these again in sub-level config files, if you want to override things for the sub-docset and its successors. =head2 DocSet must attributes The following attributes must be declared in every docset configuration: =over =item * id a unique id of the docset. The uniquness should be preserved across any parallel docsets. =item * title the title of the docset =item * abstract a short abstract =back =head2 DocSet Components Any DocSet components can be repeated as many times as wanted. This allows to mix various types of nodes and still have oredered the way you want. You can have a chapter followed by a docset and followed by a few more chapters and ended with a link. The value of each component can be either a single item or a reference to an array of items. =over =item * docsets the docset can recursively include other docsets, simply list the directories the other docsets can be found in (where the I<config.cfg> file can be found) =item * chapters Each chapter can be specified as a path to its source document. =item * links The docset supports hyperlinks. Each link must be declared as a hash reference with keys: I<id>, I<link>, I<title> and I<abstract>. If you want to link to an external resource start the link, with URI (e.g. C<http://>). But this attribute also works for local links, for example, if the same generated page should be linked from more than one place, or if there is some non parsed object that needs to be linked to after it gets copied via I<copy_glob> attribute in the same or another docset. =back This is an example: docsets => ['docs', 'cool_docset'], chapters => [ qw( about/about.html ) ], docsets => [ qw( download ) ], chapters => 'foo/bar/zed.pod', links => [ { id => 'asf', link => 'http://apache.org/foundation/projects.html', title => 'The ASF Projects', abstract => "There many other ASF Projects", }, ], Since normally books consist of parts which group chapters by a common theme, we support this feature as well. So the index can now be generated as: part I: Installation * Starting * Installing part II: Troubleshooting * Debugging * Errors * Help Links * Offline Help This happens only if this feature is used, otherwise a plain flat toc is used: to enable this feature simply splice nodes with declaration of a new group using the I<group> attribute: group => 'Installation', chapters => [qw(start.pod install.pod)], group => 'Troubleshooting', chapters => [qw(debug.pod errors.pod)], links => [{put link data here}], chapters => ['offline_help.pod'], =head2 Hidden Objects I<docsets> and I<chapters> can be marked as hidden. This means that they will be normally processed but won't be linked from anywhere. Since the hidden objects cannot belong to any group and it doesn't matter when they are listed in the config file, you simply put one or more I<docsets> and I<chapters> into a special attribute I<hidden> which of course can be repeated many times just like most of the attributes. For example: ... chapters => [qw(start.pod install.pod)], hidden => { chapters => ['offline_help.pod'], docsets => ['hidden_docset'], }, ... The cool thing is that the hidden I<docsets> and I<chapters> will see all the unhidden objects, so those who know the "secret" URL will be able to navigate back to the non-hidden objects transparently. This feature could be useful for example to create pages normally not accessed by users. For example if you want to create a page used for the Apache's I<ErrorDocument> handler, you want to mark it hidden, because it shouldn't be linked from anywhere, but once the user hit it (because a non-existing URL has been entered) the user will get a perfect page with all the proper navigation widgets (I<menu>, etc) in it. =head2 Copy unmodified Usually the generated UI includes images, CSS files and of course some files must be copied without any modifications, like files including pure code, archives, etc. There are two attributes to handle this: =over =item * copy_glob Accepts a reference to an array of files and directories to copy. Note that you must use shell wildcharacters if you want deep directory copies, which also works for things like: C<*.html>. If you simply specify a directory name it'll be copied without any contents (this is a feature!). For example: # non-pod/html files or dirs to be copied unmodified copy_glob => [ qw( style.css images/* ) ], will copy I<style.css> and all the files under the I<images/> directory. =item * copy_skip While I<copy_glob> allows specifying complete dirs with potentially many nested sub-dirs to be copied, this becomes inconvenient if we want to copy all but a few files in these directories. The I<copy_skip> rule comes to help. It accepts a reference to an array of regular expressions that will be applied to each candidate to be copied as suggested by the I<copy_glob> attribute. If the regular expression matches the file won't be copied. One of the useful examples would be: copy_skip => [ '(?:^|\/)CVS(?:\/|$)', # skip cvs control files '#|~', # skip emacs backup files ], META: does copy_skip apply to all sub-docsets, if sub-docsets specify their own copy_glob? =back =head2 Extra Features If you want in the index file include a special top and bottom sections in addition to the linked list of the docset contents, you can do: body => { top => 'index_top.html', bot => 'index_bot.html', }, any of I<top> and I<bot> sub-attributes are optional. If these source docs are for example in HTML, they have to be written in a proper HTML, so the parser will be able to extract the body. =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/Doc.pm Index: Doc.pm =================================================================== package DocSet::Doc; use strict; use warnings; use DocSet::Util; use URI; sub new { my $class = shift; my $self = bless {}, ref($class)||$class; $self->init(@_); return $self; } sub init { my($self, %args) = @_; while (my($k, $v) = each %args) { $self->{$k} = $v; } } sub scan { my($self) = @_; note "Scanning $self->{src_uri}"; $self->src_read(); $self->retrieve_meta_data(); } sub render { my($self, $cache) = @_; # if the object wasn't stored rescan #$self->scan() unless $self->meta; my $src_uri = $self->{src_uri}; my $dst_path = $self->{dst_path}; my $rel_doc_root = $self->{rel_doc_root}; my $abs_doc_root = $self->{abs_doc_root}; $abs_doc_root .= "/$rel_doc_root" if defined $rel_doc_root; $self->{dir} = { abs_doc_root => $abs_doc_root, rel_doc_root => $rel_doc_root, }; $self->{nav} = DocSet::NavigateCache->new($cache->path, $src_uri); note "Rendering $dst_path"; $self->convert(); write_file($dst_path, $self->{output}); } # read the source and remember the mod time # sets $self->{content} # $self->{timestamp} sub src_read { my($self) = @_; # META: at this moment everything is a file path my $src_uri = "file://" . $self->{src_path}; my $u = URI->new($src_uri); my $scheme = $u->scheme; if ($scheme eq 'file') { my $path = $u->path; my $content = ''; read_file($path, \$content); $self->{content} = \$content; # file change timestamp my($mon, $day, $year) = (localtime ( (stat($path))[9] ) )[4,3,5]; $self->{timestamp} = sprintf "%02d/%02d/%04d", ++$mon,$day,1900+$year; } else { die "$scheme is not implemented yet"; } if (my $sub = $self->can('src_filter')) { $self->$sub(); } } sub meta { my $self = shift; if (@_) { $self->{meta} = shift; } else { $self->{meta}; } } sub toc { my $self = shift; if (@_) { $self->{toc} = shift; } else { $self->{toc}; } } # abstract methods #sub src_filter {} 1; __END__ =head1 NAME C<DocSet::Doc> - A Base Document Class =head1 SYNOPSIS use DocSet::Doc::HTML (); my $doc = DocSet::Doc::HTML->new(%args); $doc->scan(); my $meta = $doc->meta(); my $toc = $doc->toc(); $doc->render(); # internal methods $doc->src_read(); $doc->src_filter(); =head1 DESCRIPTION This super class implement core methods for scanning a single document of a given format and rendering it into another format. It provides sub-classes with hooks that can change the default behavior. Note that this class cannot be used as it is, you have to subclass it and implement the required methods listed later. =head1 METHODS =over =item * new =item * init =item * scan scan the document into a parsed tree and retrieve its meta and toc data if possible. =item * render render the output document and write it to its final destination. =item * src_read Fetches the source of the document. The source can be read from different media, i.e. a file://, http://, relational DB or OCR :) A subclass may implement a "source" filter. For example if the source document is written in an extended POD the source filter may convert it into a standard POD. If the source includes some template directives these can be pre-processed as well. The document's content is coming out of this class ready for parsing and converting into other formats. =item * meta a simple set/get-able accessor to the I<meta> attribute. =item * toc a simple set/get-able accessor to the I<toc> attribute =back =head1 ABSTRACT METHODS These methods must be implemented by the sub-classes: =over =item retrieve_meta_data Retrieve and set the meta data that describes the input document into the I<meta> object attribute. Various documents may provide different meta information. The only required meta field is I<title>. =back These methods can be implemented by the sub-classes: =over =item src_filter A subclass may want to preprocess the source document before it'll be processed. This method is called after the source has been read. By default nothing happens. =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/DocSet.pm Index: DocSet.pm =================================================================== package DocSet::DocSet; use strict; use warnings; use DocSet::Util; use DocSet::RunTime; use DocSet::Cache (); use DocSet::Doc (); use DocSet::NavigateCache (); use vars qw(@ISA); use DocSet::Config (); @ISA = qw(DocSet::Config); ######## sub new { my $class = shift; my $self = bless {}, ref($class)||$class; $self->init(@_); return $self; } sub init { my($self, $config_file, $parent_o, $src_rel_dir) = @_; $self->read_config($config_file); # are we inside a super docset? if ($parent_o and ref($parent_o)) { $self->{parent_o} = $parent_o; $self->merge_config($src_rel_dir); } } sub scan { my($self) = @_; my $src_root = $self->get_dir('src_root'); # each output mode need its own cache, because of the destination # links which are different my $mode = $self->get('tmpl_mode'); my $cache = DocSet::Cache->new("$src_root/cache.$mode.dat"); $self->cache($cache); # store away # cleanup the cache or rebuild $cache->invalidate if get_opts('rebuild_all'); # cache the index node meta data $cache->index_node($self->get('id'), $self->get('title'), $self->get('abstract') ); # cache the location of the parent node cache if (my $parent_o = $self->get('parent_o')) { my $parent_src_root = $parent_o->get_dir('src_root'); (my $rel2parent_src_root = $src_root) =~ s|$parent_src_root||; my $rel_dir = join '/', ("..") x ($rel2parent_src_root =~ tr|/|/|); my $parent_cache_path = "$parent_src_root/cache.$mode.dat"; $cache->parent_node($parent_cache_path, $self->get('id'), $rel_dir); $self->set_dir(rel_parent_root => $rel_dir); } ### # scan the nodes of the current level and cache the meta and other # data my $hidden = 0; my @nodes_by_type = @{ $self->nodes_by_type }; while (@nodes_by_type) { my($type, $data) = splice @nodes_by_type, 0, 2; if ($type eq 'docsets') { my $docset = $self->docset_scan_n_cache($data, $hidden); $self->object_store($docset) if defined $docset and ref $docset; } elsif ($type eq 'chapters') { my $chapter = $self->chapter_scan_n_cache($data, $hidden); $self->object_store($chapter) if defined $chapter and ref $chapter; } elsif ($type eq 'links') { $self->link_scan_n_cache($data, $hidden); # we don't need to process links } else { # nothing } } # the same but for the hidden objects $hidden = 1; my @hidden_nodes_by_type = @{ $self->hidden_nodes_by_type }; while (@hidden_nodes_by_type) { my($type, $data) = splice @hidden_nodes_by_type, 0, 2; if ($type eq 'docsets') { my $docset = $self->docset_scan_n_cache($data, $hidden); $self->object_store($docset) if defined $docset and ref $docset; } elsif ($type eq 'chapters') { my $chapter = $self->chapter_scan_n_cache($data, $hidden); $self->object_store($chapter) if defined $chapter and ref $chapter; } else { # nothing } } $cache->node_groups($self->node_groups); # sync the cache $cache->write; } sub docset_scan_n_cache { my($self, $src_rel_dir, $hidden) = @_; my $src_root = $self->get_dir('src_root'); my $cfg_file = "$src_root/$src_rel_dir/config.cfg"; my $docset = $self->new($cfg_file, $self, $src_rel_dir); $docset->scan; # cache the children meta data my $id = $docset->get('id'); my $meta = { title => $docset->get('title'), link => "$src_rel_dir/index.html", abstract => $docset->get('abstract'), }; $self->cache->set($id, 'meta', $meta, $hidden); return $docset; } sub link_scan_n_cache { my($self, $link, $hidden) = @_; my %meta = %$link; # make a copy my $id = delete $meta{id}; $self->cache->set($id, 'meta', \%meta, $hidden); } sub chapter_scan_n_cache { my($self, $src_file, $hidden) = @_; my $trg_ext = $self->trg_ext(); my $src_root = $self->get_dir('src_root'); my $dst_root = $self->get_dir('dst_root'); my $abs_doc_root = $self->get_dir('abs_doc_root'); my $src_path = "$src_root/$src_file", my $src_ext = filename_ext($src_file) or die "cannot get an extension for $src_file"; my $src_mime = $self->ext2mime($src_ext) or die "unknown extension: $src_ext"; (my $basename = $src_file) =~ s/\.$src_ext$//; # destination paths my $rel_dst_path = "$basename.$trg_ext"; my $rel_doc_root = "./"; $rel_dst_path =~ s|^\./||; # strip the leading './' $rel_doc_root .= join '/', ("..") x ($rel_dst_path =~ tr|/|/|); $rel_doc_root =~ s|/$||; # remove the last '/' my $dst_path = "$dst_root/$rel_dst_path"; # push to the list of final chapter paths # e.g. used by PS/PDF build, which needs all the chapters $self->trg_chapters($rel_dst_path); ### to rebuild or not to rebuild my($should_update, $reason) = should_update($src_path, $dst_path); if (!$should_update) { note "--- $src_file: skipping ($reason)"; return undef; } ### init note "+++ $src_file: processing ($reason)"; my $dst_mime = $self->get('dst_mime'); my $conv_class = $self->conv_class($src_mime, $dst_mime); require_package($conv_class); my $chapter = $conv_class->new( tmpl_mode => $self->get('tmpl_mode'), tmpl_root => $self->get_dir('tmpl'), src_uri => $src_file, src_path => $src_path, dst_path => $dst_path, rel_dst_path => $rel_dst_path, rel_doc_root => $rel_doc_root, abs_doc_root => $abs_doc_root, ); $chapter->scan(); # cache the chapter's meta and toc data $self->cache->set($src_file, 'meta', $chapter->meta, $hidden); $self->cache->set($src_file, 'toc', $chapter->toc, $hidden); return $chapter; } sub render { my($self) = @_; # copy non-pod files like images and stylesheets $self->copy_the_rest; my $src_root = $self->get_dir('src_root'); # each output mode need its own cache, because of the destination # links which are different my $mode = $self->get('tmpl_mode'); my $cache = DocSet::Cache->new("$src_root/cache.$mode.dat"); # render the objects no matter what kind are they for my $obj ($self->stored_objects) { $obj->render($cache); } $self->complete; } #################### sub copy_the_rest { my($self) = @_; my @copy_files = @{ $self->files_to_copy || [] }; return unless @copy_files; my $src_root = $self->get_dir('src_root'); my $dst_root = $self->get_dir('dst_root'); note "+++ Copying the non-processed files from $src_root to $dst_root"; foreach my $src_path (@copy_files){ my $dst_path = $src_path; # # some OSs's File::Find returns files with no dir prefix root # # (that's what ()* is for # $dst_path =~ s/(?:$src_root)*/$dst_root/; $dst_path =~ s/$src_root/$dst_root/; # to rebuild or not to rebuild my($should_update, $reason) = should_update($src_path, $dst_path); if (!$should_update) { note "--- skipping cp $src_path $dst_path ($reason)"; next; } note "+++ processing cp $src_path $dst_path ($reason)"; copy_file($src_path, $dst_path); } } # abstract classes sub complete {} 1; __END__ =head1 NAME C<DocSet::DocSet> - An abstract docset generation class =head1 SYNOPSIS use DocSet::DocSet::HTML (); my $docset = DocSet::DocSet::HTML->new($config_file); # must start from the abs root chdir $abs_root; # must be a relative path to be able to move the generated code from # location to location, without adjusting the links $docset->set_dir(abs_root => "."); $docset->scan; $docset->render; =head1 DESCRIPTION C<DocSet::DocSet> processes a docset, which can include other docsets, documents and links. In the first pass it scans the linked to it documents and other docsets and caches this information and the objects for a later peruse. In the second pass the stored objects are rendered. And the docset is completed. This class cannot be used on its own and has to be subclassed and extended, by the sub-classes which has a specific to input and output formats of the documents that need to be processed. It handles only the partial functionality which doesn't require format specific knowledge. =head2 METHODS This class inherits from C<DocSet::Config> and you will find the documentation of methods inherited from this class in its pod. The following "public" methods are implemented in this super-class: =over =item * new $class->new($config_file, $parent_o, $src_rel_dir); =item * init $self->init($config_file, $parent_o, $src_rel_dir); =item * scan $self->scan(); Scans the docset for meta data and tocs of its items and caches this information and the item objects. =item * render $self->render(); Calls the render() method of each of the stored objects and creates an index page linking all the items. =item * copy_the_rest $self->copy_the_rest() Copies the items which aren't processed (i.e. images, css files, etc). =back =head2 ABSTRACT METHODS The following methods should be implemented by the sub-classes. =over =item * parse =item * retrieve_meta_data =item * convert =item * complete $self->complete(); put here anything that should be run after all the items have been rendered and all the meta info has been collected. i.e. generation of the I<index> file, to link to all the links and the parent node if such exists. =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/NavigateCache.pm Index: NavigateCache.pm =================================================================== package DocSet::NavigateCache; use strict; use warnings; use DocSet::RunTime; use DocSet::Util; use Storable; use Carp; # cache the loaded cache files use vars qw(%CACHE); %CACHE = (); #use vars qw(@ISA); use DocSet::Cache (); [EMAIL PROTECTED] = qw(DocSet::Cache); use constant OBJ => 0; use constant ID => 1; use constant CUR_PATH => 2; use constant REL_PATH => 3; # $rel_path (to the parent) is optional (e.g. root doesn't have a parent) sub new { my($class, $cache_path, $id, $rel_path) = @_; croak "no cache path specified" unless defined $cache_path; croak "no id specified" unless defined $id; my $cache = get_cache($cache_path); my $self = bless [], ref($class)||$class; $self->[OBJ] = $cache; $self->[CUR_PATH] = $cache_path; $self->[REL_PATH] = $rel_path if $rel_path; $self->[ID] = $id; return $self; } sub parent_rel_path { my($self) = @_; return defined $self->[REL_PATH] ? $self->[REL_PATH] : undef; } # get next item's object or undef if there are no more sub next { my($self) = @_; my $cache = $self->[OBJ]; my $seq = $cache->id2seq($self->[ID]); my $last_seq = $cache->total_ids - 1; # if the next object is hidden, it's like there is no next object, # because the hidden objects, if any, are always coming last if ($seq < $last_seq) { my $id = $cache->seq2id($seq + 1); if ($cache->is_hidden($id)) { return undef; } else { return $self->new($self->[CUR_PATH], $id); } } else { return undef; } } # get prev item's object or undef if there are no more sub prev { my($self) = @_; my $cache = $self->[OBJ]; my $seq = $cache->id2seq($self->[ID]); # since the hidden objects, if any, are always coming last # we need to go to the last of the non-hidden objects. if ($seq) { my $id = $cache->seq2id($seq - 1); if ($cache->is_hidden($id)) { return $self->new($self->[CUR_PATH], $id)->prev(); } else { return $self->new($self->[CUR_PATH], $id); } } else { return undef; } } # get the object of the first item on the same level sub first { my($self) = @_; my $cache = $self->[OBJ]; # it's possible that the whole docset is made of hidden objects. # since the hidden objects, if any, are always coming last # we simply return undef in such a case if ($cache->total_ids) { my $id = $cache->seq2id(0); if ($cache->is_hidden($id)) { return undef; } else { return $self->new($self->[CUR_PATH], $id); } } else { return undef; } } # the index node of the current level sub index_node { my($self) = @_; return $self->[OBJ]->index_node; } # get the object of the parent sub up { my($self) = @_; my($path, $id, $rel_path) = $self->[OBJ]->parent_node; $rel_path = "." unless defined $rel_path; if (defined $self->[REL_PATH] && length $self->[REL_PATH]) { # append the relative path of each child, so the overall # relative path is correct $rel_path .= "/$self->[REL_PATH]"; } # it's ok to have a hidden parent, we don't mind to see it # as non-hidden, since the children of the hidden parent aren't # linked from other non-hidden pages. In fact we must ignore the # fact that it's hidden (if it is) because otherwise the navigation # won't work. if ($path) { return $self->new($path, $id, $rel_path); } else { return undef; } } # retrieve the meta data of the current node sub meta { my($self) = @_; return $self->[OBJ]->get($self->[ID], 'meta'); } # retrieve the node groups sub node_groups { my($self) = @_; #print "OK: "; #dumper $self->[OBJ]->node_groups; return $self->[OBJ]->node_groups; } sub id { shift->[ID]; } sub get_cache { my($cache_path) = @_; $CACHE{$cache_path} ||= DocSet::Cache->new($cache_path); return $CACHE{$cache_path}; } 1; __END__ =head1 NAME C<DocSet::NavigateCache> - Navigate the DocSet's caches in a readonly mode =head1 SYNOPSIS my $nav = DocSet::NavigateCache->new($cache_path, $id, $rel_path); # go through all nodes from left to right, and remember the sequence # number of the $nav node (from which we have started) my $iterator = $nav->first; my $seq = 0; my $counter = 0; my @meta = (); while ($iterator) { $seq = $counter if $iterator->id eq $nav->id; push @meta, $iterator->meta; $iterator = $iterator->next; $counter++; } # add index node's meta data push @meta, $nav->index_node; # prev object $prev = $nav->prev; # get all the ancestry my @parents = (); $p = $nav->up; while ($p) { push @parents, $p; $p = $p->up; } =head1 DESCRIPTION C<DocSet::NavigateCache> navigates the cache created by docset objects during their scan stage. Once the navigator handle is obtained, it's possible to move between the nodes of the same level, using the next() and prev() methods or going up one level using the up() method. the first() method returns the object of the first node on the same level. Each of these methods returns a new C<DocSet::NavigateCache> object or undef if the object cannot be created. This object can be used to retrieve node's meta data, its id and its index node's meta data. Currently it is used in the templates for the internal navigation widgets creation. That's where you will find the examples of its use (e.g. I<tmpl/custom/html/menu_top_level> and I<tmpl/custom/html/navbar_global>). As C<DocSet::NavigateCache> reads cache files in, it caches them, since usually the same file is required many times in a few subsequent calls. Note that C<DocSet::NavigateCache> doesn't see any hidden objects stored in the cache. =head2 METHODS META: to be completed (see SYNOPSIS meanwhile) =over =item * new DocSet::NavigateCache->new($cache_path, $id, $rel_path); C<$cache_path> is the path of the cache file to read. C<$id> is the id of the current node. C<$rel_path> is optional and passed if an object has a parent node. It contains a relative path from the current node to its parent. =item * parent_rel_path =item * next =item * prev =item * first =item * up =item * index_node =item * meta =item * id =item * =item * =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/RunTime.pm Index: RunTime.pm =================================================================== package DocSet::RunTime; use strict; use warnings; use vars qw(@ISA @EXPORT %opts); @ISA = qw(Exporter); @EXPORT = qw(get_opts); sub set_opt { my(%args) = (); if (@_ == 1) { my $arg = shift; my $ref = ref $arg; if ($ref) { %args = $ref eq 'HASH' ? %$arg : @$arg; } else { die "must be a ref to or an array/hash"; } } else { %args = @_; } @opts{keys %args} = values %args; } sub get_opts { my $opt = shift; exists $opts{$opt} ? $opts{$opt} : ''; } # check whether we have a Storable avalable use constant HAS_STORABLE => eval { require Storable; }; sub has_storable_module { return HAS_STORABLE; } my $html2ps_exec = `which html2ps` || ''; chomp $html2ps_exec; sub can_create_ps { # ps2html is bundled, so we can always create PS return $html2ps_exec; # if you unbundle it make sure you write here a code similar to # can_create_pdf() } my $ps2pdf_exec = `which ps2pdf` || ''; chomp $ps2pdf_exec; sub can_create_pdf { # check whether ps2pdf exists return $ps2pdf_exec if $ps2pdf_exec; print(qq{It seems that you do not have ps2pdf installed! You have to install it if you want to generate the PDF file }); return 0; } 1; __END__ =head1 NAME C<DocSet::RunTime> - RunTime Configuration =head1 SYNOPSIS use DocSet::RunTime; if (get_opts('verbose') { print "verbose mode"; } DocSet::RunTime::set_opt(\%args); DocSet::RunTime::has_storable_module(); DocSet::RunTime::can_create_ps(); DocSet::RunTime::can_create_pdf(); =head1 DESCRIPTION This module is a part of the docset application, and it stores the run time arguments. i.e. whether to build PS and PDF or to run in a verbose mode and more. =head1 FUNCTIONS META: To be completed, see SYNOPSIS =over =item * set_opt =item * get_opts =item * has_storable_module =item * can_create_ps =item * can_create_pdf =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/Util.pm Index: Util.pm =================================================================== package DocSet::Util; use strict; use warnings; use Symbol (); use File::Basename (); use File::Copy (); use File::Path (); use Data::Dumper; use Carp; use Template; use DocSet::RunTime; use vars qw(@ISA @EXPORT); @ISA = qw(Exporter); @EXPORT = qw(read_file read_file_paras copy_file write_file create_dir filename_ext require_package dumper sub_trace note get_date get_timestamp proc_tmpl build_matchmany_sub banner should_update confess cluck); # copy_file($src_path, $dst_path); # copy a file at $src_path to $dst_path, # if one of the directories of the $dst_path doesn't exist -- it'll # be created. ############### sub copy_file { my($src, $dst) = @_; # make sure that the directory exist or create one my $base_dir = File::Basename::dirname $dst; create_dir($base_dir) unless (-d $base_dir); File::Copy::copy($src, $dst); } # write_file($filename, $ref_to_array||scalar); # content will be written to the file from the passed array of # paragraphs ############### sub write_file { my($filename, $content) = @_; # make sure that the directory exist or create one my $dir = File::Basename::dirname $filename; create_dir($dir) unless -d $dir; my $fh = Symbol::gensym; open $fh, ">$filename" or croak "Can't open $filename for writing: $!"; print $fh ref $content ? @$content : defined $content ? $content : ''; close $fh; } # recursively creates a multi-layer directory ############### sub create_dir { my $path = shift; return if !defined($path) || -e $path; # META: mode could be made configurable File::Path::mkpath($path, 0, 0755) or croak "Couldn't create $path: $!"; } # read_file($filename, $ref); # assign to a ref to a scalar ############### sub read_file { my($filename, $r_content) = @_; my $fh = Symbol::gensym; open $fh, $filename or croak "Can't open $filename for reading: $!"; local $/; $$r_content = <$fh>; close $fh; } # read_file_paras($filename, $ref_to_array); # read by paragraph # content will be set into a ref to an array ############### sub read_file_paras { my($filename, $ra_content) = @_; my $fh = Symbol::gensym; open $fh, $filename or croak "Can't open $filename for reading: $!"; local $/ = ""; @$ra_content = <$fh>; close $fh; } # return the passed file's extension or '' if there is no one # note: that '/foo/bar.conf.in' returns an extension: 'conf.in'; # note: a hidden file .foo will be recognized as an extension 'foo' sub filename_ext { my($filename) = @_; my $ext = (File::Basename::fileparse($filename, '\.[^\.]*'))[2] || ''; $ext =~ s/^\.(.*)/lc $1/e; $ext; } sub get_date { sprintf "%s %d, %d", (split /\s+/, scalar localtime)[1,2,4]; } sub get_timestamp { my ($mon,$day,$year) = (localtime ( time ) )[4,3,5]; sprintf "%02d/%02d/%04d", ++$mon, $day, 1900+$year; } # convert Foo::Bar into Foo/Bar.pm and require sub require_package { my $package = shift; die "no package passed" unless $package; $package =~ s|::|/|g; $package .= '.pm'; require $package; } # convert the template into the release version # $tmpl_root: a ref to an array of tmpl base dirs # tmpl_file: which template file to process # mode : in what mode (html, ps, ...) # vars : ref to a hash with vars to path to the template # # returns the processed template ################### sub proc_tmpl { my($tmpl_root, $tmpl_file, $mode, $vars) = @_; # append the specific rendering mode, so the correct template will # be picked (e.g. in 'ps' mode, the ps sub-dir(s) will be searched # first) my $search_path = join ':', map { ("$_/$mode", "$_/common", "$_") } (ref $tmpl_root ? @$tmpl_root : $tmpl_root); my $template = Template->new ({ INCLUDE_PATH => $search_path, RECURSION => 1, PLUGINS => { cnavigator => 'DocSet::Template::Plugin::NavigateCache', }, }) || die $Template::ERROR, "\n"; # use Data::Dumper; # print Dumper [EMAIL PROTECTED]; my $output; $template->process($tmpl_file, $vars, \$output) || die "error: ", $template->error(), "\n"; return $output; } # compare the timestamps/existance of src and dst paths # and return (true,reason) if src is newer than dst # otherwise return (false, reason) # # if rebuild_all runtime is on, this always returns (true, reason) # sub should_update { my($src_path, $dst_path) = @_; # to rebuild or not to rebuild my $not_modified = (-e $dst_path and -M $dst_path < -M $src_path) ? 1 : 0; my $reason = $not_modified ? 'not modified' : 'modified'; if (get_opts('rebuild_all')) { return (1, "$reason / forced"); } else { return (!$not_modified, $reason); } } sub banner { my($string) = @_; my $len = length($string) + 8; note( "#" x $len, "### $string ###", "#" x $len, ); } # see DocSet::Config::files_to_copy() for usage ######################### sub build_matchmany_sub { my $ra_regex = shift; my $expr = join '||', map { "\$_[0] =~ m/$_/o" } @$ra_regex; # note $expr; my $matchsub = eval "sub { ($expr) ? 1 : 0}"; die "Failed in building regex [EMAIL PROTECTED]: $@" if $@; $matchsub; } sub dumper { print Dumper @_; } #sub sub_trace { ## my($package) = (caller(0))[0]; # my($sub) = (caller(1))[3]; # print "=> $sub: @_\n"; #} *confess = \*Carp::confess; *cluck = \*Carp::cluck; sub note { return unless get_opts('verbose'); print join("\n", @_), "\n"; } 1; __END__ =head1 NAME C<DocSet::Util> - Commonly used functions =head1 SYNOPSIS use DocSet::Util; copy_file($src, $dst); write_file($filename, $content); create_dir($path); read_file($filename, $r_content); read_file_paras($filename, $ra_content); my $ext = filename_ext($filename); my $date = get_date(); my $timestamp = get_timestamp(); require_package($package); my $output = proc_tmpl($tmpl_root, $tmpl_file, $mode, $vars); my $should_update = should_update($src_path, $dst_path); banner($string); my $sub_ref = build_matchmany_sub($ra_regex); dumper($ref); confess($string); note($string); =head1 DESCRIPTION All the functions are exported by default. =head2 METHODS META: to be completed (see SYNOPSIS meanwhile) =over =item * copy_file =item * write_file =item * create_dir =item * read_file =item * read_file_paras =item * filename_ext =item * get_date =item * get_timestamp =item * require_package =item * proc_tmpl =item * should_update =item * banner =item * build_matchmany_sub =item * dumper =item * confess =item * note =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/Doc/HTML2HTML.pm Index: HTML2HTML.pm =================================================================== package DocSet::Doc::HTML2HTML; use strict; use warnings; use DocSet::Util; use vars qw(@ISA); require DocSet::Source::HTML; @ISA = qw(DocSet::Source::HTML); sub convert { my($self) = @_; my @body = $self->{parsed_tree}->{body}; my $vars = { meta => $self->{meta}, body => [EMAIL PROTECTED], dir => $self->{dir}, nav => $self->{nav}, last_modified => $self->{timestamp}, }; my $tmpl_file = 'page'; my $mode = $self->{tmpl_mode}; my $tmpl_root = $self->{tmpl_root}; $self->{output} = proc_tmpl($tmpl_root, $tmpl_file, $mode, {doc => $vars} ); } # need for pluggin docs into index files sub converted_body { my($self) = @_; return $self->{parsed_tree}->{body}; } 1; __END__ =head1 NAME C<DocSet::Doc::HTML2HTML> - HTML source to HTML target converter =head1 SYNOPSIS =head1 DESCRIPTION Implements an C<DocSet::Doc> sub-class which converts a source document in HTML, into an output document in HTML. =head1 METHODS For the rest of the super class methods see C<DocSet::Doc>. =over =item * convert =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/Doc/POD2HTML.pm Index: POD2HTML.pm =================================================================== package DocSet::Doc::POD2HTML; use strict; use warnings; use DocSet::Util; require Pod::POM; #require Pod::POM::View::HTML; #my $view_mode = 'Pod::POM::View::HTML'; my $view_mode = 'DocSet::Doc::POD2HTML::View::HTML'; use vars qw(@ISA); require DocSet::Source::POD; @ISA = qw(DocSet::Source::POD); sub convert { my($self) = @_; my $pom = $self->{parsed_tree}; # my @sections = $pom->head1(); # shift @sections; # skip the title my @sections = $pom->content(); shift @sections; # skip the title my @body = (); foreach my $node (@sections) { # my $type = $node->type(); # print "$type\n"; push @body, $node->present($view_mode); } # for my $head1 (@sections) { # push @body, $head1->title->present($view_mode); # push @body, $head1->content->present($view_mode); # for my $head2 ($head1->head2) { # push @body, $head2->present($view_mode); # for my $head3 ($head2->head3) { # push @body, $head3->present($view_mode); # for my $head4 ($head3->head4) { # push @body, $head4->present($view_mode); # } # } # } # } my $vars = { meta => $self->{meta}, toc => $self->{toc}, body => [EMAIL PROTECTED], dir => $self->{dir}, nav => $self->{nav}, last_modified => $self->{timestamp}, }; my $tmpl_file = 'page'; my $mode = $self->{tmpl_mode}; my $tmpl_root = $self->{tmpl_root}; $self->{output} = proc_tmpl($tmpl_root, $tmpl_file, $mode, {doc => $vars} ); } 1; package DocSet::Doc::POD2HTML::View::HTML; use vars qw(@ISA); require Pod::POM::View::HTML; @ISA = qw( Pod::POM::View::HTML); sub view_head1 { my ($self, $node) = @_; return $self->anchor($node->title) . $self->SUPER::view_head1($node); } sub view_head2 { my ($self, $node) = @_; return $self->anchor($node->title) . $self->SUPER::view_head2($node); } sub view_head3 { my ($self, $node) = @_; return $self->anchor($node->title) . $self->SUPER::view_head3($node); } sub view_head4 { my ($self, $node) = @_; return $self->anchor($node->title) . $self->SUPER::view_head4($node); } sub anchor { my($self, $title) = @_; my $anchor = "$title"; $anchor =~ s/\W/_/g; return qq{<a name="$anchor"></a>\n}; } 1; __END__ =head1 NAME C<DocSet::Doc::POD2HTML> - POD source to HTML target converter =head1 SYNOPSIS =head1 DESCRIPTION Implements an C<DocSet::Doc> sub-class which converts a source document in POD, into an output document in HTML. =head1 METHODS For the rest of the super class methods see C<DocSet::Doc>. =over =item * convert =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/Doc/Text2HTML.pm Index: Text2HTML.pm =================================================================== package DocSet::Doc::Text2HTML; use strict; use warnings; use vars qw(@ISA); require DocSet::Source::Text; @ISA = qw(DocSet::Source::Text); ########################### ### not implemented yet ### ########################### 1; __END__ =head1 NAME C<DocSet::Doc::Text2HTML> - Text source to HTML target converter =head1 SYNOPSIS =head1 DESCRIPTION Implements an C<DocSet::Doc> sub-class which converts a source document in Text, into an output document in HTML. =cut 1.1 modperl-docs/lib/DocSet/DocSet/HTML.pm Index: HTML.pm =================================================================== package DocSet::DocSet::HTML; use strict; use warnings; use DocSet::Util; use DocSet::NavigateCache (); use vars qw(@ISA); use DocSet::DocSet (); @ISA = qw(DocSet::DocSet); # what's the output format sub trg_ext { return 'html'; } sub init { my $self = shift; $self->SUPER::init(@_); # configure HTML specific run-time $self->set(dst_mime => 'text/html'); $self->set(tmpl_mode => 'html'); $self->set_dir(dst_root => $self->get_dir('dst_html')); banner("HTML DocSet: " . $self->get('title') ); } sub complete { my($self) = @_; $self->write_index_file(); } # generate the index.html based on the doc entities it includes, in # the following order: docsets, books, chapters # # Using the same template file create the long and the short index # html files ################################## sub write_index_file { my($self) = @_; my @toc = (); my $cache = $self->cache; # TOC my @node_groups = @{ $self->node_groups }; my @ids = $cache->ordered_ids; # create the toc while skipping over hidden files if (@node_groups && @ids) { # index's toc is built from groups of items' meta data while (@node_groups) { my($title, $count) = splice @node_groups, 0, 2; push @toc, { group_title => $title, subs => [map {$cache->get($_, 'meta')} grep !$cache->is_hidden($_), splice @ids, 0, $count], }; } } else { # index's toc is built from items' meta data for my $id (grep !$cache->is_hidden($_), $cache->ordered_ids) { push @toc, $cache->get($id, 'meta'); } } my $dir = { abs_doc_root => $self->get_dir('abs_doc_root'), rel_doc_root => $self->get_dir('rel_parent_root'), }; my $meta = { title => $self->get('title'), abstract => $self->get('abstract'), }; my $navigator = DocSet::NavigateCache->new($self->cache->path, $self->get('id')); my %args = ( nav => $navigator, toc => [EMAIL PROTECTED], meta => $meta, dir => $dir, version => $self->get('version')||'', date => get_date(), last_modified => get_timestamp(), # body => top ); # pluster index top and bottom docs if defined (after converting them) if (my $body = $self->get('body')) { my $src_root = $self->get_dir('src_root'); my $dst_mime = $self->get('dst_mime'); for my $sec (qw(top bot)) { my $src_file = $body->{$sec}; next unless $src_file; my $src_ext = filename_ext($src_file) or die "cannot get an extension for $src_file"; my $src_mime = $self->ext2mime($src_ext) or die "unknown extension: $src_ext"; my $conv_class = $self->conv_class($src_mime, $dst_mime); require_package($conv_class); my $chapter = $conv_class->new( tmpl_mode => $self->get('tmpl_mode'), tmpl_root => $self->get_dir('tmpl'), src_uri => $src_file, src_path => "$src_root/$src_file", ); $chapter->scan(); $args{body}{$sec} = $chapter->converted_body(); } } my $dst_root = $self->get_dir('dst_html'); my $dst_file = "$dst_root/index.html"; my $mode = $self->get('tmpl_mode'); my $tmpl_file = 'index'; my $vars = { doc => \%args }; my $tmpl_root = $self->get_dir('tmpl'); my $content = proc_tmpl($tmpl_root, $tmpl_file, $mode, $vars); note "+++ Creating $dst_file"; DocSet::Util::write_file($dst_file, $content); } 1; __END__ =head1 NAME C<DocSet::DocSet::HTML> - A subclass of C<DocSet::DocSet> for generating HTML docset =head1 SYNOPSIS See C<DocSet::DocSet> =head1 DESCRIPTION This subclass of C<DocSet::DocSet> converts the source docset into a set of HTML documents linking its items with autogenerated I<index.html>. =head2 METHODS See the majority of the methods in C<DocSet::DocSet> =over =item * trg_ext $self->trg_ext(); returns the extension of the target files. I<html> in the case of this sub-class. =item * init $self->init(@_); calls C<DocSet::DocSet::init> and then initializes its own HTML output specific settings. =item * complete see C<DocSet::DocSet> =item * write_index_file $self->write_index_file(); creates I<index.html> file linking all the items of the docset together. =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/DocSet/PSPDF.pm Index: PSPDF.pm =================================================================== package DocSet::DocSet::PSPDF; use strict; use warnings; use DocSet::Util; use DocSet::RunTime; use DocSet::NavigateCache (); use vars qw(@ISA); use DocSet::DocSet (); @ISA = qw(DocSet::DocSet); # what's the output format sub trg_ext { return 'html'; } sub init { my $self = shift; $self->SUPER::init(@_); # configure PS/PDF specific run-time # though, we build ps/pdf the intermediate product is HTML $self->set(dst_mime => 'text/html'); $self->set(tmpl_mode => 'ps'); $self->set_dir(dst_root => $self->get_dir('dst_ps')); banner("PS/PDF DocSet: " . $self->get('title') ); } sub complete { my($self) = @_; $self->write_index_file(); $self->create_ps_book; $self->create_pdf_book if get_opts('generate_pdf'); } # generate the index.html file based on the doc entities it includes, # in the following order: docsets, books, chapters # # Using the same template file create the long and the short index # html files ################################## sub write_index_file{ my($self) = @_; my $dir = { abs_doc_root => $self->get_dir('abs_doc_root'), rel_doc_root => '..', # META: probably wrong! (see write_index_html_file()) }; my $meta = { title => $self->get('title'), abstract => $self->get('abstract'), }; use DocSet::NavigateCache; my $navigator = DocSet::NavigateCache->new($self->cache->path, $self->get('id')); my %args = ( nav => $navigator, meta => $meta, dir => $dir, version => $self->get('version')||'', date => get_date(), last_modified => get_timestamp(), ); my $dst_root = $self->get_dir('dst_root'); my $dst_file = "$dst_root/index.html"; my $mode = $self->get('tmpl_mode'); my $tmpl_file = 'index'; my $vars = { doc => \%args }; my $tmpl_root = $self->get_dir('tmpl'); my $content = proc_tmpl($tmpl_root, $tmpl_file, $mode, $vars); note "+++ Creating $dst_file"; DocSet::Util::write_file($dst_file, $content); } # generate the PS book #################### sub create_ps_book{ my($self) = @_; note "+++ Generating a PostScript Book"; my $html2ps_exec = DocSet::RunTime::can_create_ps(); my $html2ps_conf = $self->get_file('html2ps_conf'); my $id = $self->get('id'); my $dst_root = $self->get_dir('dst_root'); my $command = "$html2ps_exec -f $html2ps_conf -o $dst_root/${id}.ps "; $command .= join " ", map {"$dst_root/$_"} "index.html", $self->trg_chapters; note "% $command"; system $command; } # generate the PDF book #################### sub create_pdf_book{ my($self) = @_; note "+++ Converting PS => PDF"; my $dst_root = $self->get_dir('dst_root'); my $id = $self->get('id'); my $command = "ps2pdf $dst_root/$id.ps $dst_root/$id.pdf"; note "% $command"; system $command; } 1; __END__ =head1 NAME C<DocSet::DocSet::PSPDF> - A subclass of C<DocSet::DocSet> for generating PS/PDF docset =head1 SYNOPSIS See C<DocSet::DocSet> =head1 DESCRIPTION This subclass of C<DocSet::DocSet> converts the source docset into PS and PDF "books". It uses C<html2ps> to generate the PS file, therefore it uses HTML as its intermediate product, though it uses different templates than C<DocSet::DocSet::HTML> since PS/PDF doesn't require the navigation widgets. =head2 METHODS See the majority of the methods in C<DocSet::DocSet> =over =item * trg_ext $self->trg_ext(); returns the extension of the target files. I<html> in the case of this sub-class. =item * init $self->init(@_); calls C<DocSet::DocSet::init> and then initializes its own HTML output specific settings. =item * complete see C<DocSet::DocSet> =item * write_index_file $self->write_index_file(); creates I<index.html> file linking all the items of the docset together. =item * create_ps_book Generats a PostScript Book =item * create_pdf_book Converts PS into PDF (if I<generate_pdf> runtime option is set) =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/Source/HTML.pm Index: HTML.pm =================================================================== package DocSet::Source::HTML; use strict; use warnings; use DocSet::Util; use vars qw(@ISA); require DocSet::Doc; @ISA = qw(DocSet::Doc); sub retrieve_meta_data { my($self) = @_; $self->parse; use Pod::POM::View::HTML; my $mode = 'Pod::POM::View::HTML'; #print Pod::POM::View::HTML->print($pom); $self->{meta} = { title => $self->{parsed_tree}->{title}, link => $self->{rel_dst_path}, }; # there is no autogenerated TOC for HTML files } sub parse { my($self) = @_; # already parsed return if exists $self->{parsed_tree} && $self->{parsed_tree}; # print ${ $self->{content} }; #my %segments = map {$_ => ''} qw(title body); # this one retrievs the body and the title of the given html require HTML::Parser; sub start_h {} sub end_h { my($self, $tagname, $skipped_text) = @_; # use $p itself as a tmp storage (ok according to the docs) $self->{parsed_tree}->{$tagname} = $skipped_text; } my $p = HTML::Parser->new(api_version => 3, report_tags => [qw(title body)], start_h => [\&start_h], end_h => [\&end_h, "self,tagname,skipped_text"], ); # Parse document text chunk by chunk $p->parse(${ $self->{content} }); $p->eof; # store the tree away $self->{parsed_tree} = $p->{parsed_tree}; } 1; __END__ =head1 NAME C<DocSet::Source::HTML> - A class for parsing input document in the HTML format =head1 SYNOPSIS See C<DocSet::Source> =head1 DESCRIPTION =head1 METHODS =over =item * parse Converts the source HTML document into a parsed tree. =item * retrieve_meta_data Retrieve and set the meta data that describes the input document into the I<meta> object attribute. The I<title> and I<link> meta attributes are getting set. =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/Source/POD.pm Index: POD.pm =================================================================== package DocSet::Source::POD; use strict; use warnings; use DocSet::Util; use DocSet::RunTime; use vars qw(@ISA); require DocSet::Doc; @ISA = qw(DocSet::Doc); sub retrieve_meta_data { my($self) = @_; $self->parse_pod; use Pod::POM::View::HTML; my $mode = 'Pod::POM::View::HTML'; #print Pod::POM::View::HTML->print($pom); my $meta = {}; my $pom = $self->{parsed_tree}; my @sections = $pom->head1(); # don't present on purpose ->present($mode); there should be no markup in NAME my $name_sec = shift @sections; if ($name_sec) { $meta->{title} = $name_sec->content(); } else { $meta->{title} = 'No Title'; } $meta->{title} =~ s/^\s*|\s*$//sg; $meta->{link} = $self->{rel_dst_path}; # put all the meta data under the same attribute $self->{meta} = $meta; # build the toc datastructure my @toc = (); my $level = 1; for my $node (@sections) { push @toc, $self->render_toc_level($node, $level); } $self->{toc} = [EMAIL PROTECTED]; } sub render_toc_level { my($self, $node, $level) = @_; my %toc_entry = (); my $title = $node->title; $toc_entry{link} = $toc_entry{title} = "$title"; # must stringify $toc_entry{link} =~ s/\W/_/g; # META: put into a sub? $toc_entry{link} = "#$toc_entry{link}"; # prepand '#' for internal links my @sub = (); $level++; if ($level < 5) { # if there are deeper than =head4 levels we don't go down (spec is 1-4) my $method = "head$level"; for my $sub_node ($node->$method()) { push @sub, $self->render_toc_level($sub_node, $level); } } $toc_entry{subs} = [EMAIL PROTECTED] if @sub; return \%toc_entry; } sub parse_pod { my($self) = @_; # already parsed return if exists $self->{parsed_tree} && $self->{parsed_tree}; $self->podify_items() if get_opts('podify_items'); # print ${ $self->{content} }; use Pod::POM; my %options; my $parser = Pod::POM->new(\%options); my $pom = $parser->parse_text(${ $self->{content} }) or die $parser->error(); $self->{parsed_tree} = $pom; # examine any warnings raised if (my @warnings = $parser->warnings()) { print "\n", '-' x 40, "\n"; print "File: $self->{src_path}\n"; warn "$_\n" for @warnings; } } sub src_filter { my ($self) = @_; $self->extract_pod; $self->podify_items if get_opts('podify_items'); } sub extract_pod { my($self) = @_; my @pod = (); my $in_pod = 0; for (split /\n\n/, ${ $self->{content} }) { $in_pod ||= /^=/s; next unless $in_pod; $in_pod = 0 if /^=cut/; push @pod, $_; } # handle empty files unless (@pod) { push @pod, "=head1 NAME", "=head1 Not documented", "=cut"; } my $content = join "\n\n", @pod; $self->{content} = \$content; } sub podify_items { my($self) = @_; # tmp storage my @paras = (); my $items = 0; my $second = 0; # we want the source in paragraphs my @content = split /\n\n/, ${ $self->{content} }; foreach (@content) { # is it an item? if (/^(\*|\d+)\s+((\*|\d+)\s+)?/) { $items++; if ($2) { $second++; s/^(\*|\d+)\s+//; # strip the first level shortcut s/^(\*|\d+)\s+/=item $1\n\n/; # do the second s/^/=over 4\n\n/ if $second == 1; # start 2nd level } else { # first time insert the =over pod tag s/^(\*|\d+)\s+/=item $1\n\n/; # start 1st level s/^/=over 4\n\n/ if $items == 1; s/^/=back\n\n/ if $second; # complete 2nd level $second = 0; # end 2nd level section } push @paras, split /\n\n/, $_; } else { # complete the =over =item =back tag $second=0, push @paras, "=back" if $second; # if 2nd level is not closed push @paras, "=back" if $items; push @paras, $_; # not a tag item $items = 0; } } my $content = join "\n\n", @paras; $self->{content} = \$content; } 1; __END__ =head1 NAME C<DocSet::Source::POD> - A class for parsing input document in the POD format =head1 SYNOPSIS =head1 DESCRIPTION =head2 METHODS =over =item retrieve_meta_data() =item parse_pod() =item podify_items() podify_items(); Podify text to represent items in pod, e.g: 1 Some text from item Item1 2 Some text from item Item2 becomes: =over 4 =item 1 Some text from item Item1 =item 2 Some text from item Item2 =back podify_items() accepts 'C<*>' and digits as bullets podify_items() receives a ref to array of paragraphs as a parameter and modifies it. Nothing returned. Moreover, you can use a second level of indentation. So you can have * title * * item * * item or * title * 1 item * 2 item where the second mark is which tells whether to use a ball bullet or a numbered item. =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut 1.1 modperl-docs/lib/DocSet/Source/Text.pm Index: Text.pm =================================================================== package DocSet::Source::Text; use strict; use warnings; use vars qw(@ISA); require DocSet::Doc; @ISA = qw(DocSet::Doc); ########################### ### not implemented yet ### ########################### 1; __END__ =head1 NAME C<DocSet::Source::Text> - A class for parsing input document in the text format =head1 SYNOPSIS =head1 DESCRIPTION =cut 1.1 modperl-docs/lib/DocSet/Template/Plugin/NavigateCache.pm Index: NavigateCache.pm =================================================================== package DocSet::Template::Plugin::NavigateCache; use strict; use warnings; use vars qw(@ISA); use Template::Plugin; @ISA = qw(Template::Plugin); use DocSet::NavigateCache (); sub new { my $class = shift; my $context = shift; DocSet::NavigateCache->new(@_); } 1; __END__
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]