FunctionalPerl

 view release on metacpan or  search on metacpan

htmlgen/FunctionalPerl/Htmlgen/Toc.pm  view on Meta::CPAN

#
# Copyright (c) 2014-2019 Christian Jaeger, copying@christianjaeger.ch
#
# This is free software, offered under either the same terms as perl 5
# or the terms of the Artistic License version 2 or the terms of the
# MIT License (Expat version). See the file COPYING.md that came
# bundled with this file.
#

=head1 NAME

FunctionalPerl::Htmlgen::Toc - building a table of contents

=head1 SYNOPSIS

=head1 DESCRIPTION

Expands this syntax in a PXML document:

<with_toc> <h2>..</h2> <h3>..</h3>.. <h2>..</h2> </with_toc>

into a table of contents and then the original contents but with
numbering added to the hX section headers.

=head1 SEE ALSO

This is a L<FunctionalPerl::Htmlgen::PXMLMapper>

=head1 NOTE

This is alpha software! Read the status section in the package README
or on the L<website|http://functional-perl.org/>.

=cut

package FunctionalPerl::Htmlgen::Toc;

use strict;
use warnings;
use warnings FATAL => 'uninitialized';
use experimental "signatures";

use Sub::Call::Tail;

use FP::StrictList;
use FP::List;
use Chj::TEST;
use PXML::XHTML ":all";
use FP::Stream qw(stream_mixed_state_fold_right);
use PXML ":all";
use FP::Predicates qw(is_natural);

sub rindices_numberstring($rindices) {
    is_null($rindices)
        ? ""
        : $rindices->reverse->map(sub($i) {"$i."})->strings_join("") . " "
}

TEST { rindices_numberstring(list()) } "";
TEST { rindices_numberstring(list(2, 1)) } "1.2. ";

package FunctionalPerl::Htmlgen::Toc::TocNodeBase {
    use FP::StrictList;
    use FP::Predicates;
    use FP::List;
    use PXML::XHTML ":all";

    use FP::Struct [[\&is_strictlist, "subnodes"]];

    sub subnodes_length($self) {
        $self->subnodes->length
    }

    sub subnodes_add ($self, $node) {
        is_instance_of $node, "FunctionalPerl::Htmlgen::Toc::TocNode"
            or die "wrong type";

        # (^ XX so poor, neither do I have a TypedList yet, nor the
        # syntax to add it to the method declaration)
        $self->subnodes_update(sub($l) { cons $node, $l })
    }

    sub subnodes_head_update ($self, $fn) {
        my $ss = $self->subnodes;
        if (is_null $ss) {
            die "skipped level"    ## nicer message and all?
        } else {
            $self->subnodes_set(cons(&$fn($ss->first), $ss->rest))
        }
    }

    sub level_add ($self, $levelsdown, $node) {
        is_natural $levelsdown or die "wrong type: $levelsdown";
        if ($levelsdown > 1) {
            $self->subnodes_head_update(
                sub($subnode) {
                    $subnode->level_add($levelsdown - 1, $node)
                }
            )
        } else {
            $self->subnodes_add($node)
        }
    }

    # just for debugging? $indices is a list of 1-based indices
    sub ref ($self, $indices) {
        if (is_null $indices) {
            $self
        } else {
            my ($i, $indices1) = $indices->first_and_rest;
            my $subnodes = $self->subnodes;
            $subnodes->ref($subnodes->length - $i)->ref($indices1)
        }
    }

    # used during document mapping (while the toc is collected)
    sub numberstring($self) {
        my $ss  = $self->subnodes;
        my $len = $ss->length;

        # the first is the one that was added last
        $len ? $len . "." . $ss->first->numberstring : "";

        # (O(n^2) complexity)
    }

    # build the TOC html. Need to get the numberstring differently
    # now.
    sub html_with_parents ($self, $rindices) {
        my $maybe_name = $self->name;
        my $shown      = [
            FunctionalPerl::Htmlgen::Toc::rindices_numberstring($rindices),
            $self->header_pxml_for_toc
        ];
        DIR(
            { class => "toc" },
            (
                defined $maybe_name
                ? A({ href => "#" . $maybe_name }, $shown)
                : $shown
            ),
            $self->subnodes->array__reverse__map_with_length(
                sub ($node, $num) {
                    LI($node->html_with_parents(cons $num, $rindices))
                }
            )
        )
    }

    _END_
}

package FunctionalPerl::Htmlgen::Toc::TocNode {
    use FP::Predicates "is_string";
    use PXML "is_pxml_element";
    use FP::List;

    use FP::Struct [
        [\&is_string,       "name"],    # as used in the <a name="">
                                        # already in the document
        [\&is_pxml_element, "header"]
        ],
        "FunctionalPerl::Htmlgen::Toc::TocNodeBase";

    sub header_pxml_for_toc($self) {

        # XX keep some formatting?
        $self->header->text
    }

    _END_
}

package FunctionalPerl::Htmlgen::Toc::TocRootNode {
    use FP::List;
    use PXML::XHTML ":all";

    use FP::Struct ["header_pxml_for_toc"],
        "FunctionalPerl::Htmlgen::Toc::TocNodeBase";

    sub name($self) {undef}

    sub html($self) {
        DIV({ class => "toc_box" }, $self->html_with_parents(null))
    }

    _END__
}

our $empty_toc = FunctionalPerl::Htmlgen::Toc::TocRootNode->new(strictnull,
    H3({ class => "toc_title" }, "Contents"));

sub tocnode ($name, $header) {
    FunctionalPerl::Htmlgen::Toc::TocNode->new(strictnull, $name, $header);
}

our $ttoc;
TEST {
    my $t1  = $empty_toc->level_add(1, tocnode("a", H1("First")));
    my $t11 = $t1->level_add(2, tocnode("b", H2("Sub-First")));
    my $t2  = $t11->level_add(1, tocnode("c", H1("Second")));
    $ttoc = $t2;
    [map { $_->numberstring } $t11, $t2]
}
['1.1.', '2.'];

TEST {
    my $t_paths = list(list(1), list(2), list(1, 1));
    $t_paths->map (sub($path) { $ttoc->ref($path)->name })->array
}
[qw(a c b)];

TEST { $ttoc->html->string }
'<div class="toc_box">'
    . '<dir class="toc"><h3 class="toc_title">Contents</h3>'
    . '<li><dir class="toc"><a href="#a">1. First</a>'
    . '<li><dir class="toc"><a href="#b">1.1. Sub-First</a></dir></li>'
    . '</dir></li>'
    . '<li><dir class="toc"><a href="#c">2. Second</a></dir></li>'
    . '</dir></div>';

sub process__with_toc__body ($body, $first_level, $toc, $parents) {

    # -> (processed_body, toc)
    stream_mixed_state_fold_right(
        sub ($v, $toc, $rest) {
            if (is_pxml_element($v)) {
                if (my ($_level) = $v->name =~ /^[hH](\d+)$/s) {
                    my $level = $_level - $first_level + 1;
                    is_natural $level
                        or die "The given <with_toc> tag dictates that the "
                        . "top-most header level is '$first_level', but "
                        . "encountering <"
                        . $v->name . ">";

                    my $anchor = $parents->find(



( run in 0.875 second using v1.01-cache-2.11-cpan-995e09ba956 )