Merge pull request #5714 from alexischr/update_bockbuild
[mono.git] / docs / exdoc
1 #!/usr/bin/perl
2
3 use warnings;
4 use strict;
5
6 use Getopt::Long;
7 use Pod::Usage;
8
9 # Options
10 my $HELP = 0;
11 my $SOURCE_DIR = '';
12 my $TARGET_DIR = '';
13 my $WARNINGS = 0;
14
15 GetOptions(
16     "help" => \$HELP,
17     "html|h=s" => \$SOURCE_DIR,
18     "target|t=s" => \$TARGET_DIR,
19     "warnings|W" => \$WARNINGS,
20 ) or pod2usage(1);
21
22 pod2usage(0) if $HELP;
23
24 exdoc();
25
26 #
27 # Main entry point.
28 #
29 sub exdoc {
30     my %templates = ();
31     my %docs = ();
32     my $stylesheet = load_stylesheet($SOURCE_DIR);
33     load_templates($SOURCE_DIR, \%templates);
34     process_source_files(\%docs);
35     merge(\%docs, \%templates, \$stylesheet);
36 }
37
38 #
39 # Load CSS stylesheet.
40 #
41 sub load_stylesheet {
42     my ($dir_path) = @_;
43     my $file_path = "$dir_path/api-style.css";
44     open (my $file, '<', $file_path) or die "Could not open $file_path";
45     local $/;
46     my $contents = <$file>;
47     close $file;
48     return $contents;
49 }
50
51 #
52 # Load HTML templates.
53 #
54 sub load_templates {
55     my ($dir_path, $templates) = @_;
56     opendir (my $dir, "$dir_path/sources/") or die "Could not open $dir_path";
57     while (my $file_name = readdir ($dir)) {
58         next if $file_name !~ /mono-api-.*\.html$/;
59         open (my $file, "$dir_path/sources/$file_name") or die "Could not open $file_name";
60         my $contents = '';
61         my @api = ();
62         while (<$file>) {
63             $contents .= $_;
64             if (/name="api:(.*?)"/) {
65                 s/.*name="api:(\w+?)".*/$1/;
66                 push @api, $_;
67             }
68         }
69         close $file;
70         $templates->{$file_name}->{contents} = $contents;
71         $templates->{$file_name}->{api} = \@api;
72     }
73     closedir $dir;
74 }
75
76 #
77 # Extract documentation from all source files.
78 #
79 sub process_source_files {
80     my ($docs) = @_;
81     for my $file_path (@ARGV) {
82         process_source_file($file_path, $docs);
83     }
84 }
85
86 #
87 # Extract documentation from a single source file.
88 #
89 sub process_source_file {
90     my ($file_path, $docs) = @_;
91     open (my $file, '<', $file_path) or die "Could not open $file_path";
92     while (<$file>) {
93         next if (!/\/\*\* *\n/);
94         process_function($file, $file_path, $docs);
95     }
96     close $file;
97 }
98
99 #
100 # Extract documentation from a single function.
101 #
102 sub process_function {
103
104     my ($file, $file_path, $docs) = @_;
105
106     my $PARAMETER_SECTION = 0;
107     my $BODY_SECTION = 1;
108     my $RETURN_SECTION = 2;
109     my $section = $PARAMETER_SECTION;
110
111     my $name = do {
112         $_ = <$file>;
113         chomp;
114         s/^ \* //;
115         s/:$//;
116         $_
117     };
118
119     # Ignore irrelevant functions, and those with the wrong doc format.
120     return if $name !~ /^mono_\w+$/;
121
122     my $deprecated;
123     my @parameters = ();
124     my $body = '';
125     my $returns = '';
126     my $prototype = '';
127
128     while (<$file>) {
129
130         # We've reached the last line in the documentation block.
131         if (/^ \*\*?\//) {
132
133             # Grab function prototype.
134             while (<$file>) {
135                 $prototype .= $_;
136                 last if /\{/;
137             }
138
139             # Clean up prototype.
140             $prototype = do {
141                 $_ = $prototype;
142                 # Strip braces and trailing whitespace.
143                 s/{//;
144                 s/ +$//;
145                 # Turn "Type * xxx" into "Type* xxx"
146                 s/^(\w+)\W+\*/$1*/;
147                 $_;
148             };
149
150             # Process formatting within sections.
151             for my $parameter (@parameters) {
152                 process_formatting(\$parameter->{description}, $file_path, $.);
153             }
154             process_formatting(\$returns, $file_path, $.);
155             process_formatting(\$body, $file_path, $.);
156             if (defined($deprecated)) {
157                 process_formatting(\$deprecated, $file_path, $.);
158             }
159             $body =~ s/\n/ /g;
160
161             if (exists($docs->{body}->{$name})) {
162                 my $origin = $docs->{origin}->{$name};
163                 if ($WARNINGS) {
164                     warn
165                       "$file_path:$.: Redundant documentation for $name\n",
166                       "$origin->{file}:$origin->{line}: Previously defined here\n";
167                 }
168             }
169             $docs->{origin}->{$name} = { file => $file_path, line => $. };
170             $docs->{body}->{$name} = $body;
171             $docs->{parameters}->{$name} = \@parameters;
172             $docs->{deprecated}->{$name} = $deprecated if defined $deprecated;
173             $docs->{return}->{$name} = $returns;
174             $docs->{prototype}->{$name} = $prototype;
175             last;
176
177         }
178
179         # Strip newlines and asterisk prefix.
180         chomp;
181         s/^ +\*//;
182
183         # Replace blank lines with paragraph breaks.
184         $_ = '<p>' if /^\s*$/;
185
186         if ($section == $PARAMETER_SECTION) {
187             if (/\s*\\param +(\w+)(.*)/) {
188                 # print "$file_path:$.: warning: Got parameter $1\n";
189                 push @parameters, { name => $1, description => $2 };
190             } elsif (/\s*\\deprecated(.*)/) {
191                 # print "$file_path:$.: warning: Got deprecated annotation\n";
192                 $deprecated = $1;
193             } elsif (/\s*(\w+):(.*)/) {
194                 if ($1 eq 'deprecated') {
195                     warn "$file_path:$.: Old-style monodoc notation 'deprecated:' used\n"
196                         if $WARNINGS;
197                     $deprecated = $2;
198                 } else {
199                     warn "$file_path:$.: Old-style monodoc notation 'param:' used\n"
200                         if $WARNINGS;
201                     push @parameters, { name => $1, description => $2 };
202                 }
203             } else {
204                 # $body = "\t$_\n";
205                 $section = $BODY_SECTION;
206                 redo;
207             }
208         } elsif ($section == $BODY_SECTION) {
209             if (s/(Returns?:\s*|\\returns?\s*)//) {
210                 $returns = "\t$_\n";
211                 $section = $RETURN_SECTION;
212             } else {
213                 $body .= "\n\t$_";
214             }
215         } elsif ($section == $RETURN_SECTION) {
216             $returns .= "\n\t$_";
217         } else {
218             die "Invalid section $section\n";
219         }
220     }
221 }
222
223 #
224 # Substitute formatting within documentation text.
225 #
226 sub process_formatting {
227     my ($content, $file_path, $current_line) = @_;
228     $_ = $$content;
229
230     # Constants
231     s{NULL}{<code>NULL</code>}g;
232     s{TRUE}{<code>TRUE</code>}g;
233     s{FALSE}{<code>FALSE</code>}g;
234
235     # Parameters
236     warn "$file_path:$current_line: Old-style monodoc notation '\@param' used\n"
237         if s{@(\w+)}{<i>$1</i>}g && $WARNINGS;
238     s{\\p +(\w+)}{<i>$1</i>}g;
239
240     # Code
241     warn "$file_path:$current_line: Old-style monodoc notation '#code' used\n"
242         if s{#(\w+)}{<code>$1</code>}g && $WARNINGS;
243     warn "$file_path:$current_line: Old-style monodoc notation '`code`' used\n"
244         if s{\`((?!api:)[:.\w\*]+)\`}{<code>$1</code>}g && $WARNINGS;
245     s{\\c +(\S+(?<![.,:;]))}{<code>$1</code>}g;
246
247     $$content = $_;
248 }
249
250 #
251 # Merge templates with stylesheet and documentation extracted from sources.
252 #
253 sub merge {
254     my ($docs, $templates, $stylesheet) = @_;
255     my $last = '';
256     for my $name (keys %$templates) {
257         open (my $output_file, '>', "$TARGET_DIR/html/$name")
258           or die "Could not create $TARGET_DIR/html/$name";
259         print "Merging: $name\n";
260         print $output_file <<EOF;
261 <?xml version="1.0" encoding="utf-8"?>
262 <html xmlns="http://www.w3.org/1999/xhtml">
263 <head>
264     <title>$name</title>
265     <style type="text/css">
266 $stylesheet
267    </style>
268 </head>
269 <body>
270 <div class="mapi-docs">
271 EOF
272         my @a = split (/\n/, $templates->{$name}->{contents});
273         my $strike = '';
274         my $strikeextra = '';
275         my $api_shown = 0;
276         for (my $ai = 0; $ai < $#a; $ai++) {
277             my $line = $a[$ai];
278             if (my ($api, $caption) = ($line =~ /<h4><a name=\"api:(\w+)\">(\w+)<\/a><\/h4>/)) {
279                 if ($api_shown == 1) {
280                     print $output_file "</div> <!-- class=mapi -->\n\n";
281                     if ($docs->{deprecated}->{$api}) {
282                         $strike = "mapi-strike";
283                         $strikeextra = "</div><br><div class='mapi-deprecated'><b>Deprecated:</b> " . $docs->{deprecated}->{$api};
284                     } else {
285                         $strike = "";
286                         $strikeextra = "";
287                     }
288                 }
289                 $api_shown = 1;
290                 my $proto = $docs->{prototype}->{$api} // $api;
291
292                 print $output_file <<EOF;
293 <a name="api:$api"></a>
294 <div class="mapi">
295     <div class="mapi-entry $strike"><code>$api$strikeextra</code></div>
296     <div class="mapi-height-container">
297         <div class="mapi-ptr-container"></div>
298         <div class="mapi-description">
299             <div class="mapi-ptr"></div>
300
301             <div class="mapi-declaration mapi-section">Syntax</div>
302             <div class="mapi-prototype">$proto</div>
303             <p>
304 EOF
305                 if (exists ($docs->{parameters}->{$api})) {
306                     my $ppars = $docs->{parameters}->{$api};
307                     if (@$ppars) {
308                         print $output_file
309                           "            <div class=\"mapi-section\">Parameters</div>\n",
310                           "            <table class=\"mapi-parameters\"><tbody>",
311                           render_parameters($ppars),
312                           "</tbody></table>";
313                     }
314                 }
315
316                 opt_print ($output_file, "Return value", $docs->{return}->{$api});
317                 opt_print ($output_file, "Description", $docs->{body}->{$api});
318                 print $output_file "        </div><!--mapi-description-->\n    </div><!--height container-->\n";
319             } else {
320                 if ($line =~ /\@API_IDX\@/) {
321                     my $apis_toc = create_toc ($docs, $templates->{$name}->{api});
322                     $line =~ s/\@API_IDX\@/$apis_toc/;
323                 }
324                 if ($line =~ /^<h4/) {
325                     print $output_file "</div>\n";
326                     $api_shown = 0;
327                 }
328                 if ($line =~ /`/) {
329                 }
330                 print $output_file "$line\n";
331             }
332         }
333         print $output_file
334           "   </div>",
335           "</body>",
336           "</html>";
337         close $output_file;
338         system ("$ENV{runtimedir}/mono-wrapper convert.exe $TARGET_DIR/html/$name $TARGET_DIR/html/x-$name");
339
340         # Clean up the mess that AgilityPack makes (it CDATAs our CSS).
341         open (my $hack_input, '<', "$TARGET_DIR/html/x-$name")
342           or die "Could not open $TARGET_DIR/html/x-$name";
343         open (my $hack_output, '>', "$TARGET_DIR/deploy/$name")
344           or die "Could not open output";
345
346         my $line = 0;
347         my $doprint = 0;
348         while (<$hack_input>) {
349             print $hack_output $last if ($doprint);
350             $line++;
351             s/^\/\/<!\[CDATA\[//;
352             s/^\/\/\]\]>\/\///;
353
354             # Remove the junk <span> wrapper generated by AgilityPack.
355             if ($line==1) {
356                 s/<span>//;
357             }
358             if (/<style type/) {
359                 # Replace the CSS in the XHTML output with the original CSS.
360                 print $hack_output $_;
361                 print $hack_output $$stylesheet;
362                 while (<$hack_input>) {
363                     last if (/<\/style>/);
364                 }
365             }
366             $last = $_;
367             $doprint = 1;
368         }
369         if (!($last =~ /span/)) {
370             print $hack_output $last;
371         }
372         # system ("cp.exe $TARGET_DIR/html/$name $TARGET_DIR/deploy/$name");
373     }
374 }
375
376 sub create_toc {
377     my ($docs, $apis_listed) = @_;
378     my $type_size = 0;
379     my $name_size = 0;
380     my ($ret, $xname, $args);
381     my $apis_toc = "";
382
383     # Try to align things; compute type size, method size, and arguments.
384     foreach my $line (split /\n/, $apis_listed) {
385         if (exists ($docs->{prototype}->{$line})) {
386             my $p = $docs->{prototype}->{$line};
387             if (my ($ret, $xname, $args) = ($p =~ /(.*)\n(\w+)[ \t](.*)/)) {
388                 my $tl = length ($ret);
389                 my $pl = length ($xname);
390                 $type_size = $tl if ($tl > $type_size);
391                 $name_size = $pl if ($pl > $name_size);
392             }
393         }
394     }
395
396     $type_size++;
397     $name_size++;
398
399     foreach my $line (split /\n/, $apis_listed) {
400         chomp($line);
401         if (exists($docs->{prototype}->{$line})) {
402             my $p = $docs->{prototype}->{$line};
403             if (my ($ret, $xname, $args) = ($p =~ /(.*)\n(\w+)[ \t](.*)/)) {
404                 $xname = $line if $xname eq "";
405                 my $rspace = " " x ($type_size - length ($ret));
406                 my $nspace = " " x ($name_size - length ($xname));
407                 $args = wrap ($args, length ($ret . $rspace . $xname . $nspace), 60);
408                 $apis_toc .= "$ret$rspace<a href=\"\#api:$line\">$xname</a>$nspace$args\n";
409             }
410         }
411     }
412     return $apis_toc;
413 }
414
415 sub wrap {
416     my ($args, $size, $limit) = @_;
417     my $sret = "";
418
419     # return $args if ((length (args) + size) < $limit);
420     
421     my $remain = $limit - $size;
422     my @sa = split /,/, $args;
423     my $linelen = $size;
424     foreach my $arg (@sa) {
425         if ($sret eq "") {
426             $sret = $arg . ", ";
427             $linelen += length ($sret);
428         } else {
429             if ($linelen + length ($arg) < $limit) {
430                 $sret .= "FITS" . $arg . ", ";
431             } else {
432                 my $newline = " " x ($size) . $arg . ", ";
433                 my $linelen = length ($newline);
434                 $sret .= "\n" . $newline;
435             }
436         }
437     }
438     $sret =~ s/, $/;/;
439     return $sret;
440 }
441
442 #
443 # Print a section if non-empty.
444 #
445 sub opt_print {
446     my ($output, $caption, $opttext) = @_;
447     if (defined($opttext) && $opttext ne '' && $opttext !~ /^[ \t]+$/) {
448         print $output
449           "             <div class=\"mapi-section\">$caption</div>\n",
450           "             <div>$opttext</div>\n";
451     }
452 }
453
454 #
455 # Render parameter information as table.
456 #
457 sub render_parameters {
458     my ($parameters) = @_;
459     my $result = '';
460     for my $parameter (@$parameters) {
461         $result .= "<tr><td><i>$parameter->{name}</i></td><td>$parameter->{description}</td></tr>";
462     }
463     return $result;
464 }
465
466 __END__
467
468 =head1 NAME
469
470 exdoc - Compiles API docs from Mono sources and HTML templates.
471
472 =head1 SYNOPSIS
473
474     exdoc [OPTIONS] [FILE...]
475
476 =head1 OPTIONS
477
478 =over 4
479
480 =item B<--help>
481
482 Print this help message.
483
484 =item B<--html> I<DIR>, B<-h> I<DIR>
485
486 Use I<DIR> as the input path for HTML sources.
487
488 =item B<--target> I<DIR>, B<-t> I<DIR>
489
490 Use I<DIR> as the target path for output.
491
492 =item B<--warnings>, B<-W>
493
494 Enable warnings about documentation errors.
495
496 =back
497
498 =head1 DESCRIPTION
499
500 Reads HTML templates and C sources, extracting documentation from the sources and splicing it into the templates.
501
502 =cut