|
Nocco is a quick-and-dirty, literate-programming-style documentation
generator. It is a C# port of Docco,
which was written by Jeremy Ashkenas in
Coffescript and runs on node.js.
Nocco produces HTML that displays your comments alongside your code.
Comments are passed through
Markdown, and code is
highlighted using google-code-prettify
syntax highlighting. This page is the result of running Nocco against its
own source files.
Currently, to build Nocco, you'll have to have Visual Studio 2010. The project
depends on MarkdownSharp and you'll
have to install .NET MVC 3 to get the
System.Web.Razor assembly. The MarkdownSharp is a NuGet package that will be
installed automatically when you build the project.
To use Nocco, run it from the command-line:
nocco *.cs
...will generate linked HTML documentation for the named source files, saving
it into a docs folder.
The source for Nocco is available on GitHub,
and released under the MIT license.
If .NET doesn't run on your platform, or you'd prefer a more convenient
package, get Rocco, the Ruby port that's
available as a gem. If you're writing shell scripts, try
Shocco, a port for the POSIX shell.
Both are by Ryan Tomayko. If Python's more
your speed, take a look at Nick Fitzgerald's
Pycco.
|
|
|
Import namespaces to allow us to type shorter type names.
|
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Nocco.Resources;
namespace Nocco;
public class Nocco
{
private static string _executingDirectory;
private static List<string> _files;
|
Main Documentation Generation Functions
|
|
|
Generate the documentation for a source file by reading it in, splitting it
up into comment/code sections, highlighting them for the appropriate language,
and merging them into an HTML template.
|
private static void GenerateDocumentation(string source) {
var lines = File.ReadAllLines(source);
var sections = Parse(source, lines);
Highlight(sections);
GenerateHtml(source, sections);
}
|
|
Given a string of source code, parse out each comment and the code that
follows it, and create an individual Section for it.
|
private static List<Section> Parse(string source, string[] lines) {
var sections = new List<Section>();
var language = GetLanguage(source);
var hasCode = false;
var docsText = new StringBuilder();
var codeText = new StringBuilder();
Action<string, string> save = (docs, code) => sections.Add(new Section { DocsHtml = docs, CodeHtml = code });
Func<string, string> mapToMarkdown = docs => {
if (language.MarkdownMaps != null)
docs = language.MarkdownMaps.Aggregate(docs, (currentDocs, map) => Regex.Replace(currentDocs, map.Key, map.Value, RegexOptions.Multiline));
return docs;
};
foreach (var line in lines) {
if (language.CommentMatcher.IsMatch(line) && !language.CommentFilter.IsMatch(line)) {
if (hasCode) {
save(mapToMarkdown(docsText.ToString()), codeText.ToString());
hasCode = false;
docsText = new StringBuilder();
codeText = new StringBuilder();
}
docsText.AppendLine(language.CommentMatcher.Replace(line, ""));
}
else {
hasCode = true;
codeText.AppendLine(line);
}
}
save(mapToMarkdown(docsText.ToString()), codeText.ToString());
return sections;
}
|
|
Prepares a single chunk of code for HTML output and runs the text of its
corresponding comment through Markdown, using a C# implementation
called MarkdownSharp.
|
private static void Highlight(List<Section> sections) {
var markdown = new MarkdownSharp.Markdown();
foreach (var section in sections) {
section.DocsHtml = markdown.Transform(section.DocsHtml);
section.CodeHtml = section.CodeHtml;
}
}
|
|
Once all of the code is finished highlighting, we can generate the HTML file
and write out the documentation. Pass the completed sections into the template
found in Resources/Webpage.cshtml
|
private static async void GenerateHtml(string source, List<Section> sections)
{
int depth;
var destination = GetDestination(source, out depth);
string pathToRoot = string.Concat(Enumerable.Repeat(".." + Path.DirectorySeparatorChar, depth));
IServiceCollection services = new ServiceCollection();
services.AddLogging();
IServiceProvider serviceProvider = services.BuildServiceProvider();
ILoggerFactory loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
await using var htmlRenderer = new HtmlRenderer(serviceProvider, loggerFactory);
var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
Func<string, string> getSourcePath = s =>
Path.Combine(pathToRoot, Path.ChangeExtension(s.ToLower(), ".html").Substring(2)).Replace('\\', '/');
var dictionary = new Dictionary<string, object?>
{
{ "Title", Path.GetFileName(source) },
{ "PathToCss", Path.Combine(pathToRoot, "nocco.css").Replace('\\', '/') },
{ "PathToJs", Path.Combine(pathToRoot, "prettify.js").Replace('\\', '/') },
{ "GetSourcePath", getSourcePath },
{ "Sections", sections },
{ "Sources", _files },
};
var parameters = ParameterView.FromDictionary(dictionary);
var output = await htmlRenderer.RenderComponentAsync<Webpage>(parameters);
return output.ToHtmlString();
});
File.WriteAllText(destination, html);
}
|
|
A list of the languages that Nocco supports, mapping the file extension to
the symbol that indicates a comment. To add another language to Nocco's
repertoire, add it here.
You can also specify a list of regular expression patterns and replacements. This
translates things like
XML documentation comments into Markdown.
|
private static Dictionary<string, Language> Languages = new Dictionary<string, Language> {
{ ".sql", new Language {
Name = "sql",
Symbol = "--",
}},
{ ".js", new Language {
Name = "javascript",
Symbol = "//",
Ignores = new List<string> {
"min.js"
}
}},
{ ".cs", new Language {
Name = "csharp",
Symbol = "///?",
Ignores = new List<string> {
"Designer.cs"
},
MarkdownMaps = new Dictionary<string, string> {
{ @"<c>([^<]*)</c>", "`$1`" },
{ @"<param[^\>]*name=""([^""]*)""[^\>]*>([^<]*)</param>", "**argument** *$1*: $2" + Environment.NewLine },
{ @"<returns>([^<]*)</returns>", "**returns**: $1" + Environment.NewLine },
{ @"<see\s*cref=""([^""]*)""\s*/>", "see `$1`"},
{ @"(</?example>|</?summary>|</?remarks>)", "" },
}
}},
{ ".vb", new Language {
Name = "vb.net",
Symbol = "'+",
Ignores = new List<string> {
"Designer.vb"
},
MarkdownMaps = new Dictionary<string, string> {
{ @"<c>([^<]*)</c>", "`$1`" },
{ @"<param[^\>]*>([^<]*)</param>", "" },
{ @"<returns>([^<]*)</returns>", "" },
{ @"<see\s*cref=""([^""]*)""\s*/>", "see `$1`"},
{ @"(</?example>|</?summary>|</?remarks>)", "" },
}
}}
};
|
|
Get the current language we're documenting, based on the extension.
|
private static Language GetLanguage(string source) {
var extension = Path.GetExtension(source);
return Languages.ContainsKey(extension) ? Languages[extension] : null;
}
|
|
Compute the destination HTML path for an input source file path. If the source
is Example.cs, the HTML will be at docs/example.html
|
private static string GetDestination(string filepath, out int depth) {
var dirs = Path.GetDirectoryName(filepath).Substring(1).Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
depth = dirs.Length;
var dest = Path.Combine("docs", string.Join(Path.DirectorySeparatorChar.ToString(), dirs)).ToLower();
Directory.CreateDirectory(dest);
return Path.Combine("docs", Path.ChangeExtension(filepath, "html").ToLower());
}
|
|
Find all the files that match the pattern(s) passed in as arguments and
generate documentation for each one.
|
public static void Generate(string[] targets)
{
if (targets.Length > 0) {
Directory.CreateDirectory("docs");
_executingDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
File.Copy(Path.Combine(_executingDirectory, "Resources", "Nocco.css"), Path.Combine("docs", "nocco.css"), true);
File.Copy(Path.Combine(_executingDirectory, "Resources", "prettify.js"), Path.Combine("docs", "prettify.js"), true);
_files = new List<string>();
foreach (var target in targets) {
_files.AddRange(Directory.GetFiles(".", target, SearchOption.AllDirectories).Where(filename => {
var language = GetLanguage(Path.GetFileName(filename)) ;
if (language == null)
return false;
|
|
Check if the file extension should be ignored
|
if (language.Ignores != null && language.Ignores.Any(ignore => filename.EndsWith(ignore)))
return false;
|
|
Don't include certain directories
|
var foldersToExclude = new string[] { @"\docs", @"\bin", @"\obj" };
if (foldersToExclude.Any(folder => Path.GetDirectoryName(filename).Contains(folder)))
return false;
return true;
}));
}
Console.WriteLine(_files.Count + " file(s) found.");
foreach (var file in _files)
GenerateDocumentation(file);
}
}
}
|