From 3e4a503ad3cf137952acc6ab2a4d768e1d598099 Mon Sep 17 00:00:00 2001 From: Geert van Horrik Date: Wed, 14 Jun 2017 13:10:07 +0200 Subject: [PATCH 1/6] List all headings and build a hierarchy of the headings inside a markdown file --- src/DocNet/Config.cs | 8 +++ src/DocNet/INavigationElement.cs | 3 +- src/DocNet/NavigatedPath.cs | 7 +- src/DocNet/NavigationElement.cs | 3 +- src/DocNet/NavigationLevel.cs | 7 +- src/DocNet/SimpleNavigationElement.cs | 97 +++++++++++++++++--------- src/DocNet/Utils.cs | 7 +- src/MarkdownDeep/Block.cs | 19 +++-- src/MarkdownDeep/Extensions.cs | 59 ++++++++++++++++ src/MarkdownDeep/Heading.cs | 43 ++++++++++++ src/MarkdownDeep/MardownDeep.cs | 10 +-- src/MarkdownDeep/MarkdownDeep.csproj | 2 + src/MarkdownDeepTests/ExtensionsTests.cs | 65 +++++++++++++++++ src/MarkdownDeepTests/MarkdownDeepTests.csproj | 1 + 14 files changed, 276 insertions(+), 55 deletions(-) create mode 100644 src/MarkdownDeep/Extensions.cs create mode 100644 src/MarkdownDeep/Heading.cs create mode 100644 src/MarkdownDeepTests/ExtensionsTests.cs diff --git a/src/DocNet/Config.cs b/src/DocNet/Config.cs index 2ff4195..2907117 100644 --- a/src/DocNet/Config.cs +++ b/src/DocNet/Config.cs @@ -216,6 +216,14 @@ namespace Docnet } } + public int MaxLevelInToC + { + get + { + return _configData.MaxLevelInToC ?? 2; + } + } + public string ThemeName { get diff --git a/src/DocNet/INavigationElement.cs b/src/DocNet/INavigationElement.cs index 0e97aac..12a4695 100644 --- a/src/DocNet/INavigationElement.cs +++ b/src/DocNet/INavigationElement.cs @@ -43,8 +43,9 @@ namespace Docnet /// /// 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 maximum level. /// - string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot); + string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel); /// /// Collects the search index entries. These are created from simple navigation elements found in this container, which aren't index element. /// diff --git a/src/DocNet/NavigatedPath.cs b/src/DocNet/NavigatedPath.cs index b53f6c1..e08e76e 100644 --- a/src/DocNet/NavigatedPath.cs +++ b/src/DocNet/NavigatedPath.cs @@ -73,11 +73,12 @@ namespace Docnet /// /// Creates the ToC HTML for the element reached by the elements in this path. All containers in this path are expanded, all elements inside these containers which - /// aren't, are not expanded. + /// 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 maximum level. /// - public string CreateToCHTML(string relativePathToRoot) + public string CreateToCHTML(string relativePathToRoot, int maxLevel) { // 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; @@ -86,7 +87,7 @@ namespace Docnet // no root container, no TOC return string.Empty; } - return rootContainer.GenerateToCFragment(this, relativePathToRoot); + return rootContainer.GenerateToCFragment(this, relativePathToRoot, maxLevel); } } } diff --git a/src/DocNet/NavigationElement.cs b/src/DocNet/NavigationElement.cs index dd86175..4a26709 100644 --- a/src/DocNet/NavigationElement.cs +++ b/src/DocNet/NavigationElement.cs @@ -43,8 +43,9 @@ namespace Docnet /// /// 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 maximum level. /// - public abstract string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot); + public abstract string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel); /// /// Collects the search index entries. These are created from simple navigation elements found in this container, which aren't index element. /// diff --git a/src/DocNet/NavigationLevel.cs b/src/DocNet/NavigationLevel.cs index 57bc38b..3b29373 100644 --- a/src/DocNet/NavigationLevel.cs +++ b/src/DocNet/NavigationLevel.cs @@ -106,8 +106,9 @@ namespace Docnet /// /// 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 maximum level. /// - public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot) + public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel) { var fragments = new List(); if(!this.IsRoot) @@ -138,7 +139,7 @@ namespace Docnet { if(this.IsRoot) { - fragments.Add(indexElement.PerformGenerateToCFragment(navigatedPath, relativePathToRoot)); + fragments.Add(indexElement.PerformGenerateToCFragment(navigatedPath, relativePathToRoot, maxLevel)); } else { @@ -149,7 +150,7 @@ namespace Docnet // then the elements in the container. Index elements are skipped here. foreach(var element in this.Value) { - fragments.Add(element.GenerateToCFragment(navigatedPath, relativePathToRoot)); + fragments.Add(element.GenerateToCFragment(navigatedPath, relativePathToRoot, maxLevel)); } fragments.Add(""); } diff --git a/src/DocNet/SimpleNavigationElement.cs b/src/DocNet/SimpleNavigationElement.cs index c47f2b7..6ecf0bd 100644 --- a/src/DocNet/SimpleNavigationElement.cs +++ b/src/DocNet/SimpleNavigationElement.cs @@ -27,6 +27,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web; +using MarkdownDeep; namespace Docnet { @@ -34,13 +35,14 @@ namespace Docnet { #region Members private string _targetURLForHTML; - private List> _relativeH2LinksOnPage; // first element in Tuple is anchor name, second is name for ToC. + + private readonly List _relativeLinksOnPage; // first element in Tuple is anchor name, second is name for ToC. #endregion public SimpleNavigationElement() { - _relativeH2LinksOnPage = new List>(); + _relativeLinksOnPage = new List(); } @@ -52,50 +54,50 @@ namespace Docnet public override void GenerateOutput(Config activeConfig, NavigatedPath activePath) { // 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.TargetURL); 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, HttpUtility.UrlPathEncode(sibling.TargetURL), 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)); } @@ -107,17 +109,17 @@ namespace Docnet 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)); - sb.Replace("{{ToC}}", activePath.CreateToCHTML(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)); + sb.Replace("{{ToC}}", activePath.CreateToCHTML(relativePathToRoot, activeConfig.MaxLevelInToC)); 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(); } @@ -133,7 +135,7 @@ namespace Docnet { 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.TargetURL, this.Name, activePath); @@ -148,16 +150,17 @@ namespace Docnet /// /// 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 maximum level. /// - public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot) + public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel) { // index elements are rendered in the parent container. - if(this.IsIndexElement) + if (this.IsIndexElement) { return string.Empty; } - return PerformGenerateToCFragment(navigatedPath, relativePathToRoot); + return PerformGenerateToCFragment(navigatedPath, relativePathToRoot, maxLevel, null); } @@ -167,15 +170,17 @@ namespace Docnet /// /// The navigated path. /// The relative path to root. + /// The maximum level. + /// The parent heading. /// - public string PerformGenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot) + public string PerformGenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel) { // 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"; @@ -186,20 +191,46 @@ namespace Docnet relativePathToRoot, HttpUtility.UrlPathEncode(this.TargetURL), this.Name)); - if(isCurrent && _relativeH2LinksOnPage.Any()) + + if (isCurrent) { - // generate relative links - fragments.Add(string.Format("
    ", this.ParentContainer.IsRoot ? "currentrelativeroot" : "currentrelative")); - foreach(var p in _relativeH2LinksOnPage) + var content = PerformGenerateToCFragment(navigatedPath, relativePathToRoot, maxLevel, null); + if (!string.IsNullOrWhiteSpace(content)) { - fragments.Add(string.Format("
  • {1}
  • ", p.Item1, p.Item2)); + fragments.Add(content); } - fragments.Add("
"); } - else + + fragments.Add(""); + + return string.Join(Environment.NewLine, fragments.ToArray()); + } + + private string PerformGenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel, Heading parentHeading) + { + var fragments = new List(); + + var headings = (parentHeading != null) ? parentHeading.Children : _relativeLinksOnPage; + var includedHeadings = headings.Where(x => x.Level > 1 && x.Level <= maxLevel).ToList(); + if (includedHeadings.Count > 0) { - fragments.Add(""); + fragments.Add(string.Format("
    ", this.ParentContainer.IsRoot ? "currentrelativeroot" : "currentrelative")); + + // generate relative links + foreach (var heading in includedHeadings) + { + fragments.Add(string.Format("
  • {1}
  • ", heading.Id, heading.Name)); + + var headingContent = PerformGenerateToCFragment(navigatedPath, relativePathToRoot, maxLevel, heading); + if (!string.IsNullOrWhiteSpace(headingContent)) + { + fragments.Add(headingContent); + } + } + + fragments.Add("
"); } + return string.Join(Environment.NewLine, fragments.ToArray()); } @@ -209,12 +240,12 @@ namespace Docnet { get { - if(_targetURLForHTML==null) + if (_targetURLForHTML == null) { _targetURLForHTML = (this.Value ?? string.Empty); - if(_targetURLForHTML.ToLowerInvariant().EndsWith(".md")) + if (_targetURLForHTML.ToLowerInvariant().EndsWith(".md")) { - _targetURLForHTML = _targetURLForHTML.Substring(0, _targetURLForHTML.Length-3) + ".htm"; + _targetURLForHTML = _targetURLForHTML.Substring(0, _targetURLForHTML.Length - 3) + ".htm"; } _targetURLForHTML = _targetURLForHTML.Replace("\\", "/"); } 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.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using MarkdownDeep; namespace Docnet { @@ -50,7 +51,7 @@ namespace Docnet /// 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 @@ namespace Docnet }; 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 @@ namespace MarkdownDeep { 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 9b496fe..0e2eaa9 100644 --- a/src/MarkdownDeep/MardownDeep.cs +++ b/src/MarkdownDeep/MardownDeep.cs @@ -57,7 +57,9 @@ namespace MarkdownDeep m_Footnotes = new Dictionary(); m_UsedFootnotes = new List(); m_UsedHeaderIDs = new Dictionary(); - this.CreatedH2IdCollector = new List>(); + + this.Headings = new List(); + _tabIdCounter = 0; } @@ -951,13 +953,11 @@ namespace MarkdownDeep 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 @@ + From f690ac744f255b90bdf67f3aad368949a409e516 Mon Sep 17 00:00:00 2001 From: Geert van Horrik Date: Mon, 3 Jul 2017 13:20:26 +0200 Subject: [PATCH 2/6] Introduce NavigationContext to make it easier to add new navigation properties in the future --- src/DocNet/Config.cs | 15 ++++++----- src/DocNet/Docnet.csproj | 1 + src/DocNet/Engine.cs | 15 ++++++++--- src/DocNet/INavigationElement.cs | 12 ++++----- src/DocNet/NavigatedPath.cs | 6 ++--- src/DocNet/NavigationContext.cs | 21 +++++++++++++++ src/DocNet/NavigationElement.cs | 12 ++++----- src/DocNet/NavigationLevel.cs | 26 +++++++++---------- src/DocNet/SimpleNavigationElement.cs | 48 ++++++++++++++++++----------------- 9 files changed, 94 insertions(+), 62 deletions(-) create mode 100644 src/DocNet/NavigationContext.cs diff --git a/src/DocNet/Config.cs b/src/DocNet/Config.cs index fdfdf05..0b639b9 100644 --- a/src/DocNet/Config.cs +++ b/src/DocNet/Config.cs @@ -72,10 +72,11 @@ namespace Docnet /// 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 @@ namespace Docnet /// /// 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 @@ namespace Docnet } - private void GenerateSearchPage() + private void GenerateSearchPage(NavigationContext navigationContext) { var activePath = new NavigatedPath(); activePath.Push(this.Pages); @@ -164,7 +165,7 @@ namespace Docnet searchSimpleElement.ExtraScriptProducerFunc = e=> @" "; - searchSimpleElement.GenerateOutput(this, activePath, this.PathSpecification); + searchSimpleElement.GenerateOutput(this, activePath, navigationContext); activePath.Pop(); } 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 @@ namespace Docnet { return 1; } - GeneratePages(); + + var navigationContext = new NavigationContext + { + MaxLevel = _loadedConfig.MaxLevelInToC, + PathSpecification = _loadedConfig.PathSpecification + }; + + GeneratePages(navigationContext); return 0; } @@ -86,7 +93,7 @@ namespace Docnet /// 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 @@ namespace Docnet 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 @@ namespace Docnet /// /// 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 @@ namespace Docnet /// 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 @@ namespace Docnet // 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 @@ namespace Docnet /// /// 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 @@ namespace Docnet /// /// 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 @@ namespace Docnet /// /// 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 @@ namespace Docnet /// /// 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 @@ namespace Docnet // 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 @@ namespace Docnet { 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 @@ namespace Docnet { // 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..e9fe7d0 100644 --- a/src/DocNet/SimpleNavigationElement.cs +++ b/src/DocNet/SimpleNavigationElement.cs @@ -27,6 +27,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web; +using MarkdownDeep; namespace Docnet { @@ -34,13 +35,13 @@ namespace Docnet { #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,18 +50,19 @@ namespace Docnet /// /// 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) { 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; @@ -70,7 +72,7 @@ namespace Docnet 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 { @@ -90,10 +92,10 @@ namespace Docnet continue; } defaultMarkdown.AppendFormat("* [{0}]({1}{2}){3}", sibling.Name, relativePathToRoot, - sibling.GetFinalTargetUrl(pathSpecification), Environment.NewLine); + 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 { @@ -112,8 +114,8 @@ namespace Docnet 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("{{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 @@ -132,15 +134,15 @@ namespace Docnet /// /// 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) { 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,9 +154,9 @@ namespace Docnet /// /// 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) @@ -162,7 +164,7 @@ namespace Docnet return string.Empty; } - return PerformGenerateToCFragment(navigatedPath, relativePathToRoot, pathSpecification); + return PerformGenerateToCFragment(navigatedPath, relativePathToRoot, navigationContext); } @@ -172,9 +174,9 @@ namespace Docnet /// /// 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); @@ -190,15 +192,15 @@ namespace Docnet 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.Any()) { // generate relative links fragments.Add(string.Format("
      ", this.ParentContainer.IsRoot ? "currentrelativeroot" : "currentrelative")); - foreach(var p in _relativeH2LinksOnPage) + foreach(var p in _relativeLinksOnPage) { - fragments.Add(string.Format("
    • {1}
    • ", p.Item1, p.Item2)); + fragments.Add(string.Format("
    • {1}
    • ", p.Id, p.Name)); } fragments.Add("
    "); } From 895727586ec44e92ca2ea71ab9115a1bf52fb58b Mon Sep 17 00:00:00 2001 From: Geert van Horrik Date: Mon, 3 Jul 2017 13:38:45 +0200 Subject: [PATCH 3/6] Generate the requested levels in the ToC (default = 2) --- src/DocNet/SimpleNavigationElement.cs | 64 +++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/src/DocNet/SimpleNavigationElement.cs b/src/DocNet/SimpleNavigationElement.cs index e9fe7d0..6c82159 100644 --- a/src/DocNet/SimpleNavigationElement.cs +++ b/src/DocNet/SimpleNavigationElement.cs @@ -56,7 +56,7 @@ namespace Docnet 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); } @@ -67,7 +67,7 @@ namespace Docnet 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 @@ -78,20 +78,20 @@ namespace Docnet { // 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, + defaultMarkdown.AppendFormat("* [{0}]({1}{2}){3}", sibling.Name, relativePathToRoot, sibling.GetFinalTargetUrl(navigationContext.PathSpecification), Environment.NewLine); } defaultMarkdown.Append(Environment.NewLine); @@ -100,7 +100,7 @@ namespace Docnet 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)); } @@ -112,9 +112,9 @@ namespace Docnet 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, navigationContext.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)); @@ -122,7 +122,7 @@ namespace Docnet sb.Replace("{{Content}}", content); Utils.CreateFoldersIfRequired(destinationFile); File.WriteAllText(destinationFile, sb.ToString()); - if(!this.IsIndexElement) + if (!this.IsIndexElement) { activePath.Pop(); } @@ -139,7 +139,7 @@ namespace Docnet { 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(navigationContext.PathSpecification), this.Name, activePath); @@ -159,7 +159,7 @@ namespace Docnet 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; } @@ -183,7 +183,7 @@ namespace Docnet var fragments = new List(); var liClass = "tocentry"; var aClass = string.Empty; - if(isCurrent) + if (isCurrent) { liClass = "tocentry current"; aClass = "current"; @@ -194,13 +194,17 @@ namespace Docnet relativePathToRoot, this.GetFinalTargetUrl(navigationContext.PathSpecification), this.Name)); - if(isCurrent && _relativeLinksOnPage.Any()) + if (isCurrent && _relativeLinksOnPage.Any()) { // generate relative links fragments.Add(string.Format("
      ", this.ParentContainer.IsRoot ? "currentrelativeroot" : "currentrelative")); - foreach(var p in _relativeLinksOnPage) + foreach (var heading in _relativeLinksOnPage) { - fragments.Add(string.Format("
    • {1}
    • ", p.Id, p.Name)); + var content = GenerateToCFragmentForHeading(heading, navigationContext); + if (!string.IsNullOrWhiteSpace(content)) + { + fragments.Add(content); + } } fragments.Add("
    "); } @@ -245,6 +249,32 @@ namespace Docnet return _targetURLForHTML; } + private string GenerateToCFragmentForHeading(Heading heading, NavigationContext navigationContext) + { + var stringBuilder = new StringBuilder(); + + // Skip heading 1 and larger than allowed + if (heading.Level > 1 && heading.Level <= navigationContext.MaxLevel) + { + stringBuilder.AppendLine(string.Format("
  • {1}
  • ", heading.Id, heading.Name)); + } + + stringBuilder.AppendLine("
      "); + + foreach (var child in heading.Children) + { + var childContent = GenerateToCFragmentForHeading(child, navigationContext); + if (!string.IsNullOrWhiteSpace(childContent)) + { + stringBuilder.AppendLine(childContent); + } + } + + stringBuilder.AppendLine("
    "); + + return stringBuilder.ToString(); + } + #region Properties /// /// Gets / sets a value indicating whether this element is the __index element From 1ee6de14b8169a667a716987968d514044cf022d Mon Sep 17 00:00:00 2001 From: Geert van Horrik Date: Mon, 3 Jul 2017 14:05:07 +0200 Subject: [PATCH 4/6] Improved nested ToC generated code --- src/DocNet/SimpleNavigationElement.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/DocNet/SimpleNavigationElement.cs b/src/DocNet/SimpleNavigationElement.cs index 6c82159..81bcddf 100644 --- a/src/DocNet/SimpleNavigationElement.cs +++ b/src/DocNet/SimpleNavigationElement.cs @@ -259,18 +259,25 @@ namespace Docnet stringBuilder.AppendLine(string.Format("
  • {1}
  • ", heading.Id, heading.Name)); } - stringBuilder.AppendLine("
      "); + var childContentBuilder = new StringBuilder(); foreach (var child in heading.Children) { var childContent = GenerateToCFragmentForHeading(child, navigationContext); if (!string.IsNullOrWhiteSpace(childContent)) { - stringBuilder.AppendLine(childContent); + childContentBuilder.AppendLine(childContent); } } - stringBuilder.AppendLine("
    "); + if (childContentBuilder.Length > 0) + { + stringBuilder.AppendLine("
  • "); + stringBuilder.AppendLine(string.Format("
      ", this.ParentContainer.IsRoot ? "currentrelativeroot" : "currentrelative")); + stringBuilder.AppendLine(childContentBuilder.ToString()); + stringBuilder.AppendLine("
    "); + stringBuilder.AppendLine("
  • "); + } return stringBuilder.ToString(); } From f6a6421bad3b5622fa3871150c2e8fc98cb7b98e Mon Sep 17 00:00:00 2001 From: Geert van Horrik Date: Mon, 3 Jul 2017 19:32:48 +0200 Subject: [PATCH 5/6] Skip empty ul element generation for empty h1 lists --- src/DocNet/SimpleNavigationElement.cs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/DocNet/SimpleNavigationElement.cs b/src/DocNet/SimpleNavigationElement.cs index 81bcddf..53e6105 100644 --- a/src/DocNet/SimpleNavigationElement.cs +++ b/src/DocNet/SimpleNavigationElement.cs @@ -194,10 +194,11 @@ namespace Docnet relativePathToRoot, this.GetFinalTargetUrl(navigationContext.PathSpecification), this.Name)); - if (isCurrent && _relativeLinksOnPage.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 heading in _relativeLinksOnPage) { var content = GenerateToCFragmentForHeading(heading, navigationContext); @@ -206,6 +207,7 @@ namespace Docnet fragments.Add(content); } } + fragments.Add("
    "); } else @@ -254,7 +256,8 @@ namespace Docnet var stringBuilder = new StringBuilder(); // Skip heading 1 and larger than allowed - if (heading.Level > 1 && heading.Level <= navigationContext.MaxLevel) + var isHeading1 = heading.Level <= 1; + if (!isHeading1 && heading.Level <= navigationContext.MaxLevel) { stringBuilder.AppendLine(string.Format("
  • {1}
  • ", heading.Id, heading.Name)); } @@ -272,11 +275,19 @@ namespace Docnet if (childContentBuilder.Length > 0) { - stringBuilder.AppendLine("
  • "); - stringBuilder.AppendLine(string.Format("
      ", this.ParentContainer.IsRoot ? "currentrelativeroot" : "currentrelative")); + if (!isHeading1) + { + stringBuilder.AppendLine("
    • "); + stringBuilder.AppendLine(string.Format("
        ", this.ParentContainer.IsRoot ? "currentrelativeroot" : "currentrelative")); + } + stringBuilder.AppendLine(childContentBuilder.ToString()); - stringBuilder.AppendLine("
      "); - stringBuilder.AppendLine("
    • "); + + if (!isHeading1) + { + stringBuilder.AppendLine("
    "); + stringBuilder.AppendLine("
  • "); + } } return stringBuilder.ToString(); From e39d737afda03901a6424afa8461ddb14d55dbbe Mon Sep 17 00:00:00 2001 From: Frans Bouma Date: Tue, 4 Jul 2017 11:00:31 +0200 Subject: [PATCH 6/6] Tweaked theme updates --- Themes/Default/Destination/css/theme.css | 18 +++++++++++++++++- Themes/LLBLGenPro/Destination/css/theme.css | 18 +++++++++++++++++- Themes/Light/Destination/css/theme.css | 18 +++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/Themes/Default/Destination/css/theme.css b/Themes/Default/Destination/css/theme.css index 3966afc..e7e3c3d 100644 --- a/Themes/Default/Destination/css/theme.css +++ b/Themes/Default/Destination/css/theme.css @@ -1047,10 +1047,26 @@ div.figure p { } .menu-vertical li.tocentry { - padding-left: 15px; + padding-left: 20px; padding-top: 7px; } +.menu-vertical li.tocentry.current > ul.currentrelative { + padding-left: 7px; + padding-bottom: 5px; + margin-top: 0; +} + +.menu-vertical li.tocentry > ul.currentrelative { + padding-left: 7px; + padding-bottom: 5px; + margin-top: -3px; +} + +.menu-vertical li.tocentry > ul.currentrelative > li.tocentry { + padding-top: 3px !important; +} + .menu-vertical li.tocrootentry { padding-left: 15px; } diff --git a/Themes/LLBLGenPro/Destination/css/theme.css b/Themes/LLBLGenPro/Destination/css/theme.css index 3b4ed39..133fc8c 100644 --- a/Themes/LLBLGenPro/Destination/css/theme.css +++ b/Themes/LLBLGenPro/Destination/css/theme.css @@ -1047,10 +1047,26 @@ div.figure p { } .menu-vertical li.tocentry { - padding-left: 15px; + padding-left: 20px; padding-top: 7px; } +.menu-vertical li.tocentry.current > ul.currentrelative { + padding-left: 7px; + padding-bottom: 5px; + margin-top: 0; +} + +.menu-vertical li.tocentry > ul.currentrelative { + padding-left: 7px; + padding-bottom: 5px; + margin-top: -3px; +} + +.menu-vertical li.tocentry > ul.currentrelative > li.tocentry { + padding-top: 3px !important; +} + .menu-vertical li.tocrootentry { padding-left: 15px; } diff --git a/Themes/Light/Destination/css/theme.css b/Themes/Light/Destination/css/theme.css index 3966afc..e7e3c3d 100644 --- a/Themes/Light/Destination/css/theme.css +++ b/Themes/Light/Destination/css/theme.css @@ -1047,10 +1047,26 @@ div.figure p { } .menu-vertical li.tocentry { - padding-left: 15px; + padding-left: 20px; padding-top: 7px; } +.menu-vertical li.tocentry.current > ul.currentrelative { + padding-left: 7px; + padding-bottom: 5px; + margin-top: 0; +} + +.menu-vertical li.tocentry > ul.currentrelative { + padding-left: 7px; + padding-bottom: 5px; + margin-top: -3px; +} + +.menu-vertical li.tocentry > ul.currentrelative > li.tocentry { + padding-top: 3px !important; +} + .menu-vertical li.tocrootentry { padding-left: 15px; }