063e66f3babafaa674bcd9a3d5ef472dfd2e4f20
[ikiwiki.git] / IkiWiki / Plugin / toc.pm
1 #!/usr/bin/perl
2 # Table Of Contents generator
3 package IkiWiki::Plugin::toc;
4
5 use warnings;
6 use strict;
7 use IkiWiki;
8 use HTML::Parser;
9
10 sub import { #{{{
11         IkiWiki::hook(type => "preprocess", id => "toc",
12                 call => \&preprocess);
13         IkiWiki::hook(type => "format", id => "toc",
14                 call => \&format);
15 } # }}}
16
17 my %tocpages;
18
19 sub preprocess (@) { #{{{
20         my %params=@_;
21
22         $params{levels}=1 unless exists $params{levels};
23
24         # It's too early to generate the toc here, so just record the
25         # info.
26         $tocpages{$params{destpage}}=\%params;
27
28         return "\n<div class=\"toc\"></div>\n";
29 } # }}}
30
31 sub format (@) { #{{{
32         my %params=@_;
33         my $content=$params{content};
34         
35         return $content unless exists $tocpages{$params{page}};
36         %params=%{$tocpages{$params{page}}};
37
38         my $p=HTML::Parser->new(api_version => 3);
39         my $page="";
40         my $index="";
41         my %anchors;
42         my $curlevel;
43         my $startlevel=0;
44         my $liststarted=0;
45         my $indent=sub { "\t" x $curlevel };
46         $p->handler(start => sub {
47                 my $tagname=shift;
48                 my $text=shift;
49                 if ($tagname =~ /^h(\d+)$/i) {
50                         my $level=$1;
51                         my $anchor="index".++$anchors{$level}."h$level";
52                         $page.="$text<a name=\"$anchor\" />";
53         
54                         # Take the first header level seen as the topmost level,
55                         # even if there are higher levels seen later on.
56                         if (! $startlevel) {
57                                 $startlevel=$level;
58                                 $curlevel=$startlevel-1;
59                         }
60                         elsif ($level < $startlevel) {
61                                 $level=$startlevel;
62                         }
63                         
64                         return if $level - $startlevel >= $params{levels};
65         
66                         if ($level > $curlevel) {
67                                 while ($level > $curlevel + 1) {
68                                         $index.=&$indent."<ol>\n";
69                                         $curlevel++;
70                                         $index.=&$indent."<li class=\"L$curlevel\">\n";
71                                 }
72                                 $index.=&$indent."<ol>\n";
73                                 $curlevel=$level;
74                                 $liststarted=1;
75                         }
76                         elsif ($level < $curlevel) {
77                                 while ($level < $curlevel) {
78                                         $index.=&$indent."</li>\n" if $curlevel;
79                                         $curlevel--;
80                                         $index.=&$indent."</ol>\n";
81                                 }
82                                 $liststarted=0;
83                         }
84         
85                         $p->handler(text => sub {
86                                 $page.=join("", @_);
87                                 $index.=&$indent."</li>\n" unless $liststarted;
88                                 $liststarted=0;
89                                 $index.=&$indent."<li class=\"L$curlevel\">".
90                                         "<a href=\"#$anchor\">".
91                                         join("", @_).
92                                         "</a>\n";
93                                 $p->handler(text => undef);
94                         }, "dtext");
95                 }
96                 else {
97                         $page.=$text;
98                 }
99         }, "tagname, text");
100         $p->handler(default => sub { $page.=join("", @_) }, "text");
101         $p->parse($content);
102         $p->eof;
103
104         while ($startlevel && $curlevel >= $startlevel) {
105                 $index.=&$indent."</li>\n" if $curlevel;
106                 $curlevel--;
107                 $index.=&$indent."</ol>\n";
108         }
109
110         $page=~s/(<div class=\"toc\">)/$1\n$index/;
111         return $page;
112 }
113
114 1