-
Notifications
You must be signed in to change notification settings - Fork 16
Added MSBuild Task to generate and merge markdown #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
259013d
8fd2049
87a8c96
fba7a28
0735a7b
38088a8
1502d25
522b8dc
6a16420
175f38e
4c9024f
e6b7c83
5a3bf1e
2778b52
efcccd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| using Pretzel.Logic.Templating; | ||
| 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"); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| --- | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,273 @@ | ||
| using System; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, 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; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; } | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) && | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.