diff --git a/src/DocNet/Docnet.csproj b/src/DocNet/Docnet.csproj index f083b6f..66d9900 100644 --- a/src/DocNet/Docnet.csproj +++ b/src/DocNet/Docnet.csproj @@ -33,6 +33,10 @@ 4 + + ..\..\packages\Handlebars.Net.1.6.4\lib\Handlebars.dll + True + ..\..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll True @@ -80,4 +84,4 @@ --> - + \ No newline at end of file diff --git a/src/DocNet/SimpleNavigationElement.cs b/src/DocNet/SimpleNavigationElement.cs index 53ba3fa..0a24ff0 100644 --- a/src/DocNet/SimpleNavigationElement.cs +++ b/src/DocNet/SimpleNavigationElement.cs @@ -67,7 +67,7 @@ public override void GenerateOutput(Config activeConfig, NavigatedPath activePat { this.MarkdownFromFile = File.ReadAllText(sourceFile); // Check if the content contains @@include tag - content = Utils.IncludeProcessor(this.MarkdownFromFile, Utils.MakeAbsolutePath(activeConfig.Source, activeConfig.IncludeFolder)); + content = Utils.PartialProcessor(this.MarkdownFromFile, Utils.MakeAbsolutePath(activeConfig.Source, activeConfig.IncludeFolder)); content = Utils.ConvertMarkdownToHtml(content, Path.GetDirectoryName(destinationFile), activeConfig.Destination, _relativeH2LinksOnPage); } else @@ -212,7 +212,7 @@ public override string TargetURL _targetURLForHTML = (this.Value ?? string.Empty); if(_targetURLForHTML.ToLowerInvariant().EndsWith(".md")) { - _targetURLForHTML = _targetURLForHTML.Substring(0, _targetURLForHTML.Length-3) + ".htm"; + _targetURLForHTML = _targetURLForHTML.Substring(0, _targetURLForHTML.Length-3) + ".html"; } _targetURLForHTML = _targetURLForHTML.Replace("\\", "/"); } diff --git a/src/DocNet/Utils.cs b/src/DocNet/Utils.cs index 23f9a94..1326ce4 100644 --- a/src/DocNet/Utils.cs +++ b/src/DocNet/Utils.cs @@ -20,6 +20,8 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. ////////////////////////////////////////////////////////////////////////////////////////////// +using HandlebarsDotNet; +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; @@ -30,209 +32,272 @@ namespace Docnet { - public static class Utils - { - #region Statics - /// - /// Regex expression used to parse @@include(filename.html) tag. - /// - private static Regex includeRegex = new Regex(@"@@include\((.*)\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - #endregion - - /// - /// Converts the markdown to HTML. - /// - /// The markdown string to convert. - /// The document path (without the document filename). - /// The site root. - /// The created anchor collector, for ToC sublinks for H2 headers. - /// - public static string ConvertMarkdownToHtml(string toConvert, string documentPath, string siteRoot, List> createdAnchorCollector) - { - var parser = new MarkdownDeep.Markdown - { - ExtraMode = true, - GitHubCodeBlocks = true, - AutoHeadingIDs = true, - NewWindowForExternalLinks = true, - DocNetMode = true, - DocumentLocation = documentPath, - DocumentRoot = siteRoot, - HtmlClassTitledImages = "figure", - }; - - var toReturn = parser.Transform(toConvert); - createdAnchorCollector.AddRange(parser.CreatedH2IdCollector); - return toReturn; - } - - - /// - /// Copies directories and files, eventually recursively. From MSDN. - /// - /// Name of the source dir. - /// Name of the dest dir. - /// if set to true it will recursively copy files/folders. - public static void DirectoryCopy(string sourceFolderName, string destinationFolderName, bool copySubFolders) - { - // Get the subdirectories for the specified directory. - DirectoryInfo sourceFolder = new DirectoryInfo(sourceFolderName); - if(!sourceFolder.Exists) - { - throw new DirectoryNotFoundException("Source directory does not exist or could not be found: " + sourceFolderName); - } - - DirectoryInfo[] sourceFoldersToCopy = sourceFolder.GetDirectories(); - // If the destination directory doesn't exist, create it. - if(!Directory.Exists(destinationFolderName)) - { - Directory.CreateDirectory(destinationFolderName); - } - - // Get the files in the directory and copy them to the new location. - foreach(FileInfo file in sourceFolder.GetFiles()) - { - file.CopyTo(Path.Combine(destinationFolderName, file.Name), true); - } - if(copySubFolders) - { - foreach(DirectoryInfo subFolder in sourceFoldersToCopy) - { - Utils.DirectoryCopy(subFolder.FullName, Path.Combine(destinationFolderName, subFolder.Name), copySubFolders); - } - } - } - - - - /// - /// Makes toMakeAbsolute an absolute path, if it's not already a rooted path. If it's not a rooted path it's assumed it's relative to rootPath and is combined with that. - /// - /// The root path. - /// To make absolute. - /// - public static string MakeAbsolutePath(string rootPath, string toMakeAbsolute) - { - if(string.IsNullOrWhiteSpace(toMakeAbsolute)) - { - return rootPath; - } - if(Path.IsPathRooted(toMakeAbsolute)) - { - return toMakeAbsolute; - } - var rawToReturn = Path.Combine(rootPath, toMakeAbsolute); - return Path.GetFullPath(rawToReturn).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - } - - - /// - /// Creates the folders in the path specified if they don't exist, recursively - /// - /// The full path. - public static void CreateFoldersIfRequired(string fullPath) - { - string folderToCheck = Path.GetDirectoryName(fullPath); - if(string.IsNullOrWhiteSpace(folderToCheck)) - { - // nothing to do, no folder to emit - return; - } - if(!Directory.Exists(folderToCheck)) - { - Directory.CreateDirectory(folderToCheck); - } - } - - - /// - /// Creates a relative path to get from fromPath to toPath. If one of them is empty, the emptystring is returned. If there's no common path, toPath is returned. - /// - /// From path. - /// To path. - /// - /// Only works with file paths, which is ok, as it's used to create the {{Path}} macro. - public static string MakeRelativePath(string fromPath, string toPath) - { - var fromPathToUse = fromPath; - if(string.IsNullOrEmpty(fromPathToUse)) - { - return string.Empty; - } - var toPathToUse = toPath; - if(string.IsNullOrEmpty(toPathToUse)) - { - return string.Empty; - } - if(fromPathToUse.Last() != Path.DirectorySeparatorChar) - { - fromPathToUse += Path.DirectorySeparatorChar; - } - if(toPathToUse.Last() != Path.DirectorySeparatorChar) - { - toPathToUse += Path.DirectorySeparatorChar; - } - - var fromUri = new Uri(Uri.UnescapeDataString(Path.GetFullPath(fromPathToUse))); - var toUri = new Uri(Uri.UnescapeDataString(Path.GetFullPath(toPathToUse))); - - if(fromUri.Scheme != toUri.Scheme) - { - // path can't be made relative. - return toPathToUse; - } - - var relativeUri = fromUri.MakeRelativeUri(toUri); - string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); - - if(toUri.Scheme.ToUpperInvariant() == "FILE") - { - relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); - } - - return relativePath; - } - - - /// - /// As but it also converts '\' to '/'. - /// - /// From path. - /// To path. - /// - /// Only works with file paths, which is ok, as it's used to create the {{Path}} macro. - public static string MakeRelativePathForUri(string fromPath, string toPath) - { - return Utils.MakeRelativePath(fromPath, toPath).Replace(@"\", @"/"); - } - - - /// - /// Process the input for @@include tags and embeds the included content - /// into the output. - /// - /// content to be scanned for include tags - /// Directory containing the include files (absolute folder) - /// String with @@include replaced with the actual content from the partial. - public static string IncludeProcessor(String content, string includeFolder) - { - Match m = includeRegex.Match(content); - while (m.Success) - { - if (m.Groups.Count > 1) - { - string tagToReplace = m.Groups[0].Value; - string fileName = m.Groups[1].Value; - fileName = fileName.Replace("\"", ""); - string filePath = Path.Combine(includeFolder, fileName); - if (File.Exists(filePath)) - { - content = content.Replace(tagToReplace, File.ReadAllText(filePath)); - } - } - m = m.NextMatch(); - } - return content; - } + public static class Utils + { + #region Statics + /// + /// Regex expression used to parse @@include(filename.html) tag. + /// + private static Regex includeRegex = new Regex(@"@@([^\(]+)\(([^ \)]+)\)|(```[\s\S]+?```)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + #endregion + + /// + /// Converts the markdown to HTML. + /// + /// The markdown string to convert. + /// The document path (without the document filename). + /// The site root. + /// The created anchor collector, for ToC sublinks for H2 headers. + /// + public static string ConvertMarkdownToHtml(string toConvert, string documentPath, string siteRoot, List> createdAnchorCollector) + { + var parser = new MarkdownDeep.Markdown + { + ExtraMode = true, + GitHubCodeBlocks = true, + AutoHeadingIDs = true, + NewWindowForExternalLinks = true, + DocNetMode = true, + DocumentLocation = documentPath, + DocumentRoot = siteRoot, + HtmlClassTitledImages = "figure", + }; + + var toReturn = parser.Transform(toConvert); + createdAnchorCollector.AddRange(parser.CreatedH2IdCollector); + return toReturn; + } + + + /// + /// Copies directories and files, eventually recursively. From MSDN. + /// + /// Name of the source dir. + /// Name of the dest dir. + /// if set to true it will recursively copy files/folders. + public static void DirectoryCopy(string sourceFolderName, string destinationFolderName, bool copySubFolders) + { + // Get the subdirectories for the specified directory. + DirectoryInfo sourceFolder = new DirectoryInfo(sourceFolderName); + if (!sourceFolder.Exists) + { + throw new DirectoryNotFoundException("Source directory does not exist or could not be found: " + sourceFolderName); + } + + DirectoryInfo[] sourceFoldersToCopy = sourceFolder.GetDirectories(); + // If the destination directory doesn't exist, create it. + if (!Directory.Exists(destinationFolderName)) + { + Directory.CreateDirectory(destinationFolderName); + } + + // Get the files in the directory and copy them to the new location. + foreach (FileInfo file in sourceFolder.GetFiles()) + { + file.CopyTo(Path.Combine(destinationFolderName, file.Name), true); + } + if (copySubFolders) + { + foreach (DirectoryInfo subFolder in sourceFoldersToCopy) + { + Utils.DirectoryCopy(subFolder.FullName, Path.Combine(destinationFolderName, subFolder.Name), copySubFolders); + } + } + } + + + + /// + /// Makes toMakeAbsolute an absolute path, if it's not already a rooted path. If it's not a rooted path it's assumed it's relative to rootPath and is combined with that. + /// + /// The root path. + /// To make absolute. + /// + public static string MakeAbsolutePath(string rootPath, string toMakeAbsolute) + { + if (string.IsNullOrWhiteSpace(toMakeAbsolute)) + { + return rootPath; + } + if (Path.IsPathRooted(toMakeAbsolute)) + { + return toMakeAbsolute; + } + var rawToReturn = Path.Combine(rootPath, toMakeAbsolute); + return Path.GetFullPath(rawToReturn).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + + + /// + /// Creates the folders in the path specified if they don't exist, recursively + /// + /// The full path. + public static void CreateFoldersIfRequired(string fullPath) + { + string folderToCheck = Path.GetDirectoryName(fullPath); + if (string.IsNullOrWhiteSpace(folderToCheck)) + { + // nothing to do, no folder to emit + return; + } + if (!Directory.Exists(folderToCheck)) + { + Directory.CreateDirectory(folderToCheck); + } + } + + + /// + /// Creates a relative path to get from fromPath to toPath. If one of them is empty, the emptystring is returned. If there's no common path, toPath is returned. + /// + /// From path. + /// To path. + /// + /// Only works with file paths, which is ok, as it's used to create the {{Path}} macro. + public static string MakeRelativePath(string fromPath, string toPath) + { + var fromPathToUse = fromPath; + if (string.IsNullOrEmpty(fromPathToUse)) + { + return string.Empty; + } + var toPathToUse = toPath; + if (string.IsNullOrEmpty(toPathToUse)) + { + return string.Empty; + } + if (fromPathToUse.Last() != Path.DirectorySeparatorChar) + { + fromPathToUse += Path.DirectorySeparatorChar; + } + if (toPathToUse.Last() != Path.DirectorySeparatorChar) + { + toPathToUse += Path.DirectorySeparatorChar; + } + + var fromUri = new Uri(Uri.UnescapeDataString(Path.GetFullPath(fromPathToUse))); + var toUri = new Uri(Uri.UnescapeDataString(Path.GetFullPath(toPathToUse))); + + if (fromUri.Scheme != toUri.Scheme) + { + // path can't be made relative. + return toPathToUse; + } + + var relativeUri = fromUri.MakeRelativeUri(toUri); + string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); + + if (toUri.Scheme.ToUpperInvariant() == "FILE") + { + relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + } + + return relativePath; + } + + + /// + /// As but it also converts '\' to '/'. + /// + /// From path. + /// To path. + /// + /// Only works with file paths, which is ok, as it's used to create the {{Path}} macro. + public static string MakeRelativePathForUri(string fromPath, string toPath) + { + return Utils.MakeRelativePath(fromPath, toPath).Replace(@"\", @"/"); + } + + /// + /// Process the function the begins with @@ tag. + /// + /// The string that contains the @@ tag + /// The folder containing the partials and actual data files + /// String with @@ tag replaced with the actual content from the function. + public static string PartialProcessor(string content, string includeFolder) + { + return includeRegex.Replace(content, + new MatchEvaluator(match => PartialSelector(includeFolder, match)), + content.Split(' ').Length); + } + + /// + /// Selects which function to execute (e.g. include, render, etc.). This is the string right after @@ + /// + /// The folder containing the partials and actual data files + /// The Regex that matched the @@ tag + /// String with @@ tag replaced with the actual content from the function. + public static string PartialSelector(string includeFolder, Match match) + { + if (match.Groups.Count >= 2 && match.Groups[0].Value.StartsWith("@@")) + { + var partialName = match.Groups[1].Value.ToLowerInvariant(); + var functionParams = match.Groups[2].Value; + + switch (partialName) + { + case "include": return IncludeProcessor(functionParams, includeFolder); + case "render": return RenderProcessor(functionParams, includeFolder); + default: return "Error: Couldn't find function named " + partialName; + } + } + + // Return the original string if we don't have an include match + return match.Groups[0].Value; + } + + /// + /// Gets the contents of the file in the specified folder + /// + /// Name of the file + /// Folder to search in + /// File contents as a string + public static string GetFileContents(string fileName, string includesFolder) + { + string filePath = Path.Combine(includesFolder, fileName.Replace("\"", "")); + if (File.Exists(filePath)) + return File.ReadAllText(filePath); + else + return "Error: File not found - " + filePath; + } + + /// + /// Process the input for @@include tags and embeds the included content + /// into the output. + /// + /// content to be scanned for include tags + /// Directory containing the include files (absolute folder) + /// String with @@include replaced with the actual content from the partial. + public static string IncludeProcessor(String content, string includeFolder) + { + return GetFileContents(content, includeFolder); + } + + /// + /// Process the input for @@render tags and executes the specified partial (parameter number 1) + /// using Handlebars against the given data/json file (parameter number 2) + /// + /// Comma separated parameters of the @@render function + /// Directory containing the include files (absolute folder) + /// String with @@render replaced with the actual content from the partial. + public static string RenderProcessor(string content, string includeFolder) + { + var parameters = content.Trim().Split(','); + if (parameters.Length < 2) return "Error: expected at least 2 parameters"; + + var partialContents = GetFileContents(parameters[0], includeFolder); + var dataContents = GetFileContents(parameters[1], includeFolder); + + try + { + var dataJson = JsonConvert.DeserializeObject(dataContents); + var hbTemplate = Handlebars.Compile(partialContents); + return hbTemplate(dataJson); + } + catch (Exception e) + { + return $"Error: Couldn't deserialize content of file {parameters[1]}: {e.Message}"; + } + } } } diff --git a/src/DocNet/packages.config b/src/DocNet/packages.config index d18d9b1..dfb35cd 100644 --- a/src/DocNet/packages.config +++ b/src/DocNet/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file