xref: /third_party/curl/scripts/cd2nroff (revision 13498266)
1#!/usr/bin/env perl
2#***************************************************************************
3#                                  _   _ ____  _
4#  Project                     ___| | | |  _ \| |
5#                             / __| | | | |_) | |
6#                            | (__| |_| |  _ <| |___
7#                             \___|\___/|_| \_\_____|
8#
9# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
10#
11# This software is licensed as described in the file COPYING, which
12# you should have received as part of this distribution. The terms
13# are also available at https://curl.se/docs/copyright.html.
14#
15# You may opt to use, copy, modify, merge, publish, distribute and/or sell
16# copies of the Software, and permit persons to whom the Software is
17# furnished to do so, under the terms of the COPYING file.
18#
19# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
20# KIND, either express or implied.
21#
22# SPDX-License-Identifier: curl
23#
24###########################################################################
25
26=begin comment
27
28Converts a curldown file to nroff (man page).
29
30=end comment
31=cut
32
33use strict;
34use warnings;
35
36my $cd2nroff = "0.1"; # to keep check
37my $dir;
38my $extension;
39my $keepfilename;
40
41while(@ARGV) {
42    if($ARGV[0] eq "-d") {
43        shift @ARGV;
44        $dir = shift @ARGV;
45    }
46    elsif($ARGV[0] eq "-e") {
47        shift @ARGV;
48        $extension = shift @ARGV;
49    }
50    elsif($ARGV[0] eq "-k") {
51        shift @ARGV;
52        $keepfilename = 1;
53    }
54    elsif($ARGV[0] eq "-h") {
55        print <<HELP
56Usage: cd2nroff [options] [file.md]
57
58-d <dir> Write the output to the file name from the meta-data in the
59         specified directory, instead of writing to stdout
60-e <ext> If -d is used, this option can provide an added "extension", arbitrary
61         text really, to append to the file name.
62-h       This help text,
63-v       Show version then exit
64HELP
65            ;
66        exit 0;
67    }
68    elsif($ARGV[0] eq "-v") {
69        print "cd2nroff version $cd2nroff\n";
70        exit 0;
71    }
72    else {
73        last;
74    }
75}
76
77use POSIX qw(strftime);
78my @ts;
79if (defined($ENV{SOURCE_DATE_EPOCH})) {
80    @ts = localtime($ENV{SOURCE_DATE_EPOCH});
81} else {
82    @ts = localtime;
83}
84my $date = strftime "%B %d %Y", @ts;
85
86sub outseealso {
87    my (@sa) = @_;
88    my $comma = 0;
89    my @o;
90    push @o, ".SH SEE ALSO\n";
91    for my $s (sort @sa) {
92        push @o, sprintf "%s.BR $s", $comma ? ",\n": "";
93        $comma = 1;
94    }
95    push @o, "\n";
96    return @o;
97}
98
99sub single {
100    my @seealso;
101    my $d;
102    my ($f)=@_;
103    my $copyright;
104    my $errors = 0;
105    my $fh;
106    my $line;
107    my $salist;
108    my $section;
109    my $source;
110    my $spdx;
111    my $start = 0;
112    my $title;
113
114    if(defined($f)) {
115        if(!open($fh, "<:crlf", "$f")) {
116            print STDERR "Failed to open $f : $!\n";
117            return 1;
118        }
119    }
120    else {
121        $f = "STDIN";
122        $fh = \*STDIN;
123        binmode($fh, ":crlf");
124    }
125    while(<$fh>) {
126        $line++;
127        if(!$start) {
128            if(/^---/) {
129                # header starts here
130                $start = 1;
131            }
132            next;
133        }
134        if(/^Title: *(.*)/i) {
135            $title=$1;
136        }
137        elsif(/^Section: *(.*)/i) {
138            $section=$1;
139        }
140        elsif(/^Source: *(.*)/i) {
141            $source=$1;
142        }
143        elsif(/^See-also: +(.*)/i) {
144            $salist = 0;
145            push @seealso, $1;
146        }
147        elsif(/^See-also: */i) {
148            if($seealso[0]) {
149                print STDERR "$f:$line:1:ERROR: bad See-Also, needs list\n";
150                return 2;
151            }
152            $salist = 1;
153        }
154        elsif(/^ +- (.*)/i) {
155            # the only list we support is the see-also
156            if($salist) {
157                push @seealso, $1;
158            }
159        }
160        # REUSE-IgnoreStart
161        elsif(/^C: (.*)/i) {
162            $copyright=$1;
163        }
164        elsif(/^SPDX-License-Identifier: (.*)/i) {
165            $spdx=$1;
166        }
167        # REUSE-IgnoreEnd
168        elsif(/^---/) {
169            # end of the header section
170            if(!$title) {
171                print STDERR "ERROR: no 'Title:' in $f\n";
172                return 1;
173            }
174            if(!$section) {
175                print STDERR "ERROR: no 'Section:' in $f\n";
176                return 2;
177            }
178            if(!$seealso[0]) {
179                print STDERR "$f:$line:1:ERROR: no 'See-also:' present\n";
180                return 2;
181            }
182            if(!$copyright) {
183                print STDERR "$f:$line:1:ERROR: no 'C:' field present\n";
184                return 2;
185            }
186            if(!$spdx) {
187                print STDERR "$f:$line:1:ERROR: no 'SPDX-License-Identifier:' field present\n";
188                return 2;
189            }
190            last;
191        }
192        else {
193            chomp;
194            print STDERR "WARN: unrecognized line in $f, ignoring:\n:'$_';"
195        }
196    }
197
198    if(!$start) {
199        print STDERR "$f:$line:1:ERROR: no header present\n";
200        return 2;
201    }
202
203    my @desc;
204    my $quote = 0;
205    my $blankline = 0;
206    my $header = 0;
207
208    # cut off the leading path from the file name, if any
209    $f =~ s/^(.*[\\\/])//;
210
211    push @desc, ".\\\" generated by cd2nroff $cd2nroff from $f\n";
212    push @desc, ".TH $title $section \"$date\" $source\n";
213    while(<$fh>) {
214        $line++;
215
216        $d = $_;
217
218        if($quote) {
219            if($quote == 4) {
220                # remove the indentation
221                if($d =~ /^    (.*)/) {
222                    push @desc, "$1\n";
223                    next;
224                }
225                else {
226                    # end of quote
227                    $quote = 0;
228                    push @desc, ".fi\n";
229                    next;
230                }
231            }
232            if(/^~~~/) {
233                # end of quote
234                $quote = 0;
235                push @desc, ".fi\n";
236                next;
237            }
238            # convert single backslahes to doubles
239            $d =~ s/\\/\\\\/g;
240            # lines starting with a period needs it escaped
241            $d =~ s/^\./\\&./;
242            push @desc, $d;
243            next;
244        }
245
246        # **bold**
247        $d =~ s/\*\*(\S.*?)\*\*/\\fB$1\\fP/g;
248        # *italics*
249        $d =~ s/\*(\S.*?)\*/\\fI$1\\fP/g;
250
251        # mentions of curl symbols with man pages use italics by default
252        $d =~ s/((lib|)curl([^ ]*\(3\)))/\\fI$1\\fP/gi;
253
254        # backticked becomes italics
255        $d =~ s/\`(.*?)\`/\\fI$1\\fP/g;
256
257        if(/^## (.*)/) {
258            my $word = $1;
259            # if there are enclosing quotes, remove them first
260            $word =~ s/[\"\'](.*)[\"\']\z/$1/;
261
262            # enclose in double quotes if there is a space present
263            if($word =~ / /) {
264                push @desc, ".IP \"$word\"\n";
265            }
266            else {
267                push @desc, ".IP $word\n";
268            }
269            $header = 1;
270        }
271        elsif(/^# (.*)/) {
272            my $word = $1;
273            # if there are enclosing quotes, remove them first
274            $word =~ s/[\"\'](.*)[\"\']\z/$1/;
275            push @desc, ".SH $word\n";
276            $header = 1;
277        }
278        elsif(/^~~~c/) {
279            # start of a code section, not indented
280            $quote = 1;
281            push @desc, "\n" if($blankline && !$header);
282            $header = 0;
283            push @desc, ".nf\n";
284        }
285        elsif(/^~~~/) {
286            # start of a quote section; not code, not indented
287            $quote = 1;
288            push @desc, "\n" if($blankline && !$header);
289            $header = 0;
290            push @desc, ".nf\n";
291        }
292        elsif(/^    (.*)/) {
293            # quoted, indented by 4 space
294            $quote = 4;
295            push @desc, "\n" if($blankline && !$header);
296            $header = 0;
297            push @desc, ".nf\n$1\n";
298        }
299        elsif(/^[ \t]*\n/) {
300            # count and ignore blank lines
301            $blankline++;
302        }
303        else {
304            # don't output newlines if this is the first content after a
305            # header
306            push @desc, "\n" if($blankline && !$header);
307            $blankline = 0;
308            $header = 0;
309
310            # remove single line HTML comments
311            $d =~ s/<!--.*?-->//g;
312
313            # quote minuses in the output
314            $d =~ s/([^\\])-/$1\\-/g;
315            # replace single quotes
316            $d =~ s/\'/\\(aq/g;
317            # handle double quotes first on the line
318            $d =~ s/^(\s*)\"/$1\\&\"/;
319
320            # lines starting with a period needs it escaped
321            $d =~ s/^\./\\&./;
322
323            if($d =~ /^(.*)  /) {
324                printf STDERR "$f:$line:%d:ERROR: 2 spaces detected\n",
325                    length($1);
326                $errors++;
327            }
328            if($d =~ /^[ \t]*\n/) {
329                # replaced away all contents
330                $blankline= 1;
331            }
332            else {
333                push @desc, $d;
334            }
335        }
336    }
337    if($fh != \*STDIN) {
338        close($fh);
339    }
340    push @desc, outseealso(@seealso);
341    if($dir) {
342        if($keepfilename) {
343            $title = $f;
344            $title =~ s/\.[^.]*$//;
345        }
346        my $outfile = "$dir/$title.$section";
347        if(defined($extension)) {
348            $outfile .= $extension;
349        }
350        if(!open(O, ">", $outfile)) {
351            print STDERR "Failed to open $outfile : $!\n";
352            return 1;
353        }
354        print O @desc;
355        close(O);
356    }
357    else {
358        print @desc;
359    }
360    return $errors;
361}
362
363if(@ARGV) {
364    for my $f (@ARGV) {
365        my $r = single($f);
366        if($r) {
367            exit $r;
368        }
369    }
370}
371else {
372    exit single();
373}
374