1 #! /usr/bin/perl -w
2 #
3 # Generate API documentation. See documentation/documentation.sgml for details.
4 #
5 # Copyright (C) 2000 Mike McCormack
6 # Copyright (C) 2003 Jon Griffiths
7 #
8 # This library is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU Lesser General Public
10 # License as published by the Free Software Foundation; either
11 # version 2.1 of the License, or (at your option) any later version.
12 #
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 # Lesser General Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public
19 # License along with this library; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
21 #
22 # TODO
23 # Consolidate A+W pairs together, and only write one doc, without the suffix
24 # Implement automatic docs fo structs/defines in headers
25 # SGML gurus - feel free to smarten up the SGML.
26 # Add any other relevant information for the dll - imports etc
27 # Should we have a special output mode for WineHQ?
28
29 use strict;
30 use bytes;
31
32 # Function flags. most of these come from the spec flags
33 my $FLAG_DOCUMENTED = 1;
34 my $FLAG_NONAME = 2;
35 my $FLAG_I386 = 4;
36 my $FLAG_REGISTER = 8;
37 my $FLAG_APAIR = 16; # The A version of a matching W function
38 my $FLAG_WPAIR = 32; # The W version of a matching A function
39 my $FLAG_64PAIR = 64; # The 64 bit version of a matching 32 bit function
40
41
42 # Options
43 my $opt_output_directory = "man3w"; # All default options are for nroff (man pages)
44 my $opt_manual_section = "3w";
45 my $opt_source_dir = "";
46 my $opt_wine_root_dir = "";
47 my $opt_output_format = ""; # '' = nroff, 'h' = html, 's' = sgml
48 my $opt_output_empty = 0; # Non-zero = Create 'empty' comments (for every implemented function)
49 my $opt_fussy = 1; # Non-zero = Create only if we have a RETURNS section
50 my $opt_verbose = 0; # >0 = verbosity. Can be given multiple times (for debugging)
51 my @opt_header_file_list = ();
52 my @opt_spec_file_list = ();
53 my @opt_source_file_list = ();
54
55 # All the collected details about all the .spec files being processed
56 my %spec_files;
57 # All the collected details about all the source files being processed
58 my %source_files;
59 # All documented functions that are to be placed in the index
60 my @index_entries_list = ();
61
62 # useful globals
63 my $pwd = `pwd`."/";
64 $pwd =~ s/\n//;
65 my @datetime = localtime;
66 my @months = ( "Jan", "Feb", "Mar", "Apr", "May", "Jun",
67 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" );
68 my $year = $datetime[5] + 1900;
69 my $date = "$months[$datetime[4]] $year";
70
71
72 sub output_api_comment($);
73 sub output_api_footer($);
74 sub output_api_header($);
75 sub output_api_name($);
76 sub output_api_synopsis($);
77 sub output_close_api_file();
78 sub output_comment($);
79 sub output_html_index_files();
80 sub output_html_stylesheet();
81 sub output_open_api_file($);
82 sub output_sgml_dll_file($);
83 sub output_sgml_master_file($);
84 sub output_spec($);
85 sub process_comment($);
86 sub process_extra_comment($);
87
88
89 # Generate the list of exported entries for the dll
90 sub process_spec_file($)
91 {
92 my $spec_name = shift;
93 my ($dll_name, $dll_ext) = split(/\./, $spec_name);
94 $dll_ext = "dll" if ( $dll_ext eq "spec" );
95 my $uc_dll_name = uc $dll_name;
96
97 my $spec_details =
98 {
99 NAME => $spec_name,
100 DLL_NAME => $dll_name,
101 DLL_EXT => $dll_ext,
102 NUM_EXPORTS => 0,
103 NUM_STUBS => 0,
104 NUM_FUNCS => 0,
105 NUM_FORWARDS => 0,
106 NUM_VARS => 0,
107 NUM_DOCS => 0,
108 CONTRIBUTORS => [ ],
109 SOURCES => [ ],
110 DESCRIPTION => [ ],
111 EXPORTS => [ ],
112 EXPORTED_NAMES => { },
113 IMPLEMENTATION_NAMES => { },
114 EXTRA_COMMENTS => [ ],
115 CURRENT_EXTRA => [ ] ,
116 };
117
118 if ($opt_verbose > 0)
119 {
120 print "Processing ".$spec_name."\n";
121 }
122
123 # We allow opening to fail just to cater for the peculiarities of
124 # the Wine build system. This doesn't hurt, in any case
125 open(SPEC_FILE, "<$spec_name")
126 || (($opt_source_dir ne "")
127 && open(SPEC_FILE, "<$opt_source_dir/$spec_name"))
128 || return;
129
130 while(<SPEC_FILE>)
131 {
132 s/^\s+//; # Strip leading space
133 s/\s+\n$/\n/; # Strip trailing space
134 s/\s+/ /g; # Strip multiple tabs & spaces to a single space
135 s/\s*#.*//; # Strip comments
136 s/\(.*\)/ /; # Strip arguments
137 s/\s+/ /g; # Strip multiple tabs & spaces to a single space (again)
138 s/\n$//; # Strip newline
139
140 my $flags = 0;
141 if( /\-noname/ )
142 {
143 $flags |= $FLAG_NONAME;
144 }
145 if( /\-i386/ )
146 {
147 $flags |= $FLAG_I386;
148 }
149 if( /\-register/ )
150 {
151 $flags |= $FLAG_REGISTER;
152 }
153 s/ \-[a-z0-9]+//g; # Strip flags
154
155 if( /^(([0-9]+)|@) / )
156 {
157 # This line contains an exported symbol
158 my ($ordinal, $call_convention, $exported_name, $implementation_name) = split(' ');
159
160 for ($call_convention)
161 {
162 /^(cdecl|stdcall|varargs|pascal)$/
163 && do { $spec_details->{NUM_FUNCS}++; last; };
164 /^(variable|equate)$/
165 && do { $spec_details->{NUM_VARS}++; last; };
166 /^(extern)$/
167 && do { $spec_details->{NUM_FORWARDS}++; last; };
168 /^stub$/ && do { $spec_details->{NUM_STUBS}++; last; };
169 if ($opt_verbose > 0)
170 {
171 print "Warning: didn't recognise convention \'",$call_convention,"'\n";
172 }
173 last;
174 }
175
176 # Convert ordinal only names so we can find them later
177 if ($exported_name eq "@")
178 {
179 $exported_name = $uc_dll_name.'_'.$ordinal;
180 }
181 if (!defined($implementation_name))
182 {
183 $implementation_name = $exported_name;
184 }
185 if ($implementation_name eq "")
186 {
187 $implementation_name = $exported_name;
188 }
189
190 if ($implementation_name =~ /(.*?)\./)
191 {
192 $call_convention = "forward"; # Referencing a function from another dll
193 $spec_details->{NUM_FUNCS}--;
194 $spec_details->{NUM_FORWARDS}++;
195 }
196
197 # Add indices for the exported and implementation names
198 $spec_details->{EXPORTED_NAMES}{$exported_name} = $spec_details->{NUM_EXPORTS};
199 if ($implementation_name ne $exported_name)
200 {
201 $spec_details->{IMPLEMENTATION_NAMES}{$exported_name} = $spec_details->{NUM_EXPORTS};
202 }
203
204 # Add the exported entry
205 $spec_details->{NUM_EXPORTS}++;
206 my @export = ($ordinal, $call_convention, $exported_name, $implementation_name, $flags);
207 push (@{$spec_details->{EXPORTS}},[@export]);
208 }
209 }
210 close(SPEC_FILE);
211
212 # Add this .spec files details to the list of .spec files
213 $spec_files{$uc_dll_name} = [$spec_details];
214 }
215
216 # Read each source file, extract comments, and generate API documentation if appropriate
217 sub process_source_file($)
218 {
219 my $source_file = shift;
220 my $source_details =
221 {
222 CONTRIBUTORS => [ ],
223 DEBUG_CHANNEL => "",
224 };
225 my $comment =
226 {
227 FILE => $source_file,
228 COMMENT_NAME => "",
229 ALT_NAME => "",
230 DLL_NAME => "",
231 DLL_EXT => "",
232 ORDINAL => "",
233 RETURNS => "",
234 PROTOTYPE => [],
235 TEXT => [],
236 };
237 my $parse_state = 0;
238 my $ignore_blank_lines = 1;
239 my $extra_comment = 0; # 1 if this is an extra comment, i.e its not a .spec export
240
241 if ($opt_verbose > 0)
242 {
243 print "Processing ".$source_file."\n";
244 }
245 open(SOURCE_FILE,"<$source_file")
246 || (($opt_source_dir ne "")
247 && open(SOURCE_FILE,"<$opt_source_dir/$source_file"))
248 || die "couldn't open ".$source_file."\n";
249
250 # Add this source file to the list of source files
251 $source_files{$source_file} = [$source_details];
252
253 while(<SOURCE_FILE>)
254 {
255 s/\n$//; # Strip newline
256 s/^\s+//; # Strip leading space
257 s/\s+$//; # Strip trailing space
258 if (! /^\*\|/ )
259 {
260 # Strip multiple tabs & spaces to a single space
261 s/\s+/ /g;
262 }
263
264 if ( / +Copyright *(\([Cc]\))*[0-9 \-\,\/]*([[:alpha:][:^ascii:] \.\-]+)/ )
265 {
266 # Extract a contributor to this file
267 my $contributor = $2;
268 $contributor =~ s/ *$//;
269 $contributor =~ s/^by //;
270 $contributor =~ s/\.$//;
271 $contributor =~ s/ (for .*)/ \($1\)/;
272 if ($contributor ne "")
273 {
274 if ($opt_verbose > 3)
275 {
276 print "Info: Found contributor:'".$contributor."'\n";
277 }
278 push (@{$source_details->{CONTRIBUTORS}},$contributor);
279 }
280 }
281 elsif ( /WINE_DEFAULT_DEBUG_CHANNEL\(([A-Za-z]*)\)/ )
282 {
283 # Extract the debug channel to use
284 if ($opt_verbose > 3)
285 {
286 print "Info: Found debug channel:'".$1."'\n";
287 }
288 $source_details->{DEBUG_CHANNEL} = $1;
289 }
290
291 if ($parse_state == 0) # Searching for a comment
292 {
293 if ( /^\/\**$/ )
294 {
295 # Found a comment start
296 $comment->{COMMENT_NAME} = "";
297 $comment->{ALT_NAME} = "";
298 $comment->{DLL_NAME} = "";
299 $comment->{ORDINAL} = "";
300 $comment->{RETURNS} = "";
301 $comment->{PROTOTYPE} = [];
302 $comment->{TEXT} = [];
303 $ignore_blank_lines = 1;
304 $extra_comment = 0;
305 $parse_state = 3;
306 }
307 }
308 elsif ($parse_state == 1) # Reading in a comment
309 {
310 if ( /^\**\// )
311 {
312 # Found the end of the comment
313 $parse_state = 2;
314 }
315 elsif ( s/^\*\|/\|/ )
316 {
317 # A line of comment not meant to be pre-processed
318 push (@{$comment->{TEXT}},$_); # Add the comment text
319 }
320 elsif ( s/^ *\** *// )
321 {
322 # A line of comment, starting with an asterisk
323 if ( /^[A-Z]+$/ || $_ eq "")
324 {
325 # This is a section start, so skip blank lines before and after it.
326 my $last_line = pop(@{$comment->{TEXT}});
327 if (defined($last_line) && $last_line ne "")
328 {
329 # Put it back
330 push (@{$comment->{TEXT}},$last_line);
331 }
332 if ( /^[A-Z]+$/ )
333 {
334 $ignore_blank_lines = 1;
335 }
336 else
337 {
338 $ignore_blank_lines = 0;
339 }
340 }
341
342 if ($ignore_blank_lines == 0 || $_ ne "")
343 {
344 push (@{$comment->{TEXT}},$_); # Add the comment text
345 }
346 }
347 else
348 {
349 # This isn't a well formatted comment: look for the next one
350 $parse_state = 0;
351 }
352 }
353 elsif ($parse_state == 2) # Finished reading in a comment
354 {
355 if ( /(WINAPIV|WINAPI|__cdecl|PASCAL|CALLBACK|FARPROC16)/ ||
356 /.*?\(/ )
357 {
358 # Comment is followed by a function definition
359 $parse_state = 4; # Fall through to read prototype
360 }
361 else
362 {
363 # Allow cpp directives and blank lines between the comment and prototype
364 if ($extra_comment == 1)
365 {
366 # An extra comment not followed by a function definition
367 $parse_state = 5; # Fall through to process comment
368 }
369 elsif (!/^\#/ && !/^ *$/ && !/^__ASM_GLOBAL_FUNC/)
370 {
371 # This isn't a well formatted comment: look for the next one
372 if ($opt_verbose > 1)
373 {
374 print "Info: Function '",$comment->{COMMENT_NAME},"' not followed by prototype.\n";
375 }
376 $parse_state = 0;
377 }
378 }
379 }
380 elsif ($parse_state == 3) # Reading in the first line of a comment
381 {
382 s/^ *\** *//;
383 if ( /^([\@A-Za-z0-9_]+) +(\(|\[)([A-Za-z0-9_]+)\.(([0-9]+)|@)(\)|\])\s*(.*)$/ )
384 {
385 # Found a correctly formed "ApiName (DLLNAME.Ordinal)" line.
386 if (defined ($7) && $7 ne "")
387 {
388 push (@{$comment->{TEXT}},$_); # Add the trailing comment text
389 }
390 $comment->{COMMENT_NAME} = $1;
391 $comment->{DLL_NAME} = uc $3;
392 $comment->{ORDINAL} = $4;
393 $comment->{DLL_NAME} =~ s/^KERNEL$/KRNL386/; # Too many of these to ignore, _old_ code
394 $parse_state = 1;
395 }
396 elsif ( /^([A-Za-z0-9_-]+) +\{([A-Za-z0-9_]+)\}$/ )
397 {
398 # Found a correctly formed "CommentTitle {DLLNAME}" line (extra documentation)
399 $comment->{COMMENT_NAME} = $1;
400 $comment->{DLL_NAME} = uc $2;
401 $comment->{ORDINAL} = "";
402 $extra_comment = 1;
403 $parse_state = 1;
404 }
405 else
406 {
407 # This isn't a well formatted comment: look for the next one
408 $parse_state = 0;
409 }
410 }
411
412 if ($parse_state == 4) # Reading in the function definition
413 {
414 push (@{$comment->{PROTOTYPE}},$_);
415 # Strip comments from the line before checking for ')'
416 my $stripped_line = $_;
417 $stripped_line =~ s/ *(\/\* *)(.*?)( *\*\/ *)//;
418 if ( $stripped_line =~ /\)/ )
419 {
420 # Strip a blank last line
421 my $last_line = pop(@{$comment->{TEXT}});
422 if (defined($last_line) && $last_line ne "")
423 {
424 # Put it back
425 push (@{$comment->{TEXT}},$last_line);
426 }
427
428 if ($opt_output_empty != 0 && @{$comment->{TEXT}} == 0)
429 {
430 # Create a 'not implemented' comment
431 @{$comment->{TEXT}} = ("fixme: This function has not yet been documented.");
432 }
433 $parse_state = 5;
434 }
435 }
436
437 if ($parse_state == 5) # Processing the comment
438 {
439 # Process it, if it has any text
440 if (@{$comment->{TEXT}} > 0)
441 {
442 if ($extra_comment == 1)
443 {
444 process_extra_comment($comment);
445 }
446 else
447 {
448 @{$comment->{TEXT}} = ("DESCRIPTION", @{$comment->{TEXT}});
449 process_comment($comment);
450 }
451 }
452 elsif ($opt_verbose > 1 && $opt_output_empty == 0)
453 {
454 print "Info: Function '",$comment->{COMMENT_NAME},"' has no documentation.\n";
455 }
456 $parse_state = 0;
457 }
458 }
459 close(SOURCE_FILE);
460 }
461
462 # Standardise a comments text for consistency
463 sub process_comment_text($)
464 {
465 my $comment = shift;
466 my $in_params = 0;
467 my @tmp_list = ();
468 my $i = 0;
469
470 for (@{$comment->{TEXT}})
471 {
472 my $line = $_;
473
474 if ( /^\s*$/ || /^[A-Z]+$/ || /^-/ )
475 {
476 $in_params = 0;
477 }
478 if ( $in_params > 0 && !/\[/ && !/\]/ )
479 {
480 # Possibly a continuation of the parameter description
481 my $last_line = pop(@tmp_list);
482 if ( $last_line =~ /\[/ && $last_line =~ /\]/ )
483 {
484 $line = $last_line." ".$_;
485 }
486 else
487 {
488 $in_params = 0;
489 push (@tmp_list, $last_line);
490 }
491 }
492 if ( /^(PARAMS|MEMBERS)$/ )
493 {
494 $in_params = 1;
495 }
496 push (@tmp_list, $line);
497 }
498
499 @{$comment->{TEXT}} = @tmp_list;
500
501 for (@{$comment->{TEXT}})
502 {
503 if (! /^\|/ )
504 {
505 # Map I/O values. These come in too many formats to standardise now....
506 s/\[I\]|\[i\]|\[in\]|\[IN\]/\[In\] /g;
507 s/\[O\]|\[o\]|\[out\]|\[OUT\]/\[Out\]/g;
508 s/\[I\/O\]|\[I\,O\]|\[i\/o\]|\[in\/out\]|\[IN\/OUT\]/\[In\/Out\]/g;
509 # TRUE/FALSE/NULL are defines, capitilise them
510 s/True|true/TRUE/g;
511 s/False|false/FALSE/g;
512 s/Null|null/NULL/g;
513 # Preferred capitalisations
514 s/ wine| WINE/ Wine/g;
515 s/ API | api / Api /g;
516 s/ DLL | Dll / dll /g;
517 s/ URL | url / Url /g;
518 s/WIN16|win16/Win16/g;
519 s/WIN32|win32/Win32/g;
520 s/WIN64|win64/Win64/g;
521 s/ ID | id / Id /g;
522 # Grammar
523 s/([a-z])\.([A-Z])/$1\. $2/g; # Space after full stop
524 s/ \:/\:/g; # Colons to the left
525 s/ \;/\;/g; # Semi-colons too
526 # Common idioms
527 s/^See ([A-Za-z0-9_]+)\.$/See $1\(\)\./; # Referring to A version from W
528 s/^Unicode version of ([A-Za-z0-9_]+)\.$/See $1\(\)\./; # Ditto
529 s/^64\-bit version of ([A-Za-z0-9_]+)\.$/See $1\(\)\./; # Referring to 32 bit version from 64
530 s/^PARAMETERS$/PARAMS/; # Name of parameter section should be 'PARAMS'
531 # Trademarks
532 s/( |\.)(M\$|MS|Microsoft|microsoft|micro\$oft|Micro\$oft)( |\.)/$1Microsoft\(tm\)$3/g;
533 s/( |\.)(Windows|windows|windoze|winblows)( |\.)/$1Windows\(tm\)$3/g;
534 s/( |\.)(DOS|dos|msdos)( |\.)/$1MS-DOS\(tm\)$3/g;
535 s/( |\.)(UNIX|unix)( |\.)/$1Unix\(tm\)$3/g;
536 s/( |\.)(LINIX|linux)( |\.)/$1Linux\(tm\)$3/g;
537 # Abbreviations
538 s/( char )/ character /g;
539 s/( chars )/ characters /g;
540 s/( info )/ information /g;
541 s/( app )/ application /g;
542 s/( apps )/ applications /g;
543 s/( exe )/ executable /g;
544 s/( ptr )/ pointer /g;
545 s/( obj )/ object /g;
546 s/( err )/ error /g;
547 s/( bool )/ boolean /g;
548 s/( no\. )/ number /g;
549 s/( No\. )/ Number /g;
550 # Punctuation
551 if ( /\[I|\[O/ && ! /\.$/ )
552 {
553 $_ = $_."."; # Always have a full stop at the end of parameter desc.
554 }
555 elsif ($i > 0 && /^[A-Z]*$/ &&
556 !(@{$comment->{TEXT}}[$i-1] =~ /\.$/) &&
557 !(@{$comment->{TEXT}}[$i-1] =~ /\:$/))
558 {
559
560 if (!(@{$comment->{TEXT}}[$i-1] =~ /^[A-Z]*$/))
561 {
562 # Paragraphs always end with a full stop
563 @{$comment->{TEXT}}[$i-1] = @{$comment->{TEXT}}[$i-1].".";
564 }
565 }
566 }
567 $i++;
568 }
569 }
570
571 # Standardise our comment and output it if it is suitable.
572 sub process_comment($)
573 {
574 my $comment = shift;
575
576 # Don't process this comment if the function isn't exported
577 my $spec_details = $spec_files{$comment->{DLL_NAME}}[0];
578
579 if (!defined($spec_details))
580 {
581 if ($opt_verbose > 2)
582 {
583 print "Warning: Function '".$comment->{COMMENT_NAME}."' belongs to '".
584 $comment->{DLL_NAME}."' (not passed with -w): not processing it.\n";
585 }
586 return;
587 }
588
589 if ($comment->{COMMENT_NAME} eq "@")
590 {
591 my $found = 0;
592
593 # Find the name from the .spec file
594 for (@{$spec_details->{EXPORTS}})
595 {
596 if (@$_[0] eq $comment->{ORDINAL})
597 {
598 $comment->{COMMENT_NAME} = @$_[2];
599 $found = 1;
600 }
601 }
602
603 if ($found == 0)
604 {
605 # Create an implementation name
606 $comment->{COMMENT_NAME} = $comment->{DLL_NAME}."_".$comment->{ORDINAL};
607 }
608 }
609
610 my $exported_names = $spec_details->{EXPORTED_NAMES};
611 my $export_index = $exported_names->{$comment->{COMMENT_NAME}};
612 my $implementation_names = $spec_details->{IMPLEMENTATION_NAMES};
613
614 if (!defined($export_index))
615 {
616 # Perhaps the comment uses the implementation name?
617 $export_index = $implementation_names->{$comment->{COMMENT_NAME}};
618 }
619 if (!defined($export_index))
620 {
621 # This function doesn't appear to be exported. hmm.
622 if ($opt_verbose > 2)
623 {
624 print "Warning: Function '".$comment->{COMMENT_NAME}."' claims to belong to '".
625 $comment->{DLL_NAME}."' but is not exported by it: not processing it.\n";
626 }
627 return;
628 }
629
630 # When the function is exported twice we have the second name below the first
631 # (you see this a lot in ntdll, but also in some other places).
632 my $first_line = ${@{$comment->{TEXT}}}[1];
633
634 if ( $first_line =~ /^(@|[A-Za-z0-9_]+) +(\(|\[)([A-Za-z0-9_]+)\.(([0-9]+)|@)(\)|\])$/ )
635 {
636 # Found a second name - mark it as documented
637 my $alt_index = $exported_names->{$1};
638 if (defined($alt_index))
639 {
640 if ($opt_verbose > 2)
641 {
642 print "Info: Found alternate name '",$1,"\n";
643 }
644 my $alt_export = @{$spec_details->{EXPORTS}}[$alt_index];
645 @$alt_export[4] |= $FLAG_DOCUMENTED;
646 $spec_details->{NUM_DOCS}++;
647 ${@{$comment->{TEXT}}}[1] = "";
648 }
649 }
650
651 if (@{$spec_details->{CURRENT_EXTRA}})
652 {
653 # We have an extra comment that might be related to this one
654 my $current_comment = ${@{$spec_details->{CURRENT_EXTRA}}}[0];
655 my $current_name = $current_comment->{COMMENT_NAME};
656 if ($comment->{COMMENT_NAME} =~ /^$current_name/ && $comment->{COMMENT_NAME} ne $current_name)
657 {
658 if ($opt_verbose > 2)
659 {
660 print "Linking ",$comment->{COMMENT_NAME}," to $current_name\n";
661 }
662 # Add a reference to this comment to our extra comment
663 push (@{$current_comment->{TEXT}}, $comment->{COMMENT_NAME}."()","");
664 }
665 }
666
667 # We want our docs generated using the implementation name, so they are unique
668 my $export = @{$spec_details->{EXPORTS}}[$export_index];
669 $comment->{COMMENT_NAME} = @$export[3];
670 $comment->{ALT_NAME} = @$export[2];
671
672 # Mark the function as documented
673 $spec_details->{NUM_DOCS}++;
674 @$export[4] |= $FLAG_DOCUMENTED;
675
676 # This file is used by the DLL - Make sure we get our contributors right
677 push (@{$spec_details->{SOURCES}},$comment->{FILE});
678
679 # If we have parameter comments in the prototype, extract them
680 my @parameter_comments;
681 for (@{$comment->{PROTOTYPE}})
682 {
683 s/ *\, */\,/g; # Strip spaces from around commas
684
685 if ( s/ *(\/\* *)(.*?)( *\*\/ *)// ) # Strip out comment
686 {
687 my $parameter_comment = $2;
688 if (!$parameter_comment =~ /^\[/ )
689 {
690 # Add [IO] markers so we format the comment correctly
691 $parameter_comment = "[fixme] ".$parameter_comment;
692 }
693 if ( /( |\*)([A-Za-z_]{1}[A-Za-z_0-9]*)(\,|\))/ )
694 {
695 # Add the parameter name
696 $parameter_comment = $2." ".$parameter_comment;
697 }
698 push (@parameter_comments, $parameter_comment);
699 }
700 }
701
702 # If we extracted any prototype comments, add them to the comment text.
703 if (@parameter_comments)
704 {
705 @parameter_comments = ("PARAMS", @parameter_comments);
706 my @new_comment = ();
707 my $inserted_params = 0;
708
709 for (@{$comment->{TEXT}})
710 {
711 if ( $inserted_params == 0 && /^[A-Z]+$/ )
712 {
713 # Found a section header, so this is where we insert
714 push (@new_comment, @parameter_comments);
715 $inserted_params = 1;
716 }
717 push (@new_comment, $_);
718 }
719 if ($inserted_params == 0)
720 {
721 # Add them to the end
722 push (@new_comment, @parameter_comments);
723 }
724 $comment->{TEXT} = [@new_comment];
725 }
726
727 if ($opt_fussy == 1 && $opt_output_empty == 0)
728 {
729 # Reject any comment that doesn't have a description or a RETURNS section.
730 # This is the default for now, 'coz many comments aren't suitable.
731 my $found_returns = 0;
732 my $found_description_text = 0;
733 my $in_description = 0;
734 for (@{$comment->{TEXT}})
735 {
736 if ( /^RETURNS$/ )
737 {
738 $found_returns = 1;
739 $in_description = 0;
740 }
741 elsif ( /^DESCRIPTION$/ )
742 {
743 $in_description = 1;
744 }
745 elsif ($in_description == 1)
746 {
747 if ( !/^[A-Z]+$/ )
748 {
749 # Don't reject comments that refer to another doc (e.g. A/W)
750 if ( /^See ([A-Za-z0-9_]+)\.$/ )
751 {
752 if ($comment->{COMMENT_NAME} =~ /W$/ )
753 {
754 # This is probably a Unicode version of an Ascii function.
755 # Create the Ascii name and see if its been documented
756 my $ascii_name = $comment->{COMMENT_NAME};
757 $ascii_name =~ s/W$/A/;
758
759 my $ascii_export_index = $exported_names->{$ascii_name};
760
761 if (!defined($ascii_export_index))
762 {
763 $ascii_export_index = $implementation_names->{$ascii_name};
764 }
765 if (!defined($ascii_export_index))
766 {
767 if ($opt_verbose > 2)
768 {
769 print "Warning: Function '".$comment->{COMMENT_NAME}."' is not an A/W pair.\n";
770 }
771 }
772 else
773 {
774 my $ascii_export = @{$spec_details->{EXPORTS}}[$ascii_export_index];
775 if (@$ascii_export[4] & $FLAG_DOCUMENTED)
776 {
777 # Flag these functions as an A/W pair
778 @$ascii_export[4] |= $FLAG_APAIR;
779 @$export[4] |= $FLAG_WPAIR;
780 }
781 }
782 }
783 $found_returns = 1;
784 }
785 elsif ( /^Unicode version of ([A-Za-z0-9_]+)\.$/ )
786 {
787 @$export[4] |= $FLAG_WPAIR; # Explicitly marked as W version
788 $found_returns = 1;
789 }
790 elsif ( /^64\-bit version of ([A-Za-z0-9_]+)\.$/ )
791 {
792 @$export[4] |= $FLAG_64PAIR; # Explicitly marked as 64 bit version
793 $found_returns = 1;
794 }
795 $found_description_text = 1;
796 }
797 else
798 {
799 $in_description = 0;
800 }
801 }
802 }
803 if ($found_returns == 0 || $found_description_text == 0)
804 {
805 if ($opt_verbose > 2)
806 {
807 print "Info: Function '",$comment->{COMMENT_NAME},"' has no ",
808 "description and/or RETURNS section, skipping\n";
809 }
810 $spec_details->{NUM_DOCS}--;
811 @$export[4] &= ~$FLAG_DOCUMENTED;
812 return;
813 }
814 }
815
816 process_comment_text($comment);
817
818 # Strip the prototypes return value, call convention, name and brackets
819 # (This leaves it as a list of types and names, or empty for void functions)
820 my $prototype = join(" ", @{$comment->{PROTOTYPE}});
821 $prototype =~ s/ / /g;
822
823 if ( $prototype =~ /(WINAPIV|WINAPI|__cdecl|PASCAL|CALLBACK|FARPROC16)/ )
824 {
825 $prototype =~ s/^(.*?) (WINAPIV|WINAPI|__cdecl|PASCAL|CALLBACK|FARPROC16) (.*?)\( *(.*)/$4/;
826 $comment->{RETURNS} = $1;
827 }
828 else
829 {
830 $prototype =~ s/^(.*?)([A-Za-z0-9_]+)\( *(.*)/$3/;
831 $comment->{RETURNS} = $1;
832 }
833
834 $prototype =~ s/ *\).*//; # Strip end bracket
835 $prototype =~ s/ *\* */\*/g; # Strip space around pointers
836 $prototype =~ s/ *\, */\,/g; # Strip space around commas
837 $prototype =~ s/^(void|VOID)$//; # If void, leave blank
838 $prototype =~ s/\*([A-Za-z_])/\* $1/g; # Separate pointers from parameter name
839 @{$comment->{PROTOTYPE}} = split ( /,/ ,$prototype);
840
841 # FIXME: If we have no parameters, make sure we have a PARAMS: None. section
842
843 # Find header file
844 my $h_file = "";
845 if (@$export[4] & $FLAG_NONAME)
846 {
847 $h_file = "Exported by ordinal only. Use GetProcAddress() to obtain a pointer to the function.";
848 }
849 else
850 {
851 if ($comment->{COMMENT_NAME} ne "")
852 {
853 my $tmp = "grep -s -l $comment->{COMMENT_NAME} @opt_header_file_list 2>/dev/null";
854 $tmp = `$tmp`;
855 my $exit_value = $? >> 8;
856 if ($exit_value == 0)
857 {
858 $tmp =~ s/\n.*//g;
859 if ($tmp ne "")
860 {
861 $h_file = `basename $tmp`;
862 }
863 }
864 }
865 elsif ($comment->{ALT_NAME} ne "")
866 {
867 my $tmp = "grep -s -l $comment->{ALT_NAME} @opt_header_file_list"." 2>/dev/null";
868 $tmp = `$tmp`;
869 my $exit_value = $? >> 8;
870 if ($exit_value == 0)
871 {
872 $tmp =~ s/\n.*//g;
873 if ($tmp ne "")
874 {
875 $h_file = `basename $tmp`;
876 }
877 }
878 }
879 $h_file =~ s/^ *//;
880 $h_file =~ s/\n//;
881 if ($h_file eq "")
882 {
883 $h_file = "Not defined in a Wine header. The function is either undocumented, or missing from Wine."
884 }
885 else
886 {
887 $h_file = "Defined in \"".$h_file."\".";
888 }
889 }
890
891 # Find source file
892 my $c_file = $comment->{FILE};
893 if ($opt_wine_root_dir ne "")
894 {
895 my $cfile = $pwd."/".$c_file; # Current dir + file
896 $cfile =~ s/(.+)(\/.*$)/$1/; # Strip the filename
897 $cfile = `cd $cfile && pwd`; # Strip any relative parts (e.g. "../../")
898 $cfile =~ s/\n//; # Strip newline
899 my $newfile = $c_file;
900 $newfile =~ s/(.+)(\/.*$)/$2/; # Strip all but the filename
901 $cfile = $cfile."/".$newfile; # Append filename to base path
902 $cfile =~ s/$opt_wine_root_dir//; # Get rid of the root directory
903 $cfile =~ s/\/\//\//g; # Remove any double slashes
904 $cfile =~ s/^\/+//; # Strip initial directory slash
905 $c_file = $cfile;
906 }
907 $c_file = "Implemented in \"".$c_file."\".";
908
909 # Add the implementation details
910 push (@{$comment->{TEXT}}, "IMPLEMENTATION","",$h_file,"",$c_file);
911
912 if (@$export[4] & $FLAG_I386)
913 {
914 push (@{$comment->{TEXT}}, "", "Available on x86 platforms only.");
915 }
916 if (@$export[4] & $FLAG_REGISTER)
917 {
918 push (@{$comment->{TEXT}}, "", "This function passes one or more arguments in registers. ",
919 "For more details, please read the source code.");
920 }
921 my $source_details = $source_files{$comment->{FILE}}[0];
922 if ($source_details->{DEBUG_CHANNEL} ne "")
923 {
924 push (@{$comment->{TEXT}}, "", "Debug channel \"".$source_details->{DEBUG_CHANNEL}."\".");
925 }
926
927 # Write out the documentation for the API
928 output_comment($comment)
929 }
930
931 # process our extra comment and output it if it is suitable.
932 sub process_extra_comment($)
933 {
934 my $comment = shift;
935
936 my $spec_details = $spec_files{$comment->{DLL_NAME}}[0];
937
938 if (!defined($spec_details))
939 {
940 if ($opt_verbose > 2)
941 {
942 print "Warning: Extra comment '".$comment->{COMMENT_NAME}."' belongs to '".
943 $comment->{DLL_NAME}."' (not passed with -w): not processing it.\n";
944 }
945 return;
946 }
947
948 # Check first to see if this is documentation for the DLL.
949 if ($comment->{COMMENT_NAME} eq $comment->{DLL_NAME})
950 {
951 if ($opt_verbose > 2)
952 {
953 print "Info: Found DLL documentation\n";
954 }
955 for (@{$comment->{TEXT}})
956 {
957 push (@{$spec_details->{DESCRIPTION}}, $_);
958 }
959 return;
960 }
961
962 # Add the comment to the DLL page as a link
963 push (@{$spec_details->{EXTRA_COMMENTS}},$comment->{COMMENT_NAME});
964
965 # If we have a prototype, process as a regular comment
966 if (@{$comment->{PROTOTYPE}})
967 {
968 $comment->{ORDINAL} = "@";
969
970 # Add an index for the comment name
971 $spec_details->{EXPORTED_NAMES}{$comment->{COMMENT_NAME}} = $spec_details->{NUM_EXPORTS};
972
973 # Add a fake exported entry
974 $spec_details->{NUM_EXPORTS}++;
975 my ($ordinal, $call_convention, $exported_name, $implementation_name, $documented) =
976 ("@", "fake", $comment->{COMMENT_NAME}, $comment->{COMMENT_NAME}, 0);
977 my @export = ($ordinal, $call_convention, $exported_name, $implementation_name, $documented);
978 push (@{$spec_details->{EXPORTS}},[@export]);
979 @{$comment->{TEXT}} = ("DESCRIPTION", @{$comment->{TEXT}});
980 process_comment($comment);
981 return;
982 }
983
984 if ($opt_verbose > 0)
985 {
986 print "Processing ",$comment->{COMMENT_NAME},"\n";
987 }
988
989 if (@{$spec_details->{CURRENT_EXTRA}})
990 {
991 my $current_comment = ${@{$spec_details->{CURRENT_EXTRA}}}[0];
992
993 if ($opt_verbose > 0)
994 {
995 print "Processing old current: ",$current_comment->{COMMENT_NAME},"\n";
996 }
997 # Output the current comment
998 process_comment_text($current_comment);
999 output_open_api_file($current_comment->{COMMENT_NAME});
1000 output_api_header($current_comment);
1001 output_api_name($current_comment);
1002 output_api_comment($current_comment);
1003 output_api_footer($current_comment);
1004 output_close_api_file();
1005 }
1006
1007 if ($opt_verbose > 2)
1008 {
1009 print "Setting current to ",$comment->{COMMENT_NAME},"\n";
1010 }
1011
1012 my $comment_copy =
1013 {
1014 FILE => $comment->{FILE},
1015 COMMENT_NAME => $comment->{COMMENT_NAME},
1016 ALT_NAME => $comment->{ALT_NAME},
1017 DLL_NAME => $comment->{DLL_NAME},
1018 ORDINAL => $comment->{ORDINAL},
1019 RETURNS => $comment->{RETURNS},
1020 PROTOTYPE => [],
1021 TEXT => [],
1022 };
1023
1024 for (@{$comment->{TEXT}})
1025 {
1026 push (@{$comment_copy->{TEXT}}, $_);
1027 }
1028 # Set this comment to be the current extra comment
1029 @{$spec_details->{CURRENT_EXTRA}} = ($comment_copy);
1030 }
1031
1032 # Write a standardised comment out in the appropriate format
1033 sub output_comment($)
1034 {
1035 my $comment = shift;
1036
1037 if ($opt_verbose > 0)
1038 {
1039 print "Processing ",$comment->{COMMENT_NAME},"\n";
1040 }
1041
1042 if ($opt_verbose > 4)
1043 {
1044 print "--PROTO--\n";
1045 for (@{$comment->{PROTOTYPE}})
1046 {
1047 print "'".$_."'\n";
1048 }
1049
1050 print "--COMMENT--\n";
1051 for (@{$comment->{TEXT} })
1052 {
1053 print $_."\n";
1054 }
1055 }
1056
1057 output_open_api_file($comment->{COMMENT_NAME});
1058 output_api_header($comment);
1059 output_api_name($comment);
1060 output_api_synopsis($comment);
1061 output_api_comment($comment);
1062 output_api_footer($comment);
1063 output_close_api_file();
1064 }
1065
1066 # Write out an index file for each .spec processed
1067 sub process_index_files()
1068 {
1069 foreach my $spec_file (keys %spec_files)
1070 {
1071 my $spec_details = $spec_files{$spec_file}[0];
1072 if (defined ($spec_details->{DLL_NAME}))
1073 {
1074 if (@{$spec_details->{CURRENT_EXTRA}})
1075 {
1076 # We have an unwritten extra comment, write it
1077 my $current_comment = ${@{$spec_details->{CURRENT_EXTRA}}}[0];
1078 process_extra_comment($current_comment);
1079 @{$spec_details->{CURRENT_EXTRA}} = ();
1080 }
1081 output_spec($spec_details);
1082 }
1083 }
1084 }
1085
1086 # Write a spec files documentation out in the appropriate format
1087 sub output_spec($)
1088 {
1089 my $spec_details = shift;
1090
1091 if ($opt_verbose > 2)
1092 {
1093 print "Writing:",$spec_details->{DLL_NAME},"\n";
1094 }
1095
1096 # Use the comment output functions for consistency
1097 my $comment =
1098 {
1099 FILE => $spec_details->{DLL_NAME},
1100 COMMENT_NAME => $spec_details->{DLL_NAME}.".".$spec_details->{DLL_EXT},
1101 ALT_NAME => $spec_details->{DLL_NAME},
1102 DLL_NAME => "",
1103 ORDINAL => "",
1104 RETURNS => "",
1105 PROTOTYPE => [],
1106 TEXT => [],
1107 };
1108 my $total_implemented = $spec_details->{NUM_FORWARDS} + $spec_details->{NUM_VARS} +
1109 $spec_details->{NUM_FUNCS};
1110 my $percent_implemented = 0;
1111 if ($total_implemented)
1112 {
1113 $percent_implemented = $total_implemented /
1114 ($total_implemented + $spec_details->{NUM_STUBS}) * 100;
1115 }
1116 $percent_implemented = int($percent_implemented);
1117 my $percent_documented = 0;
1118 if ($spec_details->{NUM_DOCS})
1119 {
1120 # Treat forwards and data as documented funcs for statistics
1121 $percent_documented = $spec_details->{NUM_DOCS} / $spec_details->{NUM_FUNCS} * 100;
1122 $percent_documented = int($percent_documented);
1123 }
1124
1125 # Make a list of the contributors to this DLL. Do this only for the source
1126 # files that make up the DLL, because some directories specify multiple dlls.
1127 my @contributors;
1128
1129 for (@{$spec_details->{SOURCES}})
1130 {
1131 my $source_details = $source_files{$_}[0];
1132 for (@{$source_details->{CONTRIBUTORS}})
1133 {
1134 push (@contributors, $_);
1135 }
1136 }
1137
1138 my %saw;
1139 @contributors = grep(!$saw{$_}++, @contributors); # remove dups, from perlfaq4 manpage
1140 @contributors = sort @contributors;
1141
1142 # Remove duplicates and blanks
1143 for(my $i=0; $i<@contributors; $i++)
1144 {
1145 if ($i > 0 && ($contributors[$i] =~ /$contributors[$i-1]/ || $contributors[$i-1] eq ""))
1146 {
1147 $contributors[$i-1] = $contributors[$i];
1148 }
1149 }
1150 undef %saw;
1151 @contributors = grep(!$saw{$_}++, @contributors);
1152
1153 if ($opt_verbose > 3)
1154 {
1155 print "Contributors:\n";
1156 for (@contributors)
1157 {
1158 print "'".$_."'\n";
1159 }
1160 }
1161 my $contribstring = join (", ", @contributors);
1162
1163 # Create the initial comment text
1164 @{$comment->{TEXT}} = (
1165 "NAME",
1166 $comment->{COMMENT_NAME}
1167 );
1168
1169 # Add the description, if we have one
1170 if (@{$spec_details->{DESCRIPTION}})
1171 {
1172 push (@{$comment->{TEXT}}, "DESCRIPTION");
1173 for (@{$spec_details->{DESCRIPTION}})
1174 {
1175 push (@{$comment->{TEXT}}, $_);
1176 }
1177 }
1178
1179 # Add the statistics and contributors
1180 push (@{$comment->{TEXT}},
1181 "STATISTICS",
1182 "Forwards: ".$spec_details->{NUM_FORWARDS},
1183 "Variables: ".$spec_details->{NUM_VARS},
1184 "Stubs: ".$spec_details->{NUM_STUBS},
1185 "Functions: ".$spec_details->{NUM_FUNCS},
1186 "Exports-Total: ".$spec_details->{NUM_EXPORTS},
1187 "Implemented-Total: ".$total_implemented." (".$percent_implemented."%)",
1188 "Documented-Total: ".$spec_details->{NUM_DOCS}." (".$percent_documented."%)",
1189 "CONTRIBUTORS",
1190 "The following people hold copyrights on the source files comprising this dll:",
1191 "",
1192 $contribstring,
1193 "Note: This list may not be complete.",
1194 "For a complete listing, see the Files \"AUTHORS\" and \"Changelog\" in the Wine source tree.",
1195 "",
1196 );
1197
1198 if ($opt_output_format eq "h")
1199 {
1200 # Add the exports to the comment text
1201 push (@{$comment->{TEXT}},"EXPORTS");
1202 my $exports = $spec_details->{EXPORTS};
1203 for (@$exports)
1204 {
1205 my $line = "";
1206
1207 # @$_ => ordinal, call convention, exported name, implementation name, flags;
1208 if (@$_[1] eq "forward")
1209 {
1210 my $forward_dll = @$_[3];
1211 $forward_dll =~ s/\.(.*)//;
1212 $line = @$_[2]." (forward to ".$1."() in ".$forward_dll."())";
1213 }
1214 elsif (@$_[1] eq "extern")
1215 {
1216 $line = @$_[2]." (extern)";
1217 }
1218 elsif (@$_[1] eq "stub")
1219 {
1220 $line = @$_[2]." (stub)";
1221 }
1222 elsif (@$_[1] eq "fake")
1223 {
1224 # Don't add this function here, it gets listed with the extra documentation
1225 if (!(@$_[4] & $FLAG_WPAIR))
1226 {
1227 # This function should be indexed
1228 push (@index_entries_list, @$_[3].",".@$_[3]);
1229 }
1230 }
1231 elsif (@$_[1] eq "equate" || @$_[1] eq "variable")
1232 {
1233 $line = @$_[2]." (data)";
1234 }
1235 else
1236 {
1237 # A function
1238 if (@$_[4] & $FLAG_DOCUMENTED)
1239 {
1240 # Documented
1241 $line = @$_[2]." (implemented as ".@$_[3]."())";
1242 if (@$_[2] ne @$_[3])
1243 {
1244 $line = @$_[2]." (implemented as ".@$_[3]."())";
1245 }
1246 else
1247 {
1248 $line = @$_[2]."()";
1249 }
1250 if (!(@$_[4] & $FLAG_WPAIR))
1251 {
1252 # This function should be indexed
1253 push (@index_entries_list, @$_[2].",".@$_[3]);
1254 }
1255 }
1256 else
1257 {
1258 $line = @$_[2]." (not documented)";
1259 }
1260 }
1261 if ($line ne "")
1262 {
1263 push (@{$comment->{TEXT}}, $line, "");
1264 }
1265 }
1266
1267 # Add links to the extra documentation
1268 if (@{$spec_details->{EXTRA_COMMENTS}})
1269 {
1270 push (@{$comment->{TEXT}}, "SEE ALSO");
1271 my %htmp;
1272 @{$spec_details->{EXTRA_COMMENTS}} = grep(!$htmp{$_}++, @{$spec_details->{EXTRA_COMMENTS}});
1273 for (@{$spec_details->{EXTRA_COMMENTS}})
1274 {
1275 push (@{$comment->{TEXT}}, $_."()", "");
1276 }
1277 }
1278 }
1279 # The dll entry should also be indexed
1280 push (@index_entries_list, $spec_details->{DLL_NAME}.",".$spec_details->{DLL_NAME});
1281
1282 # Write out the document
1283 output_open_api_file($spec_details->{DLL_NAME});
1284 output_api_header($comment);
1285 output_api_comment($comment);
1286 output_api_footer($comment);
1287 output_close_api_file();
1288
1289 # Add this dll to the database of dll names
1290 my $output_file = $opt_output_directory."/dlls.db";
1291
1292 # Append the dllname to the output db of names
1293 open(DLLDB,">>$output_file") || die "Couldn't create $output_file\n";
1294 print DLLDB $spec_details->{DLL_NAME},"\n";
1295 close(DLLDB);
1296
1297 if ($opt_output_format eq "s")
1298 {
1299 output_sgml_dll_file($spec_details);
1300 return;
1301 }
1302 }
1303
1304 #
1305 # OUTPUT FUNCTIONS
1306 # ----------------
1307 # Only these functions know anything about formatting for a specific
1308 # output type. The functions above work only with plain text.
1309 # This is to allow new types of output to be added easily.
1310
1311 # Open the api file
1312 sub output_open_api_file($)
1313 {
1314 my $output_name = shift;
1315 $output_name = $opt_output_directory."/".$output_name;
1316
1317 if ($opt_output_format eq "h")
1318 {
1319 $output_name = $output_name.".html";
1320 }
1321 elsif ($opt_output_format eq "s")
1322 {
1323 $output_name = $output_name.".sgml";
1324 }
1325 else
1326 {
1327 $output_name = $output_name.".".$opt_manual_section;
1328 }
1329 open(OUTPUT,">$output_name") || die "Couldn't create file '$output_name'\n";
1330 }
1331
1332 # Close the api file
1333 sub output_close_api_file()
1334 {
1335 close (OUTPUT);
1336 }
1337
1338 # Output the api file header
1339 sub output_api_header($)
1340 {
1341 my $comment = shift;
1342
1343 if ($opt_output_format eq "h")
1344 {
1345 print OUTPUT "<!-- Generated file - DO NOT EDIT! -->\n";
1346 print OUTPUT "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n";
1347 print OUTPUT "<HTML>\n<HEAD>\n";
1348 print OUTPUT "<LINK REL=\"StyleSheet\" href=\"apidoc.css\" type=\"text/css\">\n";
1349 print OUTPUT "<META NAME=\"GENERATOR\" CONTENT=\"tools/c2man.pl\">\n";
1350 print OUTPUT "<META NAME=\"keywords\" CONTENT=\"Win32,Wine,API,$comment->{COMMENT_NAME}\">\n";
1351 print OUTPUT "<TITLE>Wine API: $comment->{COMMENT_NAME}</TITLE>\n</HEAD>\n<BODY>\n";
1352 }
1353 elsif ($opt_output_format eq "s")
1354 {
1355 print OUTPUT "<!-- Generated file - DO NOT EDIT! -->\n",
1356 "<sect1>\n",
1357 "<title>$comment->{COMMENT_NAME}</title>\n";
1358 }
1359 else