Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,7 @@ paket-files/

# SSDT artifacts
Model.xml
*.FileListAbsolute.txt
*.FileListAbsolute.txt
PxtlCa.XmlCommentMarkdownGenerator.MSBuild.Test/Readme.md
PxtlCa.XmlCommentMarkdownGenerator.MSBuild.Test/Docs/PxtlCa.XmlCommentMarkDownGenerator.MSBuild_altered.md
PxtlCa.XmlCommentMarkdownGenerator.MSBuild.Test/Docs/PxtlCa.XmlCommentMarkDownGenerator.MSBuild.md
60 changes: 60 additions & 0 deletions PretzelPlugin/Plugin.csx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Pretzel.Logic.Templating;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file still being used? I thought the new workflow got rid of this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • No, not used.
  • Yes, it has been gotten rid of.
  • The delete didn't get committed apparently.

using Pretzel.Logic.Templating.Context;
using System.ComponentModel.Composition;
using System.Collections.Generic;
using System.Linq;


[SiteEngineInfo(Engine="customxmlmd")]
public class Processor : ISiteEngine
{

public string AllowableCustomTags {get; set;}
public void Initialize()
{

}

public bool CanProcess(SiteContext context)
{
var readmePage = context.Posts.Where(p => p.File.ToLower().EndsWith("readme.md")).FirstOrDefault();
if(null == readmePage)
{
//if no readme.md then return false
return false;
}
if(readmePage.Bag.ContainsKey("mergexmlcomments"))
{
Console.WriteLine("about to check 'mergexmlcomments' a bool");
if(!(bool)(readmePage.Bag["mergexmlcomments"]))
{
Console.WriteLine("as boolean 'mergexmlcomments' is false");
//if there is a mergexmlcomments value in the front matter
//but it is false
return false;
}
Console.WriteLine("as boolean 'mergexmlcomments' is true");
AllowableCustomTags = (string)(readmePage.Bag["allowedcustomtags"]);
return true;
}
else
{
Console.WriteLine("Bag doesn't contain 'mergexmlcomments'");
//no mergexmlcomments value
return false;
}
}

public void Process(SiteContext context, bool skipFileOnError = false)
{
Console.WriteLine($@"About to check CanProcess(context) again");
if(CanProcess(context))
{
Console.WriteLine($@"Custom Tag to allow: {AllowableCustomTags}");
}
else
{
Console.WriteLine($@"CanProcess(context) is false this time");
}
}
}
20 changes: 20 additions & 0 deletions PretzelPlugin/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, more Pretzel stuff here.

layout: post
title: "README"
author: "The end user"
comments: false
mergexmlcomments: true
allowedcustomtags: "all"
---

## Hello world...

```cs
static void Main()
{
Console.WriteLine("Hello World!");
}
```


This is my first post on the site!
273 changes: 273 additions & 0 deletions PxtlCa.XmlCommentMarkDownGenerator.MSBuild/GenerateMarkdown.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
using System;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General problem in this file: You have 3 config paths. InputXml (an array of paths to XML files), DocumentationPath (a path to a markdown file or folder) and OutputFile (a path to the output file to be created). This naming is very inconsistent - they're all paths (although the Input and Output ones are to a file while DocumentationPath is permitted to be a folder). Also, DocumentationPath wears two hats - it's both the folder the pre-merged documentation gets written to and the folder where the MD files get output to before they merged into the single giant MD document. Many projects have their readmes stored outside of the ./Docs folder, and so it would make sense to have those feed into the ./Docs folder output.

Imho, split the documentation path into two folders.

Clearer naming would also definitely help. My recommendations:

InputMdDocumentationPath,
OutputMdDocumentationPath,
MergedOutputFilePath,
InputXmlFilePaths.

I prefer clear verbosity to terse ambiguity.

Also, I'm looking at the behavior here: it looks like if the DocumentationPath is a file and merge is off, it will read from that file to get parameters, and then replace the body of that file with the generated documentation. That seems bad - really highlights the need to separate the input DocumentationPath and the output one.

using System.Collections.Generic;
using System.Linq;
using System.Text;
//using System.Threading.Tasks;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove commented code.

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.IO;
using System.Xml.Linq;
using System.Xml;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using PxtlCa.XmlCommentMarkDownGenerator.MSBuild.Options;

namespace PxtlCa.XmlCommentMarkDownGenerator.MSBuild
{
/// <summary>
/// A task that generates and optionally merges markdown
/// </summary>
public class GenerateMarkdown : Task
{
/// <summary>
/// The file(s) from which to generate markdown. This should be in XmlDocumentation format.
/// </summary>
[Required]
public ITaskItem[] InputXml { get; set; }

/// <summary>
/// DocumentationPath is the top level directory in which to search for files.
/// It is also the path where generated markdown files are created.
Copy link
Owner

@Pxtl Pxtl Nov 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment highlights the problem of how DocumentationPath wears two hats - it does not make sense that the header docs must be present in the same ./Docs folder they're going to get merged into.

/// </summary>
[Required]
public ITaskItem DocumentationPath { get; set; }

/// <summary>
/// Whether the generated markdown files should merge. Only valid if multiple markdown files exist.
/// DocumentationPath is the top level directory in which to search for files.
/// Both existing markdown files and the generated files are merged.
/// </summary>
public bool MergeFiles { get; set; }

/// <summary>
/// The file to be created by the merge. Unused if MergeFiles evaluates to false.
/// </summary>
public ITaskItem OutputFile { get; set; }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's so tightly tied to MergeFiles behavior, rename to MergedOutputFile. Doubly confusing because you also have an OutputPath property below that has a completely unrelated behavior.


/// <summary>
/// Defaults to false. When true unexpected tags in the documentation
/// will generate warnings rather than errors.
/// </summary>
public bool WarnOnUnexpectedTag { get; set; } = false;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inconsistent naming between the Enum, the Bool, and the Enum type itself is problematic but I'd rather just put this in and refactor that later.




/// <summary>
/// Runs the task as configured
/// </summary>
/// <returns>true if task has succeeded</returns>
public override bool Execute()
{
if (InputXml.Length == 0)
{
Log.LogError("InputXml cannot be empty");
}
else
{
UpdateParametersFromInput();
if (MergeFiles && (null == OutputFile))
{
Log.LogError("OutputFile must be specified if input files are merged");
}
else if (DocumentationPathIsFile && InputXml.Length != 1)
{
Log.LogError("DocumentationPath must specify a directory if more than one input XML value is supplied");
}
else
{
try
{
CreateDirectoryIfNeeded();
GenerateFiles();
if (MergeFiles)
{
Merge();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't there be a separate CreateDirectoryIfNeeded for outputfile because it's possible that the merged outputfile is outside of documentation path?

}
return true;
}
catch (Exception ex)
{
LoggedException = ex;
Log.LogErrorFromException(ex);
}
}
}
return false;
}

/// <summary>
/// This updates the tag and merge parameters based on the input front matter
/// </summary>
public void UpdateParametersFromInput()
{
if(DocumentationPathIsFile)
{
if (TryGetFrontMatter(DocumentationPath.ItemSpec, out string frontMatter, out bool isEmpty) &&
(!isEmpty))
{
ReadOptionsFromString(frontMatter);
return;
}
}
else
{
var mdFiles = Directory.EnumerateFiles(DocumentationPath.ItemSpec, "*.md", SearchOption.AllDirectories).ToList();
foreach (var mdFile in mdFiles)
{
if (TryGetFrontMatter(mdFile, out string frontMatter, out bool isEmpty) &&
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not fond of the fact that it does this in apparently undefined order isntead of looking for an .md file that matches the XML file by name first, or look fora file named readme.md first... but either way, it's good for now. This looks like an opportunity for future enhancement.

(!isEmpty))
{
ReadOptionsFromString(frontMatter);
return;
}
}
}
}

/// <summary>
/// Use this to handle front matter in markdown files
/// </summary>
/// <param name="filePath">the path to the file</param>
/// <param name="frontMatter">the front matter found</param>
/// <param name="isEmpty">whether the front matter found is trivial</param>
/// <returns>true if front matter indicator(s) are found</returns>
public static bool TryGetFrontMatter(string filePath, out string frontMatter, out bool isEmpty)
{
var lines = File.ReadLines(filePath);
var firstDashedLine = lines.FirstOrDefault() ?? string.Empty;
if (firstDashedLine.StartsWith("---"))
{
var followingLines = lines.Skip(1).TakeWhile(line => !line.StartsWith("---")).ToList();
if(followingLines.Count == 0)
{
frontMatter = firstDashedLine;
isEmpty = true;
return true;
}
else
{
followingLines.Insert(0, firstDashedLine);
frontMatter = String.Join(Environment.NewLine, followingLines);
isEmpty = false;
return true;
}
}
frontMatter = string.Empty;
isEmpty = true;
return false;
}

private void ReadOptionsFromString(string frontMatter)
{
var input = new StringReader(frontMatter);
var deserializer = new DeserializerBuilder()
.WithNamingConvention(new CamelCaseNamingConvention())
.Build();
var options = deserializer.Deserialize<YamlOptions>(input);
MergeFiles = options.MergeXmlComments;
if(Enum.TryParse<AllowedTagOptions>(options.AllowedCustomTags, true,
out AllowedTagOptions result))
{
//warn (rather than treat as error condition)
//if user indicates one of the two currently used options
WarnOnUnexpectedTag = result == AllowedTagOptions.All;
}
}

/// <summary>
/// for testing.
/// sets the exception for throw outside the catch
/// </summary>
public Exception LoggedException { get; set; }

private void Merge()
{
//get all md files in Documentation Path
//except those generated in this task
var otherMDFiles = Directory.EnumerateFiles(DocumentationPath.ItemSpec, "*.md", SearchOption.AllDirectories).ToList();
otherMDFiles = otherMDFiles.Except(GeneratedMDFiles).ToList();
var mergeInto = otherMDFiles.FirstOrDefault();
if (null == mergeInto)
{
mergeInto = GeneratedMDFiles.First();
File.Copy(mergeInto, OutputFile.ItemSpec, true);
foreach (var mdFile in GeneratedMDFiles.Skip(1))
{
File.AppendAllText(OutputFile.ItemSpec, Environment.NewLine);
File.AppendAllText(OutputFile.ItemSpec, File.ReadAllText(mdFile));
}
}
else
{
File.Copy(mergeInto, OutputFile.ItemSpec, true);
foreach (var mdFile in otherMDFiles.Skip(1))
{
File.AppendAllText(OutputFile.ItemSpec, Environment.NewLine);
File.AppendAllText(OutputFile.ItemSpec, File.ReadAllText(mdFile));
}
foreach (var mdFile in GeneratedMDFiles)
{
File.AppendAllText(OutputFile.ItemSpec, Environment.NewLine);
File.AppendAllText(OutputFile.ItemSpec, File.ReadAllText(mdFile));
}
}
}

private void GenerateFiles()
{
foreach (var inputFile in InputXml)
{
try
{
var mdOutput = OutputPath(inputFile.ItemSpec);
GeneratedMDFiles.Add(mdOutput);
var sr = new StreamReader(inputFile.ItemSpec);
using (var sw = new StreamWriter(mdOutput))
{
var xml = sr.ReadToEnd();
var doc = XDocument.Parse(xml);
var md = doc.Root.ToMarkDown();
sw.Write(md);
sw.Close();
}
}catch(XmlException xmlException)
{
if(WarnOnUnexpectedTag && null != xmlException.InnerException &&
xmlException.InnerException.GetType() == typeof(KeyNotFoundException))
{
Log.LogWarningFromException(xmlException);
continue;
}
throw;
}
}
}

private string OutputPath(string inputXml)
{
if (DocumentationPathIsFile)
{
return DocumentationPath.ItemSpec;
}
return $@"{DocumentationPath.ItemSpec}\{Path.GetFileNameWithoutExtension(inputXml)}.md";
}

private bool DocumentationPathIsFile
{
get { return File.Exists(DocumentationPath.ItemSpec); }
}

/// <summary>
/// The files generated during execution of the task
/// </summary>
public List<string> GeneratedMDFiles { get; private set; } = new List<string>();

private void CreateDirectoryIfNeeded()
{
if ((!DocumentationPathIsFile) && (!Directory.Exists(DocumentationPath.ItemSpec)))
{
Directory.CreateDirectory(DocumentationPath.ItemSpec);
}
}
}
}
Loading