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); use constant HEAD_MAX_LEVEL => 4; use constant MAX_DESC_LENGTH => 500; # META: we are presenting too early, or this code should be moved to # POD2HTML specific module require Pod::POM::View::HTML; my $mode = 'Pod::POM::View::HTML'; sub retrieve_meta_data { my ($self) = @_; $self->parse_pod; #print Pod::POM::View::HTML->print($pom); my $meta = { title => 'No Title', abstract => '', }; my $pom = $self->{parsed_tree}; my @sections = $pom->head1(); if (@sections) { # extract the title from the NAME section and remove it from content if ($sections[0]->title =~ /NAME/) { # don't present on purpose ->present($mode); there should # be no markup in NAME a problem with # <CODE>....</CODE><TITLE> and alike $meta->{title} = (shift @sections)->content(); $meta->{title} =~ s/^\s*|\s*$//sg; } # stitle is the same in docs $meta->{stitle} = $meta->{title}; # locate the DESCRIPTION section (should be in the first three # sections) for (0..2) { next unless defined $sections[$_] && $sections[$_]->title =~ /DESCRIPTION/i; my $abstract = $sections[$_]->content->present($mode); # cannot do this now, as it might cut some markup in the middle: <i>1 2</i> # # we are interested only in the first paragraph, or if its # # too big first MAX_DESC_LENGTH chars. # my $index = index $abstract, " ", MAX_DESC_LENGTH; # # cut only if index didn't return '-1' which is when the the # # space wasn't found starting from location MAX_DESC_LENGTH # unless ($index == -1) { # $abstract = substr $abstract, 0, $index+1; # $abstract .= " ... <i>(continued)</i>"; # } # # # temp workaround, but can only split on paras $abstract =~ s|<p>(.*?)</p>.*|$1|s; $meta->{abstract} = $abstract; last; } } $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} = \@toc; } sub render_toc_level { my ($self, $node, $level) = @_; my $title = $node->title; my $link = "$title"; # must stringify to get the raw string $link =~ s/^\s*|\s*$//g; # strip leading and closing spaces $link =~ s/\W/_/g; # META: put into a sub? see Doc::Common::pod_pom_html_anchor # prepand '#' for internal links my $toc_link = "toc_$link"; # self referring toc entry $link = "#$link"; my %toc_entry = ( title => $title->present($mode), # run the formatting if any link => $link, toc_link => $toc_link, ); my @sub = (); $level++; if ($level <= HEAD_MAX_LEVEL) { # 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} = \@sub if @sub; return \%toc_entry; } sub parse_pod { my ($self) = @_; # already parsed return if exists $self->{parsed_tree} && $self->{parsed_tree}; # 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->head2page_breaks() if $self->{docset}->options('slides_mode'); $self->podify_items() if $self->{docset}->options('podify_items'); } sub extract_pod { my ($self) = @_; my @pod = (); my $in_pod = 0; for (split /\n{2,}/, ${ $self->{content} }) { unless ($in_pod) { s/^[\s\n]*//ms; # skip empty lines in preamble $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; } # add a page break for =headX in slides mode sub head2page_breaks { my ($self) = @_; # we want the source in paragraphs my @content = split /\n\n/, ${ $self->{content} }; my $count = 0; my @paras = (); foreach (@content) { # add a page break starting from the third head (since the # first is removed anyway, and we don't want to start a new # page on the very first page) if (/^=head/) { $count++; if ($count > 2) { push @paras, qq{=for html <?page-break>}; } } push @paras, $_; } 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 META: not sure if the customized implementation of L<> belongs here. But it works as follows: Assuming that the main I<config.cfg> specifies the following argument: dir => { ... # search path for pods, etc. must put more specific paths first! search_paths => [qw( docs/2.0/api/mod_perl-2.0 docs/2.0/api/ModPerl-Registry docs/2.0 docs/1.0 . )], # what extensions to search for search_exts => [qw(pod pm html)], }, Whenever the pod includes L<Title|foo::bar/section>, the code will first convert C<foo::bar> into I<foo/bar> and then will try to find the file I<foo/bar.pod> in the search path (similar to C<@INC>), as well as files I<foo/bar.pm> and I<foo/bar.html> under dir I<src>. If other C<search_exts> are specified they will be searched as well. If there is a much the link will be created, otherwise only the title of the link will be displayed. Notice that the C<search_paths> must specify more specific paths first. If you don't they won't be searched. Currently this is done only to optimize memory usage and some speed, not sure if that's very important. But this is different from how Perl does search with C<@INC> since DocSet reads all the files in memory once and then reuses this data. =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. =item head2page_breaks in the I<slides_mode> we want each =headX to start a new slide, so this mode inserts the page-breaks: =for html <?page-break> starting from the second header (well actually from the third in the raw POD, because the first one (NAME) gets stripped before it's seen by the rendering engine. =back =head1 AUTHORS Stas Bekman E<lt>stas (at) stason.orgE<gt> =cut