initial FAQ 2 wiki script
[exim.git] / doc / doc-scripts / f2wiki
1 #!/usr/bin/perl
2 #
3 # Script to convert Exim FAQ into wiki markup - moin flavour
4 #
5 # $Cambridge: exim/doc/doc-scripts/f2wiki,v 1.1 2004/11/12 14:48:08 nm4 Exp $
6 #
7 use strict;
8 use integer;
9 use bytes;
10 use Data::Dumper;
11 use IO::File;
12
13 sub mkwikiname (@) {
14     my @in = @_;
15
16     my @bits;
17     foreach my $str (@in) {
18         $str =~ tr/0-9A-Za-z _//cd;
19         push(@bits, $str);
20     }
21     return join('/', @bits);
22 }
23
24 \f
25
26 sub mkwikifilename ($) {
27     my $wn = shift;
28
29     $wn =~ s/([^A-Za-z0-9])/sprintf("_%02x",ord($1))/eg;
30
31     return $wn;
32 }
33
34 \f
35
36 sub format_wiki_question_header ($) {
37     my $lp = shift;
38
39     my @lines = @{$lp};
40     my $i;
41     for($i =0; ($i <= $#lines); $i++) {
42         unless (defined($lines[$i])) {
43             splice(@lines, $i);
44             last;
45         }
46         $lines[$i] =~ s/^\s+//;
47     }
48     return('= ' . join(' ', @lines) . ' =');
49 }
50
51 \f
52
53 sub wikiref ($$) {
54     my $meta = shift;
55     my $qref = shift;
56
57     my $qtag = $meta->qtags->{$qref};
58     unless ($qtag) {
59         warn "Unknown qtag $qref\n";
60         return $qref;
61     }
62
63     return join(':', '[', $qref->{wikiname}, $qref . ']');
64 }
65
66 \f
67
68 sub wiki_markup ($$) {
69     my $meta = shift;
70     my $s = shift;
71
72     $s =~ s/@\\/\@\@backslash\@\@/g;            # @\ temporarily hidden
73
74     $s =~ s/\\#/ /g;                             # \# is a hard space
75
76     $s =~ s/\\\*\*([^*]*)\*\*\\/'''$1'''/g;     # \**...**\   => bold
77     $s =~ s/\\\*([^*]*)\*\\/''$1''/g;           # \*.....*\   => italic
78     $s =~ s/\\"([^"]*)"\\/`$1`/g;               # \"....."\   => fixed pitch
79     $s =~ s/\\\$([^\$]*)\$\\/''\$$1''/g;        # \$.....$\   => $italic
80     $s =~ s/\\\\([^\\]*)\\\\/<small>$1<\/small>/g; # \\.....\\   => small
81     $s =~ s/\\\(([^)]*)\)\\/''$1''/g;           # \(.....)\   => italic
82     $s =~ s/\\-([^\\]*)-\\/'''-$1'''/g;         # \-.....-\   => -bold
83     $s =~ s/\\\[([^]]*)\]\\/''$1''/gx;          # \[.....]\   => <italic>
84     $s =~ s/\\\?(.*?)\?\\/$1/g;                 # \?.....?\   => URL
85     $s =~ s/\\\^\^([^^]*)\^\^\\/''$1''/g;       # \^^...^^\   => italic
86     $s =~ s/\\\^([^^]*)\^\\/''$1''/g;           # \^.....^\   => italic
87     $s =~ s/\\%([^%]*)%\\/'''$1'''/g;           # \%.....%\   => bold
88     $s =~ s/\\\/([^\/]*)\/\\/''$1''/g;          # \/...../\   => italic
89     $s =~ s/\\([^\\]+)\\/`$1`/g;                # \.......\   => fixed pitch
90
91     $s =~ s"//([^/\"]*)//"''$1</i>"g;           # //.....//   => italic
92     $s =~ s/::([^:]*)::/''$1:''/g;              # ::.....::   => italic:
93
94     $s =~ s/``(.*?)''/&#147;$1&#148;/g;                   # ``.....''   => quoted text
95
96     #$s =~ s/\s*\[\[br\]\]\s*/<br>/g;           # [[br]]      => <br>
97
98     $s =~ s/\@\@backslash\@\@/\\/g;                     # Put back single backslash
99
100     $s =~ s/^(\s*\(\d\)\s)/$1&nbsp;/;           # Extra space after (1), etc.
101
102     # Cross references within paragraphs
103
104     $s =~ s/Q(\d{4})(?!:)/wikiref($meta, $1)/xg;
105
106     # References to configuration samples
107
108     ##$s =~ s/\b([CFLS]\d\d\d)\b/<a href="$1.txt">$1<\/a>/g;
109
110     # Remove white space preceding a newline in the middle of paragraphs,
111     # to keep the file smaller (and for human reading when debugging).
112
113     ##$s =~ s/^\s+//mg;
114
115     return $s;
116 }
117
118 \f
119
120 sub clip_paragraph ($) {
121     my $lines = shift;
122
123     my $ret;
124     my $flags;
125     my $offlen;
126
127     # split off and throw initial para breaks
128     while (($#{$lines} >= 0) && (!defined($lines->[0]))) {
129         shift @{$lines};
130     }
131
132     # if nothing else return
133     return('', 'empty')
134       unless ($#{$lines} >= 0);
135
136     # deal with example chunks
137     if ($lines->[0] =~ /^(\=\=\>\s+)\S/) {
138         $offlen = length($1);
139         while (($#{$lines} >= 0) && (defined($lines->[0]))) {
140             my $txt = substr(shift @{$lines}, $offlen);
141             $ret .= (defined($ret)) ? "\n$txt" : $txt;
142         }
143         return ($ret, 'code');
144     }
145
146     my $skipone;
147     # deal with rest - numeric lines first
148     if ($lines->[0] =~ /^(\s+\(\d+\)\s*)/) {
149         $offlen = length($1);
150         $flags = 'numlist';
151         $skipone = 0;
152     } elsif ($lines->[0] =~ /^(\s+)\S/) {
153         $offlen = length($1);
154         $flags = 'normal';
155         $skipone = 0;
156     } else {
157         $offlen = 7;
158         $flags = 'normal';
159         $skipone = 1;
160     }
161
162     while (($#{$lines} >= 0) && (defined($lines->[0]))) {
163         my $txt = $skipone ?
164           shift @{$lines} :
165             substr(shift @{$lines}, $offlen);
166         $ret .= $txt;
167         $ret .= ' ';
168         $skipone = 0;
169     }
170     return ($ret, $flags);
171 }
172
173 \f
174
175 sub format_wiki_text ($$) {
176     my $meta = shift;
177     my $lp = shift;
178
179     my @lines = @{$lp};
180
181     my $out;
182     while ($#lines >= 0) {
183         my($para, $flags) = clip_paragraph(\@lines);
184         if ($flags eq 'code') {
185             $out .= "{{{\n" . $para . "\n}}}\n";
186         } elsif ($flags eq 'numlist') {
187             $out .= ' 1. ' . wiki_markup($meta, $para) . "\n";
188         } elsif ($flags eq 'empty') {
189         } else {
190             $out .= wiki_markup($meta, $para) . "\n";
191         }
192     }
193     return $out;
194 }
195
196 \f
197
198 sub output_wiki_header ($$$) {
199     my $fh = shift;
200     my $meta = shift;
201     my $qset = shift;
202
203     $fh->print(join("\n",
204                     '##language:en',
205                     '#pragma section-numbers off',
206                     '## Autogenerated by f2wiki',
207                     join('', '["FAQ"] / [:', 
208                          $qset->{section}->{wikiname},
209                          ':',
210                          $qset->{section}->{title},
211                          '] / ',
212                          $qset->{qtag}),
213                     '----',
214                     '[[Navigation(siblings)]]',
215                     '----',
216                     ''));
217 }
218
219 \f
220
221 sub output_wiki_question ($$$$) {
222     my $fh = shift;
223     my $meta = shift;
224     my $qset = shift;
225     my $lines = shift;
226
227     $fh->print(join("\n",
228                     ('= ' . $qset->{qtag} . ' ='),
229                     '',
230                     '=== Question ===',
231                     '##qstart',
232                     format_wiki_text($meta, $lines),
233                     '##qend',
234                     ''));
235 }
236
237 \f
238
239 sub output_wiki_answer ($$$$) {
240     my $fh = shift;
241     my $meta = shift;
242     my $qset = shift;
243     my $lines = shift;
244
245     $fh->print(join("\n",
246                     '=== Answer ===',
247                     format_wiki_text($meta, $lines),
248                     ''));
249 }
250
251 \f
252
253 sub output_wiki_trailer ($$$) {
254     my $fh = shift;
255     my $meta = shift;
256     my $qset = shift;
257
258     $fh->print(join("\n",
259                     '----',
260                     '[[Navigation(siblings)]]',
261                     '----',
262                     join('', '["FAQ"] / [:', 
263                          $qset->{section}->{wikiname},
264                          ':',
265                          $qset->{section}->{title},
266                          '] / ',
267                          $qset->{qtag}),
268                     '----',
269                     'CategoryFrequentlyAskedQuestions',
270                     ''));
271 }
272
273 \f
274
275 sub build_tocs ($) {
276     my $meta = shift;
277
278     my $tfh = IO::File->new('FAQ', 'w');
279     foreach my $sect (values %{$meta->{sections}}) {
280         my $fh = IO::File->new($sect->{wikifile}, 'w');
281         $fh->print(join("\n",
282                         '##language:en',
283                         '#pragma section-numbers off',
284                         '## Autogenerated by f2wiki',
285                         join('', '["FAQ"] / [:', 
286                              $sect->{wikiname},
287                              ':',
288                              $sect->{title},
289                              '] '),
290                         '----',
291                         '[[Navigation(siblings,1)]]',
292                         '----',
293                         '[[Navigation(children)]]',
294                         '----',
295                         '',
296                         '',
297                        '= ' . $sect->{title} . ' =',
298                        '',
299                        join('',
300                             '[[Include(^',
301                             $sect->{wikiname},
302                             '/.*,,2,from="##qstart",to="##qend")]]'),
303                         '',
304                         '----',
305                         '[[Navigation(siblings,1)]]',
306                         '----',
307                         '[[Navigation(children)]]',
308                         '----',
309                         join('', '["FAQ"] / [:', 
310                              $sect->{wikiname},
311                              ':',
312                              $sect->{title},
313                              '] '),
314                         '----',
315                         'CategoryFrequentlyAskedQuestions',
316                         ''));
317
318         $tfh->print(' * [:', $sect->{wikiname}, ':', $sect->{title}, "]\n");
319     }
320 }
321
322 \f
323
324 sub process_qset ($$$$) {
325     my $meta = shift;
326     my $qset = shift;
327     my $qlines = shift;
328     my $alines = shift;
329
330     unless ($qset->{wikifile}) {
331         print(join("\n#",
332                    $qset->{qtag},
333                    $qset->{wikiname},
334                    $qset->{wikifile}),
335               "\n");
336         return;
337     }
338     my $fh = IO::File->new($qset->{wikifile}, 'w') ||
339       die "$qset->{wikifile} OUT $!";
340     output_wiki_header($fh, $meta, $qset);
341     output_wiki_question($fh, $meta, $qset, $qlines);
342     output_wiki_answer($fh, $meta, $qset, $alines);
343     output_wiki_trailer($fh, $meta, $qset);
344 }
345
346 \f
347
348 sub parse_faqsrc ($$) {
349     my $fh = shift;
350     my $meta = shift;
351
352     my $section;
353     my $sect;
354
355     while(<$fh>) {
356         chomp;
357         unless(defined($section)) {
358             unless (/^\d+\.\s/) {
359                 if (/^\s+\d+\./) {
360                     my($junk,
361                        $secnum,
362                        $sectitle) = split(/\s+/, $_, 3);
363                     $secnum =~ tr/0-9//cd;
364                     my $wikiname = mkwikiname('FAQ', $sectitle);
365                     my $wikifile = mkwikifilename($wikiname);
366                     $meta->{sections}->{$secnum} =
367                       {title    => $sectitle,
368                        num      => $secnum,
369                        wikiname => $wikiname,
370                        wikifile => $wikifile,
371                        qtags    => []};
372                 }
373                 next;
374             }
375         }
376         if (/^(\d+)\.\s/) {
377             $section = $1;
378             $sect = $meta->{sections}->{$section};
379             $sect->{seen}++;
380         } elsif (/^(Q\d+):/) {
381             my $qtag = $1;
382             my $wikiname = mkwikiname('FAQ', $sect->{title}, $qtag);
383             my $wikifile = mkwikifilename($wikiname);
384             my $qset = {section         => $sect,
385                         qtag            => $qtag,
386                         wikiname        => $wikiname,
387                         wikifile        => $wikifile};
388             $meta->{qtags}->{$qtag} = $qset;
389             push(@{$sect->{qtags}}, $qset);
390         }
391     }
392 }
393
394 \f
395
396 sub process_faqsrc ($$) {
397     my $fh = shift;
398     my $meta = shift;
399
400     my $qset;
401     my $qlines = [];
402     my $alines = [];
403     my $clines = $qlines;
404
405     while(<$fh>) {
406         chomp;
407         next if (/^#/);
408         # skip preceding stuff....
409         unless(defined($qset)) {
410             next unless (/^Q\d+/);
411         }
412
413         if (/^(\d+)\.\s/) {
414             # just skip section boundaries - we have done those before
415             next;
416         } elsif (/^([QA]\d+):\s+(.+)$/) {
417             my $qtag = $1;
418             my $line = $2;
419             if (substr($1, 0, 1) eq 'Q') {
420                 process_qset($meta, $qset, $qlines, $alines);
421                 $qlines = [];
422                 $alines = [];
423                 $clines = $qlines;
424                 $qset = $meta->{qtags}->{$qtag};
425             } else {
426                 $clines = $alines;
427             }
428             push(@{$clines}, $line);
429         } elsif (/^\s*$/) {
430             push(@{$clines}, undef);
431         } else {
432             push(@{$clines}, $_);
433         }
434     }
435     # mop up last q&a
436     process_qset($meta, $qset, $qlines, $alines);
437
438     # now build the tocs
439     build_tocs($meta);
440 }
441
442 \f
443
444 # main
445 {
446     my $section;
447
448     my $fh = IO::File->new(shift, 'r') || die $!;
449     my $state = {};
450     parse_faqsrc($fh, $state);
451     $fh->seek(0,0);
452 #    print Dumper($state);
453     process_faqsrc($fh, $state);
454
455 }
456
457 # -*-perl-*-