diff --git a/src/DocNet/Config.cs b/src/DocNet/Config.cs index 2ff4195..482e665 100644 --- a/src/DocNet/Config.cs +++ b/src/DocNet/Config.cs @@ -263,7 +263,7 @@ namespace Docnet if(_pages == null) { JObject rawPages = _configData.Pages; - _pages = new NavigationLevel() {Name = "Home", IsRoot = true}; + _pages = new NavigationLevel(Source) {Name = "Home", IsRoot = true}; _pages.Load(rawPages); } return _pages; diff --git a/src/DocNet/NavigationLevel.cs b/src/DocNet/NavigationLevel.cs index 57bc38b..5aafea2 100644 --- a/src/DocNet/NavigationLevel.cs +++ b/src/DocNet/NavigationLevel.cs @@ -31,198 +31,325 @@ using Newtonsoft.Json.Linq; namespace Docnet { - public class NavigationLevel : NavigationElement> - { - public NavigationLevel() : base() - { - this.Value = new List(); - } - - - public void Load(JObject dataFromFile) - { - foreach(KeyValuePair child in dataFromFile) - { - INavigationElement toAdd; - if(child.Value.Type == JTokenType.String) - { - var nameToUse = child.Key; - var isIndexElement = child.Key == "__index"; - if(isIndexElement) - { - nameToUse = this.Name; - } - toAdd = new SimpleNavigationElement() { Name = nameToUse, Value = child.Value.ToObject(), IsIndexElement = isIndexElement}; - } - else - { - var subLevel = new NavigationLevel() { Name = child.Key, IsRoot = false}; - subLevel.Load((JObject)child.Value); - toAdd = subLevel; - } - toAdd.ParentContainer = this; - this.Value.Add(toAdd); - } - } - - - /// - /// 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. - public override void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath) - { - activePath.Push(this); - foreach(var element in this.Value) - { - element.CollectSearchIndexEntries(collectedEntries, activePath); - } - activePath.Pop(); - } - - - /// - /// Generates the output for this navigation element - /// - /// The active configuration to use for the output. - /// The active path navigated through the ToC to reach this element. - public override void GenerateOutput(Config activeConfig, NavigatedPath activePath) - { - activePath.Push(this); - int i = 0; - while(i - /// 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. - /// - public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot) - { - var fragments = new List(); - if(!this.IsRoot) - { - fragments.Add("
  • "); - } - if(navigatedPath.Contains(this)) - { - // we're expanded. If we're not root and on the top of the navigated path stack, our index page is the page we're currently generating the ToC for, so - // we have to mark the entry as 'current' - if(navigatedPath.Peek() == this && !this.IsRoot) - { - fragments.Add("
      "); - } - else - { - fragments.Add("
        "); - } - - // 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.IndexElement; - if(indexElement == null) - { - fragments.Add(string.Format("{0}{1}
      • ", elementStartTag, this.Name)); - } - else - { - if(this.IsRoot) - { - fragments.Add(indexElement.PerformGenerateToCFragment(navigatedPath, relativePathToRoot)); - } - else - { - fragments.Add(string.Format("{0}{3}", elementStartTag, relativePathToRoot, HttpUtility.UrlPathEncode(indexElement.TargetURL), - 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)); - } - fragments.Add("
      "); - } - else - { - // just a link - fragments.Add(string.Format(" {2}", - relativePathToRoot, HttpUtility.UrlPathEncode(this.TargetURL), this.Name)); - } - if(!this.IsRoot) - { - fragments.Add(""); - } - return string.Join(Environment.NewLine, fragments.ToArray()); - } - - - #region Properties - public override string TargetURL - { - get - { - var defaultElement = this.IndexElement; - if(defaultElement == null) - { - return string.Empty; - } - return defaultElement.TargetURL ?? string.Empty; - } - } - - - public SimpleNavigationElement IndexElement - { - get - { - var toReturn = this.Value.FirstOrDefault(e => e.IsIndexElement) as SimpleNavigationElement; - if(toReturn == null) - { - // no index element, add an artificial one. - var path = string.Empty; - if(this.ParentContainer != null) - { - path = Path.GetDirectoryName(this.ParentContainer.TargetURL); - } - var nameToUse = this.Name.Replace(".", "").Replace('/', '_').Replace("\\", "_").Replace(":", "").Replace(" ", ""); - if(string.IsNullOrWhiteSpace(nameToUse)) - { - return null; - } - toReturn = new SimpleNavigationElement() {ParentContainer = this, Value = string.Format("{0}{1}.md", path, nameToUse), Name = this.Name, IsIndexElement = true}; - this.Value.Add(toReturn); - } - - return toReturn; - } - } - - - /// - /// Gets / sets a value indicating whether this element is the __index element - /// - public override bool IsIndexElement - { - // never an index - get { return false; } - set { - // nop; - } - } - - - public bool IsRoot { get; set; } - #endregion - } + public class NavigationLevel : NavigationElement> + { + private readonly string _rootDirectory; + + public NavigationLevel(string rootDirectory) + : base() + { + this._rootDirectory = rootDirectory; + this.Value = new List(); + } + + + public void Load(JObject dataFromFile) + { + foreach (KeyValuePair child in dataFromFile) + { + INavigationElement toAdd; + if (child.Value.Type == JTokenType.String) + { + var nameToUse = child.Key; + + var isIndexElement = child.Key == "__index"; + if (isIndexElement) + { + nameToUse = this.Name; + } + + var childValue = child.Value.ToObject(); + + var endsWithWildcards = childValue.EndsWith("**"); + if (endsWithWildcards) + { + var path = childValue.Replace("**", string.Empty) + .Replace('\\', Path.DirectorySeparatorChar) + .Replace('/', Path.DirectorySeparatorChar); + + if (!Path.IsPathRooted(path)) + { + path = Path.Combine(_rootDirectory, path); + } + + toAdd = CreateGeneratedLevel(path); + toAdd.Name = nameToUse; + } + else + { + toAdd = new SimpleNavigationElement + { + Name = nameToUse, + Value = childValue, + IsIndexElement = isIndexElement + }; + } + } + else + { + var subLevel = new NavigationLevel(_rootDirectory) + { + Name = child.Key, + IsRoot = false + }; + subLevel.Load((JObject)child.Value); + toAdd = subLevel; + } + toAdd.ParentContainer = this; + this.Value.Add(toAdd); + } + } + + + /// + /// 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. + public override void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath) + { + activePath.Push(this); + foreach (var element in this.Value) + { + element.CollectSearchIndexEntries(collectedEntries, activePath); + } + activePath.Pop(); + } + + + /// + /// Generates the output for this navigation element + /// + /// The active configuration to use for the output. + /// The active path navigated through the ToC to reach this element. + public override void GenerateOutput(Config activeConfig, NavigatedPath activePath) + { + activePath.Push(this); + int i = 0; + while (i < this.Value.Count) + { + var element = this.Value[i]; + element.GenerateOutput(activeConfig, activePath); + i++; + } + activePath.Pop(); + } + + + /// + /// 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. + /// + public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot) + { + var fragments = new List(); + if (!this.IsRoot) + { + fragments.Add("
    • "); + } + if (navigatedPath.Contains(this)) + { + // we're expanded. If we're not root and on the top of the navigated path stack, our index page is the page we're currently generating the ToC for, so + // we have to mark the entry as 'current' + if (navigatedPath.Peek() == this && !this.IsRoot) + { + fragments.Add("
        "); + } + else + { + fragments.Add("
          "); + } + + // 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.IndexElement; + if (indexElement == null) + { + fragments.Add(string.Format("{0}{1}
        • ", elementStartTag, this.Name)); + } + else + { + if (this.IsRoot) + { + fragments.Add(indexElement.PerformGenerateToCFragment(navigatedPath, relativePathToRoot)); + } + else + { + fragments.Add(string.Format("{0}{3}", elementStartTag, relativePathToRoot, HttpUtility.UrlPathEncode(indexElement.TargetURL), + 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)); + } + fragments.Add("
        "); + } + else + { + // just a link + fragments.Add(string.Format(" {2}", + relativePathToRoot, HttpUtility.UrlPathEncode(this.TargetURL), this.Name)); + } + if (!this.IsRoot) + { + fragments.Add(""); + } + return string.Join(Environment.NewLine, fragments.ToArray()); + } + + private NavigationLevel CreateGeneratedLevel(string path) + { + var root = new NavigationLevel(_rootDirectory) + { + ParentContainer = this + }; + + foreach (var mdFile in Directory.GetFiles(path, "*.md", SearchOption.TopDirectoryOnly)) + { + var name = FindTitleInMdFile(mdFile); + if (string.IsNullOrWhiteSpace(name)) + { + continue; + } + + var relativeFilePath = GetRelativePath(_rootDirectory, mdFile); + + var item = new SimpleNavigationElement + { + Name = name, + Value = relativeFilePath, + ParentContainer = root + }; + + root.Value.Add(item); + } + + foreach (var directory in Directory.GetDirectories(path, "*", SearchOption.TopDirectoryOnly)) + { + var directoryInfo = new DirectoryInfo(directory); + + var subDirectoryNavigationElement = CreateGeneratedLevel(directory); + + subDirectoryNavigationElement.Name = directoryInfo.Name; + subDirectoryNavigationElement.ParentContainer = root; + + root.Value.Add(subDirectoryNavigationElement); + } + + return root; + } + + private string GetRelativePath(string origin, string fullPath) + { + var pathUri = new Uri(fullPath); + + if (!origin.EndsWith(Path.DirectorySeparatorChar.ToString())) + { + origin += Path.DirectorySeparatorChar; + } + + var folderUri = new Uri(origin); + return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar)); + } + + private string FindTitleInMdFile(string path) + { + var title = string.Empty; + + using (var fileStream = File.OpenRead(path)) + { + using (var streamReader = new StreamReader(fileStream)) + { + var line = string.Empty; + + while (string.IsNullOrWhiteSpace(line)) + { + line = streamReader.ReadLine(); + + if (!string.IsNullOrWhiteSpace(line)) + { + line = line.Trim(); + + while (line.StartsWith("#")) + { + line = line.Substring(1).Trim(); + } + + if (!string.IsNullOrWhiteSpace(line)) + { + title = line; + break; + } + } + } + } + } + + return title; + } + + + #region Properties + public override string TargetURL + { + get + { + var defaultElement = this.IndexElement; + if (defaultElement == null) + { + return string.Empty; + } + return defaultElement.TargetURL ?? string.Empty; + } + } + + + public SimpleNavigationElement IndexElement + { + get + { + var toReturn = this.Value.FirstOrDefault(e => e.IsIndexElement) as SimpleNavigationElement; + if (toReturn == null) + { + // no index element, add an artificial one. + var path = string.Empty; + if (this.ParentContainer != null) + { + path = Path.GetDirectoryName(this.ParentContainer.TargetURL); + } + var nameToUse = this.Name.Replace(".", "").Replace('/', '_').Replace("\\", "_").Replace(":", "").Replace(" ", ""); + if (string.IsNullOrWhiteSpace(nameToUse)) + { + return null; + } + toReturn = new SimpleNavigationElement() { ParentContainer = this, Value = string.Format("{0}{1}.md", path, nameToUse), Name = this.Name, IsIndexElement = true }; + this.Value.Add(toReturn); + } + + return toReturn; + } + } + + + /// + /// Gets / sets a value indicating whether this element is the __index element + /// + public override bool IsIndexElement + { + // never an index + get { return false; } + set + { + // nop; + } + } + + + public bool IsRoot { get; set; } + #endregion + } }