diff --git a/src/DocNet/Config.cs b/src/DocNet/Config.cs index 55b15d6..0b639b9 100644 --- a/src/DocNet/Config.cs +++ b/src/DocNet/Config.cs @@ -72,10 +72,11 @@ internal bool Load(string configFile) /// Generates the search data, which is the json file called 'search_index.json' with search data of all pages as well as the docnet_search.htm file in the output. /// The search index is written to the root of the output folder. /// - internal void GenerateSearchData() + /// The navigation context. + internal void GenerateSearchData(NavigationContext navigationContext) { - GenerateSearchPage(); - GenerateSearchDataIndex(); + GenerateSearchPage(navigationContext); + GenerateSearchDataIndex(navigationContext); } internal void CopyThemeToDestination() @@ -128,10 +129,10 @@ internal void ClearDestinationFolder() /// /// Generates the index of the search data. this is a json file with per page which has markdown a couple of data elements. /// - private void GenerateSearchDataIndex() + private void GenerateSearchDataIndex(NavigationContext navigationContext) { var collectedSearchEntries = new List(); - this.Pages.CollectSearchIndexEntries(collectedSearchEntries, new NavigatedPath(), this.PathSpecification); + this.Pages.CollectSearchIndexEntries(collectedSearchEntries, new NavigatedPath(), navigationContext); JObject searchIndex = new JObject(new JProperty("docs", new JArray( collectedSearchEntries.Select(e=>new JObject( @@ -145,7 +146,7 @@ private void GenerateSearchDataIndex() } - private void GenerateSearchPage() + private void GenerateSearchPage(NavigationContext navigationContext) { var activePath = new NavigatedPath(); activePath.Push(this.Pages); @@ -164,7 +165,7 @@ private void GenerateSearchPage() searchSimpleElement.ExtraScriptProducerFunc = e=> @" "; - searchSimpleElement.GenerateOutput(this, activePath, this.PathSpecification); + searchSimpleElement.GenerateOutput(this, activePath, navigationContext); activePath.Pop(); } @@ -216,6 +217,14 @@ public bool ConvertLocalLinks } } + public int MaxLevelInToC + { + get + { + return _configData.MaxLevelInToC ?? 2; + } + } + public string ThemeName { get diff --git a/src/DocNet/Docnet.csproj b/src/DocNet/Docnet.csproj index 99b3653..917c8f0 100644 --- a/src/DocNet/Docnet.csproj +++ b/src/DocNet/Docnet.csproj @@ -58,6 +58,7 @@ + diff --git a/src/DocNet/Engine.cs b/src/DocNet/Engine.cs index 5078bd1..0857ab7 100644 --- a/src/DocNet/Engine.cs +++ b/src/DocNet/Engine.cs @@ -44,7 +44,14 @@ public int DoWork() { return 1; } - GeneratePages(); + + var navigationContext = new NavigationContext + { + MaxLevel = _loadedConfig.MaxLevelInToC, + PathSpecification = _loadedConfig.PathSpecification + }; + + GeneratePages(navigationContext); return 0; } @@ -86,7 +93,7 @@ public Config LoadConfig() /// Generates the pages from the md files in the source, using the page template loaded and the loaded config. /// /// true if everything went ok, false otherwise - private void GeneratePages() + private void GeneratePages(NavigationContext navigationContext) { if(_input.ClearDestinationFolder) { @@ -98,9 +105,9 @@ private void GeneratePages() Console.WriteLine("Copying source folders to copy."); _loadedConfig.CopySourceFoldersToCopy(); Console.WriteLine("Generating pages in '{0}'", _loadedConfig.Destination); - _loadedConfig.Pages.GenerateOutput(_loadedConfig, new NavigatedPath(), _loadedConfig.PathSpecification); + _loadedConfig.Pages.GenerateOutput(_loadedConfig, new NavigatedPath(), navigationContext); Console.WriteLine("Generating search index"); - _loadedConfig.GenerateSearchData(); + _loadedConfig.GenerateSearchData(navigationContext); Console.WriteLine("Done!"); } } diff --git a/src/DocNet/INavigationElement.cs b/src/DocNet/INavigationElement.cs index 6a524be..c36c64b 100644 --- a/src/DocNet/INavigationElement.cs +++ b/src/DocNet/INavigationElement.cs @@ -35,24 +35,24 @@ public interface INavigationElement /// /// The active configuration to use for the output. /// The active path navigated through the ToC to reach this element. - /// The path specification. - void GenerateOutput(Config activeConfig, NavigatedPath activePath, PathSpecification pathSpecification); + /// The navigation context. + void GenerateOutput(Config activeConfig, NavigatedPath activePath, NavigationContext navigationContext); /// /// Generates the ToC fragment for this element, which can either be a simple line or a full expanded menu. /// /// The navigated path to the current element, which doesn't necessarily have to be this element. /// The relative path back to the URL root, e.g. ../.., so it can be used for links to elements in this path. - /// The path specification. + /// The navigation context. /// - string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, PathSpecification pathSpecification); + string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, NavigationContext navigationContext); /// /// Collects the search index entries. These are created from simple navigation elements found in this container, which aren't index element. /// /// The collected entries. /// The active path currently navigated. - /// The path specification. - void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, PathSpecification pathSpecification); + /// The navigation context. + void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, NavigationContext navigationContext); /// /// Gets the target URL with respect to the . diff --git a/src/DocNet/NavigatedPath.cs b/src/DocNet/NavigatedPath.cs index c93f21c..920ac65 100644 --- a/src/DocNet/NavigatedPath.cs +++ b/src/DocNet/NavigatedPath.cs @@ -77,9 +77,9 @@ public string CreateBreadCrumbsText(string relativePathToRoot) /// aren't, are not expanded. /// /// The relative path back to the URL root, e.g. ../.., so it can be used for links to elements in this path. - /// The path specification. + /// The navigation context. /// - public string CreateToCHTML(string relativePathToRoot, PathSpecification pathSpecification) + public string CreateToCHTML(string relativePathToRoot, NavigationContext navigationContext) { // the root container is the bottom element of this path. We use that container to build the root and navigate any node open along the navigated path. var rootContainer = this.Reverse().FirstOrDefault() as NavigationLevel; @@ -88,7 +88,7 @@ public string CreateToCHTML(string relativePathToRoot, PathSpecification pathSpe // no root container, no TOC return string.Empty; } - return rootContainer.GenerateToCFragment(this, relativePathToRoot, pathSpecification); + return rootContainer.GenerateToCFragment(this, relativePathToRoot, navigationContext); } } } diff --git a/src/DocNet/NavigationContext.cs b/src/DocNet/NavigationContext.cs new file mode 100644 index 0000000..271c081 --- /dev/null +++ b/src/DocNet/NavigationContext.cs @@ -0,0 +1,21 @@ +namespace Docnet +{ + public class NavigationContext + { + public NavigationContext() + { + MaxLevel = 2; + } + + public NavigationContext(PathSpecification pathSpecification, int maxLevel) + : this() + { + PathSpecification = pathSpecification; + MaxLevel = maxLevel; + } + + public int MaxLevel { get; set; } + + public PathSpecification PathSpecification { get; set; } + } +} \ No newline at end of file diff --git a/src/DocNet/NavigationElement.cs b/src/DocNet/NavigationElement.cs index 11f210c..8638772 100644 --- a/src/DocNet/NavigationElement.cs +++ b/src/DocNet/NavigationElement.cs @@ -36,23 +36,23 @@ public abstract class NavigationElement : INavigationElement /// /// The active configuration to use for the output. /// The active path navigated through the ToC to reach this element. - /// The path specification. - public abstract void GenerateOutput(Config activeConfig, NavigatedPath activePath, PathSpecification pathSpecification); + /// The navigation context. + public abstract void GenerateOutput(Config activeConfig, NavigatedPath activePath, NavigationContext navigationContext); /// /// Generates the ToC fragment for this element, which can either be a simple line or a full expanded menu. /// /// The navigated path to the current element, which doesn't necessarily have to be this element. /// The relative path back to the URL root, e.g. ../.., so it can be used for links to elements in this path. - /// The path specification. + /// The navigation context. /// - public abstract string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, PathSpecification pathSpecification); + public abstract string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, NavigationContext navigationContext); /// /// Collects the search index entries. These are created from simple navigation elements found in this container, which aren't index element. /// /// The collected entries. /// The active path currently navigated. - /// The path specification. - public abstract void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, PathSpecification pathSpecification); + /// The navigation context. + public abstract void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, NavigationContext navigationContext); /// /// Gets the target URL with respect to the . diff --git a/src/DocNet/NavigationLevel.cs b/src/DocNet/NavigationLevel.cs index ba1d781..b81b4b8 100644 --- a/src/DocNet/NavigationLevel.cs +++ b/src/DocNet/NavigationLevel.cs @@ -106,13 +106,13 @@ public void Load(JObject dataFromFile) /// /// The collected entries. /// The active path currently navigated. - /// The path specification. - public override void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, PathSpecification pathSpecification) + /// The navigation context. + public override void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, NavigationContext navigationContext) { activePath.Push(this); foreach (var element in this.Value) { - element.CollectSearchIndexEntries(collectedEntries, activePath, pathSpecification); + element.CollectSearchIndexEntries(collectedEntries, activePath, navigationContext); } activePath.Pop(); } @@ -123,15 +123,15 @@ public override void CollectSearchIndexEntries(List collectedE /// /// The active configuration to use for the output. /// The active path navigated through the ToC to reach this element. - /// The path specification. - public override void GenerateOutput(Config activeConfig, NavigatedPath activePath, PathSpecification pathSpecification) + /// The navigation context. + public override void GenerateOutput(Config activeConfig, NavigatedPath activePath, NavigationContext navigationContext) { activePath.Push(this); int i = 0; while (i < this.Value.Count) { var element = this.Value[i]; - element.GenerateOutput(activeConfig, activePath, pathSpecification); + element.GenerateOutput(activeConfig, activePath, navigationContext); i++; } activePath.Pop(); @@ -143,9 +143,9 @@ public override void GenerateOutput(Config activeConfig, NavigatedPath activePat /// /// The navigated path to the current element, which doesn't necessarily have to be this element. /// The relative path back to the URL root, e.g. ../.., so it can be used for links to elements in this path. - /// The path specification. + /// The navigation context. /// - public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, PathSpecification pathSpecification) + public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, NavigationContext navigationContext) { var fragments = new List(); if (!this.IsRoot) @@ -167,7 +167,7 @@ public override string GenerateToCFragment(NavigatedPath navigatedPath, string r // first render the level header, which is the index element, if present or a label. The root always has an __index element otherwise we'd have stopped at load. var elementStartTag = "
  • "; - var indexElement = this.GetIndexElement(pathSpecification); + var indexElement = this.GetIndexElement(navigationContext.PathSpecification); if (indexElement == null) { fragments.Add(string.Format("{0}{1}
  • ", elementStartTag, this.Name)); @@ -176,18 +176,18 @@ public override string GenerateToCFragment(NavigatedPath navigatedPath, string r { if (this.IsRoot) { - fragments.Add(indexElement.PerformGenerateToCFragment(navigatedPath, relativePathToRoot, pathSpecification)); + fragments.Add(indexElement.PerformGenerateToCFragment(navigatedPath, relativePathToRoot, navigationContext)); } else { fragments.Add(string.Format("{0}{3}", - elementStartTag, relativePathToRoot, indexElement.GetFinalTargetUrl(pathSpecification), this.Name)); + elementStartTag, relativePathToRoot, indexElement.GetFinalTargetUrl(navigationContext.PathSpecification), this.Name)); } } // then the elements in the container. Index elements are skipped here. foreach (var element in this.Value) { - fragments.Add(element.GenerateToCFragment(navigatedPath, relativePathToRoot, pathSpecification)); + fragments.Add(element.GenerateToCFragment(navigatedPath, relativePathToRoot, navigationContext)); } fragments.Add(""); } @@ -195,7 +195,7 @@ public override string GenerateToCFragment(NavigatedPath navigatedPath, string r { // just a link fragments.Add(string.Format(" {2}", - relativePathToRoot, this.GetFinalTargetUrl(pathSpecification), this.Name)); + relativePathToRoot, this.GetFinalTargetUrl(navigationContext.PathSpecification), this.Name)); } if (!this.IsRoot) { diff --git a/src/DocNet/SimpleNavigationElement.cs b/src/DocNet/SimpleNavigationElement.cs index db9df9b..53e6105 100644 --- a/src/DocNet/SimpleNavigationElement.cs +++ b/src/DocNet/SimpleNavigationElement.cs @@ -27,6 +27,7 @@ using System.Text; using System.Threading.Tasks; using System.Web; +using MarkdownDeep; namespace Docnet { @@ -34,13 +35,13 @@ public class SimpleNavigationElement : NavigationElement { #region Members private string _targetURLForHTML; - private List> _relativeH2LinksOnPage; // first element in Tuple is anchor name, second is name for ToC. + private readonly List _relativeLinksOnPage; #endregion public SimpleNavigationElement() { - _relativeH2LinksOnPage = new List>(); + _relativeLinksOnPage = new List(); } @@ -49,56 +50,57 @@ public SimpleNavigationElement() /// /// The active configuration to use for the output. /// The active path navigated through the ToC to reach this element. - /// The path specification. + /// The navigation context. + /// /// - public override void GenerateOutput(Config activeConfig, NavigatedPath activePath, PathSpecification pathSpecification) + public override void GenerateOutput(Config activeConfig, NavigatedPath activePath, NavigationContext navigationContext) { // if we're the __index element, we're not pushing ourselves on the path, as we're representing the container we're in, which is already on the path. - if(!this.IsIndexElement) + if (!this.IsIndexElement) { activePath.Push(this); } - _relativeH2LinksOnPage.Clear(); + _relativeLinksOnPage.Clear(); var sourceFile = Utils.MakeAbsolutePath(activeConfig.Source, this.Value); - var destinationFile = Utils.MakeAbsolutePath(activeConfig.Destination, this.GetTargetURL(pathSpecification)); + var destinationFile = Utils.MakeAbsolutePath(activeConfig.Destination, this.GetTargetURL(navigationContext.PathSpecification)); var sb = new StringBuilder(activeConfig.PageTemplateContents.Length + 2048); var content = string.Empty; this.MarkdownFromFile = string.Empty; var relativePathToRoot = Utils.MakeRelativePathForUri(Path.GetDirectoryName(destinationFile), activeConfig.Destination); - if(File.Exists(sourceFile)) + if (File.Exists(sourceFile)) { this.MarkdownFromFile = File.ReadAllText(sourceFile, Encoding.UTF8); // Check if the content contains @@include tag content = Utils.IncludeProcessor(this.MarkdownFromFile, Utils.MakeAbsolutePath(activeConfig.Source, activeConfig.IncludeFolder)); - content = Utils.ConvertMarkdownToHtml(content, Path.GetDirectoryName(destinationFile), activeConfig.Destination, sourceFile, _relativeH2LinksOnPage, activeConfig.ConvertLocalLinks); + content = Utils.ConvertMarkdownToHtml(content, Path.GetDirectoryName(destinationFile), activeConfig.Destination, sourceFile, _relativeLinksOnPage, activeConfig.ConvertLocalLinks); } else { // if we're not the index element, the file is missing and potentially it's an error in the config page. // Otherwise we can simply assume we are a missing index page and we'll generate default markdown so the user has something to look at. - if(this.IsIndexElement) + if (this.IsIndexElement) { // replace with default markdown snippet. This is the name of our container and links to the elements in that container as we are the index page that's not // specified / existend. var defaultMarkdown = new StringBuilder(); defaultMarkdown.AppendFormat("# {0}{1}{1}", this.ParentContainer.Name, Environment.NewLine); defaultMarkdown.AppendFormat("Please select one of the topics in this section:{0}{0}", Environment.NewLine); - foreach(var sibling in this.ParentContainer.Value) + foreach (var sibling in this.ParentContainer.Value) { - if(sibling == this) + if (sibling == this) { continue; } - defaultMarkdown.AppendFormat("* [{0}]({1}{2}){3}", sibling.Name, relativePathToRoot, - sibling.GetFinalTargetUrl(pathSpecification), Environment.NewLine); + defaultMarkdown.AppendFormat("* [{0}]({1}{2}){3}", sibling.Name, relativePathToRoot, + sibling.GetFinalTargetUrl(navigationContext.PathSpecification), Environment.NewLine); } defaultMarkdown.Append(Environment.NewLine); - content = Utils.ConvertMarkdownToHtml(defaultMarkdown.ToString(), Path.GetDirectoryName(destinationFile), activeConfig.Destination, string.Empty, _relativeH2LinksOnPage, activeConfig.ConvertLocalLinks); + content = Utils.ConvertMarkdownToHtml(defaultMarkdown.ToString(), Path.GetDirectoryName(destinationFile), activeConfig.Destination, string.Empty, _relativeLinksOnPage, activeConfig.ConvertLocalLinks); } else { // target not found. See if there's a content producer func to produce html for us. If not, we can only conclude an error in the config file. - if(this.ContentProducerFunc == null) + if (this.ContentProducerFunc == null) { throw new FileNotFoundException(string.Format("The specified markdown file '{0}' couldn't be found. Aborting", sourceFile)); } @@ -110,17 +112,17 @@ public override void GenerateOutput(Config activeConfig, NavigatedPath activePat sb.Replace("{{Footer}}", activeConfig.Footer); sb.Replace("{{TopicTitle}}", this.Name); sb.Replace("{{Path}}", relativePathToRoot); - sb.Replace("{{RelativeSourceFileName}}", Utils.MakeRelativePathForUri(activeConfig.Destination, sourceFile).TrimEnd('/')); - sb.Replace("{{RelativeTargetFileName}}", Utils.MakeRelativePathForUri(activeConfig.Destination, destinationFile).TrimEnd('/')); - sb.Replace("{{Breadcrumbs}}", activePath.CreateBreadCrumbsHTML(relativePathToRoot, pathSpecification)); - sb.Replace("{{ToC}}", activePath.CreateToCHTML(relativePathToRoot, pathSpecification)); + sb.Replace("{{RelativeSourceFileName}}", Utils.MakeRelativePathForUri(activeConfig.Destination, sourceFile).TrimEnd('/')); + sb.Replace("{{RelativeTargetFileName}}", Utils.MakeRelativePathForUri(activeConfig.Destination, destinationFile).TrimEnd('/')); + sb.Replace("{{Breadcrumbs}}", activePath.CreateBreadCrumbsHTML(relativePathToRoot, navigationContext.PathSpecification)); + sb.Replace("{{ToC}}", activePath.CreateToCHTML(relativePathToRoot, navigationContext)); sb.Replace("{{ExtraScript}}", (this.ExtraScriptProducerFunc == null) ? string.Empty : this.ExtraScriptProducerFunc(this)); // the last action has to be replacing the content marker, so markers in the content which we have in the template as well aren't replaced sb.Replace("{{Content}}", content); Utils.CreateFoldersIfRequired(destinationFile); File.WriteAllText(destinationFile, sb.ToString()); - if(!this.IsIndexElement) + if (!this.IsIndexElement) { activePath.Pop(); } @@ -132,15 +134,15 @@ public override void GenerateOutput(Config activeConfig, NavigatedPath activePat /// /// The collected entries. /// The active path currently navigated. - /// The path specification. - public override void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, PathSpecification pathSpecification) + /// The navigation context. + public override void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, NavigationContext navigationContext) { activePath.Push(this); // simply convert ourselves into an entry if we're not an index - if(!this.IsIndexElement) + if (!this.IsIndexElement) { var toAdd = new SearchIndexEntry(); - toAdd.Fill(this.MarkdownFromFile, this.GetTargetURL(pathSpecification), this.Name, activePath); + toAdd.Fill(this.MarkdownFromFile, this.GetTargetURL(navigationContext.PathSpecification), this.Name, activePath); collectedEntries.Add(toAdd); } activePath.Pop(); @@ -152,17 +154,17 @@ public override void CollectSearchIndexEntries(List collectedE /// /// The navigated path to the current element, which doesn't necessarily have to be this element. /// The relative path back to the URL root, e.g. ../.., so it can be used for links to elements in this path. - /// The path specification. + /// The navigation context. /// - public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, PathSpecification pathSpecification) + public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, NavigationContext navigationContext) { // index elements are rendered in the parent container. - if(this.IsIndexElement) + if (this.IsIndexElement) { return string.Empty; } - return PerformGenerateToCFragment(navigatedPath, relativePathToRoot, pathSpecification); + return PerformGenerateToCFragment(navigatedPath, relativePathToRoot, navigationContext); } @@ -172,16 +174,16 @@ public override string GenerateToCFragment(NavigatedPath navigatedPath, string r /// /// The navigated path. /// The relative path to root. - /// The path specification. + /// The navigation context. /// - public string PerformGenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, PathSpecification pathSpecification) + public string PerformGenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, NavigationContext navigationContext) { // we can't navigate deeper from here. If we are the element being navigated to, we are the current and will have to emit any additional relative URLs too. bool isCurrent = navigatedPath.Contains(this); var fragments = new List(); var liClass = "tocentry"; var aClass = string.Empty; - if(isCurrent) + if (isCurrent) { liClass = "tocentry current"; aClass = "current"; @@ -190,16 +192,22 @@ public string PerformGenerateToCFragment(NavigatedPath navigatedPath, string rel string.IsNullOrWhiteSpace(liClass) ? string.Empty : string.Format(" class=\"{0}\"", liClass), string.IsNullOrWhiteSpace(aClass) ? string.Empty : string.Format(" class=\"{0}\"", aClass), relativePathToRoot, - this.GetFinalTargetUrl(pathSpecification), + this.GetFinalTargetUrl(navigationContext.PathSpecification), this.Name)); - if(isCurrent && _relativeH2LinksOnPage.Any()) + if (isCurrent && _relativeLinksOnPage.SelectMany(x => x.Children).Any(x => x.Level > 1)) { // generate relative links fragments.Add(string.Format("
      ", this.ParentContainer.IsRoot ? "currentrelativeroot" : "currentrelative")); - foreach(var p in _relativeH2LinksOnPage) + + foreach (var heading in _relativeLinksOnPage) { - fragments.Add(string.Format("
    • {1}
    • ", p.Item1, p.Item2)); + var content = GenerateToCFragmentForHeading(heading, navigationContext); + if (!string.IsNullOrWhiteSpace(content)) + { + fragments.Add(content); + } } + fragments.Add("
    "); } else @@ -243,6 +251,48 @@ public override string GetTargetURL(PathSpecification pathSpecification) return _targetURLForHTML; } + private string GenerateToCFragmentForHeading(Heading heading, NavigationContext navigationContext) + { + var stringBuilder = new StringBuilder(); + + // Skip heading 1 and larger than allowed + var isHeading1 = heading.Level <= 1; + if (!isHeading1 && heading.Level <= navigationContext.MaxLevel) + { + stringBuilder.AppendLine(string.Format("
  • {1}
  • ", heading.Id, heading.Name)); + } + + var childContentBuilder = new StringBuilder(); + + foreach (var child in heading.Children) + { + var childContent = GenerateToCFragmentForHeading(child, navigationContext); + if (!string.IsNullOrWhiteSpace(childContent)) + { + childContentBuilder.AppendLine(childContent); + } + } + + if (childContentBuilder.Length > 0) + { + if (!isHeading1) + { + stringBuilder.AppendLine("
  • "); + stringBuilder.AppendLine(string.Format("
      ", this.ParentContainer.IsRoot ? "currentrelativeroot" : "currentrelative")); + } + + stringBuilder.AppendLine(childContentBuilder.ToString()); + + if (!isHeading1) + { + stringBuilder.AppendLine("
    "); + stringBuilder.AppendLine("
  • "); + } + } + + return stringBuilder.ToString(); + } + #region Properties /// /// Gets / sets a value indicating whether this element is the __index element diff --git a/src/DocNet/Utils.cs b/src/DocNet/Utils.cs index d5becc3..1aefc4e 100644 --- a/src/DocNet/Utils.cs +++ b/src/DocNet/Utils.cs @@ -27,6 +27,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using MarkdownDeep; namespace Docnet { @@ -50,7 +51,7 @@ public static class Utils /// if set to true, convert local links to md files to target files. /// public static string ConvertMarkdownToHtml(string toConvert, string destinationDocumentPath, string siteRoot, string sourceDocumentFilename, - List> createdAnchorCollector, bool convertLocalLinks) + List createdAnchorCollector, bool convertLocalLinks) { var parser = new MarkdownDeep.Markdown { @@ -67,7 +68,9 @@ public static string ConvertMarkdownToHtml(string toConvert, string destinationD }; var toReturn = parser.Transform(toConvert); - createdAnchorCollector.AddRange(parser.CreatedH2IdCollector); + + createdAnchorCollector.AddRange(parser.Headings.ConvertToHierarchy()); + return toReturn; } diff --git a/src/MarkdownDeep/Block.cs b/src/MarkdownDeep/Block.cs index 4df2802..08fb719 100644 --- a/src/MarkdownDeep/Block.cs +++ b/src/MarkdownDeep/Block.cs @@ -193,14 +193,19 @@ internal void Render(Markdown m, StringBuilder b) { b.Append("<" + BlockType.ToString() + ">"); } - if(m.DocNetMode && BlockType == BlockType.h2 && !string.IsNullOrWhiteSpace(id)) + if(m.DocNetMode && !string.IsNullOrWhiteSpace(id)) { - // collect h2 id + text in collector - var h2ContentSb = new StringBuilder(); - m.SpanFormatter.Format(h2ContentSb, Buf, ContentStart, ContentLen); - var h2ContentAsString = h2ContentSb.ToString(); - b.Append(h2ContentAsString); - m.CreatedH2IdCollector.Add(new Tuple(id, h2ContentAsString)); + // collect id + text in collector + var headerContentStringBuilder = new StringBuilder(); + m.SpanFormatter.Format(headerContentStringBuilder, Buf, ContentStart, ContentLen); + var headerContentAsString = headerContentStringBuilder.ToString(); + b.Append(headerContentAsString); + m.Headings.Add(new Heading + { + Level = (int)BlockType, + Id = id, + Name = headerContentAsString + }); } else { diff --git a/src/MarkdownDeep/Extensions.cs b/src/MarkdownDeep/Extensions.cs new file mode 100644 index 0000000..4901e42 --- /dev/null +++ b/src/MarkdownDeep/Extensions.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; + +namespace MarkdownDeep +{ + public static class Extensions + { + public static List ConvertToHierarchy(this List headings) + { + var hierarchy = new List(); + + for (var i = 0; i < headings.Count; i++) + { + if (i > 0) + { + var previousHeading = headings[i - 1]; + var currentHeading = headings[i]; + + SetParentForHeading(previousHeading, currentHeading); + + var parent = currentHeading.Parent; + if (parent == null) + { + hierarchy.Add(currentHeading); + } + else + { + parent.Children.Add(currentHeading); + } + } + else + { + hierarchy.Add(headings[i]); + } + } + + return hierarchy; + } + + private static void SetParentForHeading(Heading previousHeading, Heading headingToAdd) + { + if (previousHeading.Level == headingToAdd.Level) + { + headingToAdd.Parent = previousHeading.Parent; + } + else if (previousHeading.Level < headingToAdd.Level) + { + headingToAdd.Parent = previousHeading; + } + else if (previousHeading.Level > headingToAdd.Level) + { + var previousHeadingParent = previousHeading.Parent; + if (previousHeadingParent != null) + { + SetParentForHeading(previousHeadingParent, headingToAdd); + } + } + } + } +} \ No newline at end of file diff --git a/src/MarkdownDeep/Heading.cs b/src/MarkdownDeep/Heading.cs new file mode 100644 index 0000000..6ff8814 --- /dev/null +++ b/src/MarkdownDeep/Heading.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Text; + +namespace MarkdownDeep +{ + public class Heading + { + public Heading() + { + Children = new List(); + } + + public Heading Parent { get; set; } + + public List Children { get; private set; } + + public int Level { get; set; } + + public string Id { get; set; } + + public string Name { get; set; } + + public override string ToString() + { + var stringBuilder = new StringBuilder(); + + for (var i = 0; i < Level; i++) + { + stringBuilder.Append("#"); + } + + stringBuilder.AppendLine($"{Id} - {Name}"); + + foreach (var child in Children) + { + stringBuilder.AppendLine(child.ToString()); + } + + var value = stringBuilder.ToString(); + return value; + } + } +} \ No newline at end of file diff --git a/src/MarkdownDeep/MardownDeep.cs b/src/MarkdownDeep/MardownDeep.cs index 796525c..d851caf 100644 --- a/src/MarkdownDeep/MardownDeep.cs +++ b/src/MarkdownDeep/MardownDeep.cs @@ -58,7 +58,9 @@ public Markdown() m_Footnotes = new Dictionary(); m_UsedFootnotes = new List(); m_UsedHeaderIDs = new Dictionary(); - this.CreatedH2IdCollector = new List>(); + + this.Headings = new List(); + _tabIdCounter = 0; } @@ -977,13 +979,11 @@ public bool NoFollowExternalLinks set; } - /// - /// Collector for the created id's for H2 headers. First element in Tuple is id name, second is name for ToC (the text for H2). Id's are generated + /// Collector for the created id's for headers. First element in Tuple is id name, second is name for ToC (the text for header). Id's are generated /// by the parser and use pandoc algorithm, as AutoHeadingId's is switched on. Only in use if DocNetMode is set to true /// - public List> CreatedH2IdCollector { get; private set; } - + public List Headings { get; private set; } // Set the html class for the footnotes div // (defaults to "footnotes") diff --git a/src/MarkdownDeep/MarkdownDeep.csproj b/src/MarkdownDeep/MarkdownDeep.csproj index 15dce5a..9cec4eb 100644 --- a/src/MarkdownDeep/MarkdownDeep.csproj +++ b/src/MarkdownDeep/MarkdownDeep.csproj @@ -77,7 +77,9 @@ + + diff --git a/src/MarkdownDeepTests/ExtensionsTests.cs b/src/MarkdownDeepTests/ExtensionsTests.cs new file mode 100644 index 0000000..601e5d8 --- /dev/null +++ b/src/MarkdownDeepTests/ExtensionsTests.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using MarkdownDeep; +using NUnit.Framework; + +namespace MarkdownDeepTests +{ + [TestFixture] + public class ExtensionsTests + { + [TestCase] + public void ConvertsHeadingsHierarchy() + { + var headings = new List(); + headings.Add(new Heading { Level = 1, Name = "1" }); + headings.Add(new Heading { Level = 2, Name = "1.1" }); + headings.Add(new Heading { Level = 3, Name = "1.1.1" }); + headings.Add(new Heading { Level = 2, Name = "1.2" }); + headings.Add(new Heading { Level = 4, Name = "1.2.1.1" }); + headings.Add(new Heading { Level = 2, Name = "1.3" }); + headings.Add(new Heading { Level = 1, Name = "2" }); + headings.Add(new Heading { Level = 3, Name = "2.1.1" }); + headings.Add(new Heading { Level = 2, Name = "2.2" }); + + var hierarchy = headings.ConvertToHierarchy(); + + Assert.AreEqual(2, hierarchy.Count); + + var heading1 = hierarchy[0]; + Assert.AreEqual("1", heading1.Name); + Assert.AreEqual(3, heading1.Children.Count); + + var heading1_1 = heading1.Children[0]; + Assert.AreEqual("1.1", heading1_1.Name); + Assert.AreEqual(1, heading1_1.Children.Count); + + var heading1_1_1 = heading1_1.Children[0]; + Assert.AreEqual("1.1.1", heading1_1_1.Name); + Assert.AreEqual(0, heading1_1_1.Children.Count); + + var heading1_2 = heading1.Children[1]; + Assert.AreEqual("1.2", heading1_2.Name); + Assert.AreEqual(1, heading1_2.Children.Count); + + var heading1_2_1_1 = heading1_2.Children[0]; + Assert.AreEqual("1.2.1.1", heading1_2_1_1.Name); + Assert.AreEqual(0, heading1_2_1_1.Children.Count); + + var heading1_3 = heading1.Children[2]; + Assert.AreEqual("1.3", heading1_3.Name); + Assert.AreEqual(0, heading1_3.Children.Count); + + var heading2 = hierarchy[1]; + Assert.AreEqual("2", heading2.Name); + Assert.AreEqual(2, heading2.Children.Count); + + var heading2_1_1 = heading2.Children[0]; + Assert.AreEqual("2.1.1", heading2_1_1.Name); + Assert.AreEqual(0, heading2_1_1.Children.Count); + + var heading2_2 = heading2.Children[1]; + Assert.AreEqual("2.2", heading2_2.Name); + Assert.AreEqual(0, heading2_2.Children.Count); + } + } +} \ No newline at end of file diff --git a/src/MarkdownDeepTests/MarkdownDeepTests.csproj b/src/MarkdownDeepTests/MarkdownDeepTests.csproj index 21def54..d3512f9 100644 --- a/src/MarkdownDeepTests/MarkdownDeepTests.csproj +++ b/src/MarkdownDeepTests/MarkdownDeepTests.csproj @@ -75,6 +75,7 @@ +