Commit | Line | Data |
---|---|---|
fcb4c255 NM |
1 | #!/usr/bin/perl |
2 | # | |
3 | # Script to convert Exim FAQ into wiki markup - moin flavour | |
4 | # | |
dd3f6d30 | 5 | # $Cambridge: exim/doc/doc-scripts/f2wiki,v 1.2 2004/11/12 15:25:39 nm4 Exp $ |
fcb4c255 NM |
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/``(.*?)''/“$1”/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 /; # 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'); | |
dd3f6d30 NM |
279 | my @sections = sort { $a->{num} <=> $b->{num} } |
280 | values %{$meta->{sections}}; | |
281 | foreach my $sect (@sections) { | |
fcb4c255 NM |
282 | my $fh = IO::File->new($sect->{wikifile}, 'w'); |
283 | $fh->print(join("\n", | |
284 | '##language:en', | |
285 | '#pragma section-numbers off', | |
286 | '## Autogenerated by f2wiki', | |
287 | join('', '["FAQ"] / [:', | |
288 | $sect->{wikiname}, | |
289 | ':', | |
290 | $sect->{title}, | |
291 | '] '), | |
292 | '----', | |
293 | '[[Navigation(siblings,1)]]', | |
294 | '----', | |
295 | '[[Navigation(children)]]', | |
296 | '----', | |
297 | '', | |
298 | '', | |
299 | '= ' . $sect->{title} . ' =', | |
300 | '', | |
301 | join('', | |
302 | '[[Include(^', | |
303 | $sect->{wikiname}, | |
304 | '/.*,,2,from="##qstart",to="##qend")]]'), | |
305 | '', | |
306 | '----', | |
307 | '[[Navigation(siblings,1)]]', | |
308 | '----', | |
309 | '[[Navigation(children)]]', | |
310 | '----', | |
311 | join('', '["FAQ"] / [:', | |
312 | $sect->{wikiname}, | |
313 | ':', | |
314 | $sect->{title}, | |
315 | '] '), | |
316 | '----', | |
317 | 'CategoryFrequentlyAskedQuestions', | |
318 | '')); | |
319 | ||
320 | $tfh->print(' * [:', $sect->{wikiname}, ':', $sect->{title}, "]\n"); | |
321 | } | |
322 | } | |
323 | ||
324 | \f | |
325 | ||
326 | sub process_qset ($$$$) { | |
327 | my $meta = shift; | |
328 | my $qset = shift; | |
329 | my $qlines = shift; | |
330 | my $alines = shift; | |
331 | ||
332 | unless ($qset->{wikifile}) { | |
333 | print(join("\n#", | |
334 | $qset->{qtag}, | |
335 | $qset->{wikiname}, | |
336 | $qset->{wikifile}), | |
337 | "\n"); | |
338 | return; | |
339 | } | |
340 | my $fh = IO::File->new($qset->{wikifile}, 'w') || | |
341 | die "$qset->{wikifile} OUT $!"; | |
342 | output_wiki_header($fh, $meta, $qset); | |
343 | output_wiki_question($fh, $meta, $qset, $qlines); | |
344 | output_wiki_answer($fh, $meta, $qset, $alines); | |
345 | output_wiki_trailer($fh, $meta, $qset); | |
346 | } | |
347 | ||
348 | \f | |
349 | ||
350 | sub parse_faqsrc ($$) { | |
351 | my $fh = shift; | |
352 | my $meta = shift; | |
353 | ||
354 | my $section; | |
355 | my $sect; | |
356 | ||
357 | while(<$fh>) { | |
358 | chomp; | |
359 | unless(defined($section)) { | |
360 | unless (/^\d+\.\s/) { | |
361 | if (/^\s+\d+\./) { | |
362 | my($junk, | |
363 | $secnum, | |
364 | $sectitle) = split(/\s+/, $_, 3); | |
365 | $secnum =~ tr/0-9//cd; | |
366 | my $wikiname = mkwikiname('FAQ', $sectitle); | |
367 | my $wikifile = mkwikifilename($wikiname); | |
368 | $meta->{sections}->{$secnum} = | |
369 | {title => $sectitle, | |
370 | num => $secnum, | |
371 | wikiname => $wikiname, | |
372 | wikifile => $wikifile, | |
373 | qtags => []}; | |
374 | } | |
375 | next; | |
376 | } | |
377 | } | |
378 | if (/^(\d+)\.\s/) { | |
379 | $section = $1; | |
380 | $sect = $meta->{sections}->{$section}; | |
381 | $sect->{seen}++; | |
382 | } elsif (/^(Q\d+):/) { | |
383 | my $qtag = $1; | |
384 | my $wikiname = mkwikiname('FAQ', $sect->{title}, $qtag); | |
385 | my $wikifile = mkwikifilename($wikiname); | |
386 | my $qset = {section => $sect, | |
387 | qtag => $qtag, | |
388 | wikiname => $wikiname, | |
389 | wikifile => $wikifile}; | |
390 | $meta->{qtags}->{$qtag} = $qset; | |
391 | push(@{$sect->{qtags}}, $qset); | |
392 | } | |
393 | } | |
394 | } | |
395 | ||
396 | \f | |
397 | ||
398 | sub process_faqsrc ($$) { | |
399 | my $fh = shift; | |
400 | my $meta = shift; | |
401 | ||
402 | my $qset; | |
403 | my $qlines = []; | |
404 | my $alines = []; | |
405 | my $clines = $qlines; | |
406 | ||
407 | while(<$fh>) { | |
408 | chomp; | |
409 | next if (/^#/); | |
410 | # skip preceding stuff.... | |
411 | unless(defined($qset)) { | |
412 | next unless (/^Q\d+/); | |
413 | } | |
414 | ||
415 | if (/^(\d+)\.\s/) { | |
416 | # just skip section boundaries - we have done those before | |
417 | next; | |
418 | } elsif (/^([QA]\d+):\s+(.+)$/) { | |
419 | my $qtag = $1; | |
420 | my $line = $2; | |
421 | if (substr($1, 0, 1) eq 'Q') { | |
422 | process_qset($meta, $qset, $qlines, $alines); | |
423 | $qlines = []; | |
424 | $alines = []; | |
425 | $clines = $qlines; | |
426 | $qset = $meta->{qtags}->{$qtag}; | |
427 | } else { | |
428 | $clines = $alines; | |
429 | } | |
430 | push(@{$clines}, $line); | |
431 | } elsif (/^\s*$/) { | |
432 | push(@{$clines}, undef); | |
433 | } else { | |
434 | push(@{$clines}, $_); | |
435 | } | |
436 | } | |
437 | # mop up last q&a | |
438 | process_qset($meta, $qset, $qlines, $alines); | |
439 | ||
440 | # now build the tocs | |
441 | build_tocs($meta); | |
442 | } | |
443 | ||
444 | \f | |
445 | ||
446 | # main | |
447 | { | |
448 | my $section; | |
449 | ||
450 | my $fh = IO::File->new(shift, 'r') || die $!; | |
451 | my $state = {}; | |
452 | parse_faqsrc($fh, $state); | |
453 | $fh->seek(0,0); | |
454 | # print Dumper($state); | |
455 | process_faqsrc($fh, $state); | |
456 | ||
457 | } | |
458 | ||
459 | # -*-perl-*- |