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