diff --git a/scripts/man/.gitignore b/scripts/man/.gitignore index 2c2a370..a893283 100644 --- a/scripts/man/.gitignore +++ b/scripts/man/.gitignore @@ -1,2 +1,3 @@ *.1 +*.md /gen_man diff --git a/scripts/man/gen_man.d b/scripts/man/gen_man.d index aff9118..e789ebe 100755 --- a/scripts/man/gen_man.d +++ b/scripts/man/gen_man.d @@ -3,30 +3,11 @@ dependency "dub" path="../.." +/ -import std.algorithm, std.conv, std.format, std.path, std.range, std.stdio; +import std.algorithm, std.conv, std.format, std.path, std.range; +import std.stdio : File; import dub.commandline; -string italic(string w) -{ - return `\fI` ~ w ~ `\fR`; -} - -string bold(string w) -{ - return `\fB` ~ w ~ `\fR`; -} - -string header(string heading) -{ - return ".SH " ~ heading; -} - -string br(string s) -{ - return ".BR " ~ s; -} - -struct Config +static struct Config { import std.datetime; SysTime date; @@ -46,165 +27,443 @@ string cwd; } -void writeHeader(ref File manFile, string manName, const Config config) +struct ManWriter { - static immutable manHeader = + enum Mode + { + man, markdown + } + + File output; + Mode mode; + + string escapeWord(string s) + { + final switch (mode) { + case Mode.man: return s.replace(`\`, `\\`).replace(`-`, `\-`).replace(`.`, `\&.`); + case Mode.markdown: return s.replace(`<`, `<`).replace(`>`, `>`); + } + } + + string escapeFulltext(string s) + { + final switch (mode) { + case Mode.man: return s; + case Mode.markdown: return s.replace(`<`, `<`).replace(`>`, `>`); + } + } + + string italic(string w) + { + final switch (mode) { + case Mode.man: return `\fI` ~ w ~ `\fR`; + case Mode.markdown: return `` ~ w ~ ``; + } + } + + string bold(string w) + { + final switch (mode) { + case Mode.man: return `\fB` ~ w ~ `\fR`; + case Mode.markdown: return `` ~ w ~ ``; + } + } + + string header(string heading) + { + final switch (mode) { + case Mode.man: return ".SH " ~ heading; + case Mode.markdown: return "## " ~ heading; + } + } + + string subheader(string heading) + { + final switch (mode) { + case Mode.man: return ".SS " ~ heading; + case Mode.markdown: return "### " ~ heading; + } + } + + string url(string urlAndText) + { + return url(urlAndText, urlAndText); + } + + string url(string url, string text) + { + final switch (mode) { + case Mode.man: return ".UR" ~ url ~ "\n" ~ text ~ "\n.UE"; + case Mode.markdown: return format!"[%s](%s)"(text, url); + } + } + + string autolink(string s) + { + final switch (mode) { + case Mode.man: return s; + case Mode.markdown: + auto sanitized = s + .replace("", "") + .replace("", "") + .replace("", "") + .replace("", "") + .replace("*", ""); + if (sanitized.startsWith("dub") && sanitized.endsWith("(1)")) { + sanitized = sanitized[0 .. $ - 3]; + return url(sanitized ~ ".md", s); + } + return s; + } + } + + /// Links subcommands in the main dub.md file (converts the subcommand name + /// like `init` into a link to `dub-init.md`) + string specialLinkMainCmd(string s) + { + final switch (mode) { + case Mode.man: return s; + case Mode.markdown: return url("dub-" ~ s ~ ".md", s); + } + } + + void write(T...)(T args) + { + output.write(args); + } + + void writeln(T...)(T args) + { + output.writeln(args); + } + + void writefln(T...)(T args) + { + output.writefln(args); + } + + void writeHeader(string manName, const Config config) + { + import std.uni : toLower; + + final switch (mode) + { + case Mode.man: + static immutable manHeader = `.TH %s 1 "%s" "The D Language Foundation" "The D Language Foundation" .SH NAME`; - manFile.writefln(manHeader, manName, config.date.toISOExtString.take(10)); -} + writefln(manHeader, manName, config.date.toISOExtString.take(10)); + break; + case Mode.markdown: + writefln("# %s(1)", manName.toLower); + break; + } + } -void writeFooter(ref File manFile, string seeAlso, const Config config) -{ - static immutable manFooter = -`.SH FILES -\fIdub\&.sdl\fR, \fIdub\&.json\fR -.SH AUTHOR -Copyright (c) 1999-%s by The D Language Foundation -.SH "ONLINE DOCUMENTATION" -.UR http://code.dlang.org/docs/commandline -http://code.dlang.org/docs/commandline -.UE -.SH "SEE ALSO" -%s`; - manFile.writefln(manFooter, config.date.year, seeAlso); + void writeFooter(string seeAlso, const Config config) + { + const manFooter = + header("FILES") ~ '\n' + ~ italic(escapeWord("dub.sdl")) ~ ", " ~ italic(escapeWord("dub.json")) ~ '\n' + ~ header("AUTHOR") ~ '\n' + ~ `Copyright (c) 1999-%s by The D Language Foundation` ~ '\n' + ~ header("ONLINE DOCUMENTATION") ~ '\n' + ~ url(`http://code.dlang.org/docs/commandline`) ~ '\n' + ~ header("SEE ALSO"); + writefln(manFooter, config.date.year); + writeln(seeAlso); + } + + string highlightArguments(string args) + { + import std.regex : regex, replaceAll; + static auto re = regex("<([^>]*)>"); + const reReplacement = escapeWord("<%s>").format(italic(escapeWord(`$1`))); + auto ret = args.replaceAll(re, reReplacement); + if (ret.length) ret ~= ' '; + return ret; + } + + void beginArgs(string cmd) + { + if (mode == Mode.markdown) + writeln("\n
\n"); + } + + void endArgs() + { + if (mode == Mode.markdown) + writeln("\n
\n"); + } + + void writeArgName(string cmd, string name) + { + import std.regex : regex, replaceAll; + final switch ( mode ) + { + case Mode.man: + writeln(".PP"); + writeln(name); + break; + case Mode.markdown: + string nameEscape = name.replaceAll(regex("[^a-zA-Z0-9_-]+"), "-"); + writeln(); + writefln(`
`, cmd, nameEscape); + writefln(``, cmd, nameEscape); + writeln(); + writeln(name); + writeln(); + writeln(`
`); + writeln(); + break; + } + } + + void beginArgDescription() + { + final switch ( mode ) + { + case Mode.man: + writeln(".RS 4"); + break; + case Mode.markdown: + writeln(); + writefln(`
`); + writeln(); + break; + } + } + + void endArgDescription() + { + final switch ( mode ) + { + case Mode.man: + writeln(".RE"); + break; + case Mode.markdown: + writeln(); + writefln(`
`); + writeln(); + break; + } + } + + void writeArgs(string cmdName, CommandArgs args) + { + beginArgs(cmdName); + foreach (arg; args.recognizedArgs) + { + auto names = arg.names.split("|"); + assert(names.length == 1 || names.length == 2); + string sarg = names[0].length == 1 ? names[0] : null; + string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null; + string name; + if (sarg !is null) { + name ~= bold(escapeWord("-%s".format(sarg))); + if (larg !is null) + name ~= ", "; + } + if (larg !is null) { + name ~= bold(escapeWord("--%s".format(larg))); + if (!arg.defaultValue.peek!bool) + name ~= escapeWord("=") ~ italic("VALUE"); + } + writeArgName(cmdName, name); + beginArgDescription(); + writeln(arg.helpText.join(mode == Mode.man ? "\n" : "\n\n")); + endArgDescription(); + } + endArgs(); + } + + void writeDefinition(string key, string definition) + { + final switch (mode) + { + case Mode.man: + writeln(".TP"); + writeln(bold(key)); + writeln(definition); + break; + case Mode.markdown: + writeln(`
`); + writeln(); + writeln(bold(key)); + writeln(); + writeln("
"); + writeln(`
`); + writeln(); + writeln(definition); + writeln(); + writeln("
"); + break; + } + } + + void beginDefinitionList() + { + final switch (mode) + { + case Mode.man: + break; + case Mode.markdown: + writeln(); + writeln(`
`); + writeln(); + break; + } + } + + void endDefinitionList() + { + final switch (mode) + { + case Mode.man: + break; + case Mode.markdown: + writeln("\n
\n"); + break; + } + } + + void writeDefaultExitCodes() + { + string[2][] exitCodes = [ + ["0", "DUB succeeded"], + ["1", "usage errors, unknown command line flags"], + ["2", "package not found, package failed to load, miscellaneous error"] + ]; + + final switch (mode) + { + case Mode.man: + foreach (cm; exitCodes) { + writeln(".TP"); + writeln(".BR ", cm[0]); + writeln(cm[1]); + } + break; + case Mode.markdown: + beginDefinitionList(); + foreach (cm; exitCodes) { + writeDefinition(cm[0], cm[1]); + } + endDefinitionList(); + break; + } + } } void writeMainManFile(CommandArgs args, CommandGroup[] commands, - string fileName, const Config config) + string fileName, const Config config) { - auto manFile = File(config.cwd.buildPath(fileName), "w"); + auto manFile = ManWriter( + File(config.cwd.buildPath(fileName), "w"), + fileName.endsWith(".md") ? ManWriter.Mode.markdown : ManWriter.Mode.man + ); manFile.writeHeader("DUB", config); - auto seeAlso = ["dmd(1)", "rdmd(1)"] - .chain(commands.map!(a => a.commands).joiner - .map!(cmd => format("dub-%s(1)", cmd.name))) - .joiner(", ").to!string.bold; + auto seeAlso = [ + manFile.autolink(manFile.bold("dmd") ~ "(1)"), + manFile.autolink(manFile.bold("rdmd") ~ "(1)") + ] + .chain(commands + .map!(a => a.commands) + .joiner + .map!(cmd => manFile.autolink(manFile.bold("dub-" ~ cmd.name) ~ "(1)"))) + .joiner(", ") + .to!string; scope(exit) manFile.writeFooter(seeAlso, config); alias writeln = (m) => manFile.writeln(m); writeln(`dub \- Package and build management system for D`); - writeln("SYNOPSIS".header); - writeln(`.B dub -[\-\-version] -[\fICOMMAND\fR] -[\fIOPTIONS\&.\&.\&.\fR] -[\-\- [\fIAPPLICATION ARGUMENTS\&.\&.\&.\fR]]`); + writeln(manFile.header("SYNOPSIS")); + writeln(manFile.bold("dub") ~ text( + " [", + manFile.escapeWord("--version"), + "] [", + manFile.italic("COMMAND"), + "] [", + manFile.italic(manFile.escapeWord("OPTIONS...")), + "] ", manFile.escapeWord("--"), " [", + manFile.italic(manFile.escapeWord("APPLICATION ARGUMENTS...")), + "]" + )); - writeln("DESCRIPTION".header); - writeln(`Manages the DUB project in the current directory\&. DUB can serve as a build + writeln(manFile.header("DESCRIPTION")); + writeln(`Manages the DUB project in the current directory. DUB can serve as a build system and a package manager, automatically keeping track of project's dependencies \- both downloading them and linking them into the application.`); - writeln(".SH COMMANDS"); + writeln(manFile.header("COMMANDS")); + manFile.beginDefinitionList(); foreach (grp; commands) { foreach (cmd; grp.commands) { - writeln(".TP"); - writeln(cmd.name.bold); - writeln(cmd.helpText.joiner("\n")); + manFile.writeDefinition(manFile.specialLinkMainCmd(cmd.name), cmd.helpText.join( + manFile.mode == ManWriter.Mode.markdown ? "\n\n" : "\n" + )); } - } + } + + - writeln("COMMON OPTIONS".header); - args.writeArgs(manFile); + writeln(manFile.header("COMMON OPTIONS")); + manFile.writeArgs("-", args); } -string highlightArguments(string args) -{ - import std.regex : regex, replaceAll; - static auto re = regex("<([^>]*)>"); - static const reReplacement = "<%s>".format(`$1`.italic); - return args.replaceAll(re, reReplacement); -} - -void writeArgs(CommandArgs args, ref File manFile) -{ - alias write = (m) => manFile.write(m.replace(`-`, `\-`)); - foreach (arg; args.recognizedArgs) - { - auto names = arg.names.split("|"); - assert(names.length == 1 || names.length == 2); - string sarg = names[0].length == 1 ? names[0] : null; - string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null; - manFile.writeln(".PP"); - if (sarg !is null) { - write("-%s".format(sarg).bold); - if (larg !is null) - write(", "); - } - if (larg !is null) { - write("--%s".format(larg).bold); - if (!arg.defaultValue.peek!bool) - write("=VALUE"); - } - manFile.writeln; - manFile.writeln(".RS 4"); - manFile.writeln(arg.helpText.join("\n")); - manFile.writeln(".RE"); - } -} - -void writeManFile(Command command, const Config config) +void writeManFile(Command command, const Config config, ManWriter.Mode mode) { import std.uni : toUpper; auto args = new CommandArgs(null); command.prepare(args); - string fileName = format("dub-%s.1", command.name); - auto manFile = File(config.cwd.buildPath(fileName), "w"); + string fileName = format(mode == ManWriter.Mode.markdown ? "dub-%s.md" : "dub-%s.1", command.name); + auto manFile = ManWriter(File(config.cwd.buildPath(fileName), "w"), mode); auto manName = format("DUB-%s", command.name).toUpper; manFile.writeHeader(manName, config); string[] extraRelated; foreach (arg; args.recognizedArgs) { if (arg.names.canFind("rdmd")) - extraRelated ~= "rdmd(1)"; + extraRelated ~= manFile.autolink(manFile.bold("rdmd") ~ "(1)"); } if (command.name == "dustmite") - extraRelated ~= "dustmite(1)"; + extraRelated ~= manFile.autolink(manFile.bold("dustmite") ~ "(1)"); - const seeAlso = ["dub(1)"] - .chain(config.relatedSubCommands.map!(s => s.format!"dub-%s(1)")) + const seeAlso = [manFile.autolink(manFile.bold("dub") ~ "(1)")] + .chain(config.relatedSubCommands.map!(s => manFile.autolink(manFile.bold("dub-" ~ s) ~ "(1)"))) .chain(extraRelated) - .map!bold .joiner(", ") .to!string; scope(exit) manFile.writeFooter(seeAlso, config); alias writeln = (m) => manFile.writeln(m); - manFile.writefln(`dub-%s \- %s`, command.name, command.description); - writeln("SYNOPSIS".header); - writeln("dub %s".format(command.name).bold); - writeln(command.argumentsPattern.highlightArguments); - writeln(`OPTIONS\&.\&.\&.`.italic); + manFile.writefln(`dub-%s \- %s`, command.name, manFile.escapeFulltext(command.description)); + + writeln(manFile.header("SYNOPSIS")); + manFile.write(manFile.bold("dub %s ".format(command.name))); + manFile.write(manFile.highlightArguments(command.argumentsPattern)); + writeln(manFile.italic(manFile.escapeWord(`OPTIONS...`))); if (command.acceptsAppArgs) { - writeln("[-- <%s>]".format("application arguments...".italic)); + writeln("[-- <%s>]".format(manFile.italic(manFile.escapeWord("application arguments...")))); } - writeln("DESCRIPTION".header); - writeln(command.helpText.joiner("\n\n")); - writeln("OPTIONS".header); - args.writeArgs(manFile); + writeln(manFile.header("DESCRIPTION")); + writeln(manFile.escapeFulltext(command.helpText.join("\n\n"))); + writeln(manFile.header("OPTIONS")); + manFile.writeArgs(command.name, args); - static immutable exitStatus = -`.SH EXIT STATUS -.TP -.BR 0 -DUB succeeded -.TP -.BR 1 -usage errors, unknown command line flags -.TP -.BR 2 -package not found, package failed to load, miscellaneous error`; - static immutable exitStatusDustmite = -`.SH EXIT STATUS -Forwards the exit code from ` ~ `dustmite(1)`.bold; - if (command.name == "dustmite") - manFile.writeln(exitStatusDustmite); - else - manFile.writeln(exitStatus); + writeln(manFile.subheader("COMMON OPTIONS")); + manFile.writeln("See ", manFile.autolink(manFile.bold("dub") ~ "(1)")); + + manFile.writeln(manFile.header("EXIT STATUS")); + if (command.name == "dustmite") { + manFile.writeln("Forwards the exit code from " ~ manFile.autolink(manFile.bold(`dustmite`) ~ `(1)`)); + } else { + manFile.writeDefaultExitCodes(); + } } void main() @@ -218,12 +477,13 @@ auto args = new CommandArgs(null); options.prepare(args); args.writeMainManFile(commands, "dub.1", config); + args.writeMainManFile(commands, "dub.md", config); } string[][] relatedSubCommands = [ ["run", "build", "test"], ["test", "dustmite", "lint"], - ["describe", "gemerate"], + ["describe", "generate"], ["add", "fetch"], ["init", "add", "convert"], ["add-path", "remove-path"], @@ -244,6 +504,7 @@ related = related.remove!(c => c == cmd.name); config.relatedSubCommands = related; - cmd.writeManFile(config); + cmd.writeManFile(config, ManWriter.Mode.man); + cmd.writeManFile(config, ManWriter.Mode.markdown); } }