| @@ -31,325 +31,312 @@ using Newtonsoft.Json.Linq; | |||
| namespace Docnet | |||
| { | |||
| public class NavigationLevel : NavigationElement<List<INavigationElement>> | |||
| { | |||
| private readonly string _rootDirectory; | |||
| public NavigationLevel(string rootDirectory) | |||
| : base() | |||
| { | |||
| this._rootDirectory = rootDirectory; | |||
| this.Value = new List<INavigationElement>(); | |||
| } | |||
| public void Load(JObject dataFromFile) | |||
| { | |||
| foreach (KeyValuePair<string, JToken> 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<string>(); | |||
| 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); | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Collects the search index entries. These are created from simple navigation elements found in this container, which aren't index element. | |||
| /// </summary> | |||
| /// <param name="collectedEntries">The collected entries.</param> | |||
| /// <param name="activePath">The active path currently navigated.</param> | |||
| public override void CollectSearchIndexEntries(List<SearchIndexEntry> collectedEntries, NavigatedPath activePath) | |||
| { | |||
| activePath.Push(this); | |||
| foreach (var element in this.Value) | |||
| { | |||
| element.CollectSearchIndexEntries(collectedEntries, activePath); | |||
| } | |||
| activePath.Pop(); | |||
| } | |||
| /// <summary> | |||
| /// Generates the output for this navigation element | |||
| /// </summary> | |||
| /// <param name="activeConfig">The active configuration to use for the output.</param> | |||
| /// <param name="activePath">The active path navigated through the ToC to reach this element.</param> | |||
| 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(); | |||
| } | |||
| /// <summary> | |||
| /// Generates the ToC fragment for this element, which can either be a simple line or a full expanded menu. | |||
| /// </summary> | |||
| /// <param name="navigatedPath">The navigated path to the current element, which doesn't necessarily have to be this element.</param> | |||
| /// <param name="relativePathToRoot">The relative path back to the URL root, e.g. ../.., so it can be used for links to elements in this path.</param> | |||
| /// <returns></returns> | |||
| public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot) | |||
| { | |||
| var fragments = new List<string>(); | |||
| if (!this.IsRoot) | |||
| { | |||
| fragments.Add("<li class=\"tocentry\">"); | |||
| } | |||
| 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("<ul class=\"current\">"); | |||
| } | |||
| else | |||
| { | |||
| fragments.Add("<ul>"); | |||
| } | |||
| // 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 = "<li><span class=\"navigationgroup\"><i class=\"fa fa-caret-down\"></i> "; | |||
| var indexElement = this.IndexElement; | |||
| if (indexElement == null) | |||
| { | |||
| fragments.Add(string.Format("{0}{1}</span></li>", elementStartTag, this.Name)); | |||
| } | |||
| else | |||
| { | |||
| if (this.IsRoot) | |||
| { | |||
| fragments.Add(indexElement.PerformGenerateToCFragment(navigatedPath, relativePathToRoot)); | |||
| } | |||
| else | |||
| { | |||
| fragments.Add(string.Format("{0}<a href=\"{1}{2}\">{3}</a></span></li>", 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("</ul>"); | |||
| } | |||
| else | |||
| { | |||
| // just a link | |||
| fragments.Add(string.Format("<span class=\"navigationgroup\"><i class=\"fa fa-caret-right\"></i> <a href=\"{0}{1}\">{2}</a></span>", | |||
| relativePathToRoot, HttpUtility.UrlPathEncode(this.TargetURL), this.Name)); | |||
| } | |||
| if (!this.IsRoot) | |||
| { | |||
| fragments.Add("</li>"); | |||
| } | |||
| 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; | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Gets / sets a value indicating whether this element is the __index element | |||
| /// </summary> | |||
| public override bool IsIndexElement | |||
| { | |||
| // never an index | |||
| get { return false; } | |||
| set | |||
| { | |||
| // nop; | |||
| } | |||
| } | |||
| public bool IsRoot { get; set; } | |||
| #endregion | |||
| } | |||
| public class NavigationLevel : NavigationElement<List<INavigationElement>> | |||
| { | |||
| #region Members | |||
| private readonly string _rootDirectory; | |||
| #endregion | |||
| public NavigationLevel(string rootDirectory) | |||
| : base() | |||
| { | |||
| this._rootDirectory = rootDirectory; | |||
| this.Value = new List<INavigationElement>(); | |||
| } | |||
| public void Load(JObject dataFromFile) | |||
| { | |||
| foreach (KeyValuePair<string, JToken> 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<string>(); | |||
| if (childValue.EndsWith("**")) | |||
| { | |||
| 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); | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Collects the search index entries. These are created from simple navigation elements found in this container, which aren't index element. | |||
| /// </summary> | |||
| /// <param name="collectedEntries">The collected entries.</param> | |||
| /// <param name="activePath">The active path currently navigated.</param> | |||
| public override void CollectSearchIndexEntries(List<SearchIndexEntry> collectedEntries, NavigatedPath activePath) | |||
| { | |||
| activePath.Push(this); | |||
| foreach (var element in this.Value) | |||
| { | |||
| element.CollectSearchIndexEntries(collectedEntries, activePath); | |||
| } | |||
| activePath.Pop(); | |||
| } | |||
| /// <summary> | |||
| /// Generates the output for this navigation element | |||
| /// </summary> | |||
| /// <param name="activeConfig">The active configuration to use for the output.</param> | |||
| /// <param name="activePath">The active path navigated through the ToC to reach this element.</param> | |||
| 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(); | |||
| } | |||
| /// <summary> | |||
| /// Generates the ToC fragment for this element, which can either be a simple line or a full expanded menu. | |||
| /// </summary> | |||
| /// <param name="navigatedPath">The navigated path to the current element, which doesn't necessarily have to be this element.</param> | |||
| /// <param name="relativePathToRoot">The relative path back to the URL root, e.g. ../.., so it can be used for links to elements in this path.</param> | |||
| /// <returns></returns> | |||
| public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot) | |||
| { | |||
| var fragments = new List<string>(); | |||
| if (!this.IsRoot) | |||
| { | |||
| fragments.Add("<li class=\"tocentry\">"); | |||
| } | |||
| 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("<ul class=\"current\">"); | |||
| } | |||
| else | |||
| { | |||
| fragments.Add("<ul>"); | |||
| } | |||
| // 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 = "<li><span class=\"navigationgroup\"><i class=\"fa fa-caret-down\"></i> "; | |||
| var indexElement = this.IndexElement; | |||
| if (indexElement == null) | |||
| { | |||
| fragments.Add(string.Format("{0}{1}</span></li>", elementStartTag, this.Name)); | |||
| } | |||
| else | |||
| { | |||
| if (this.IsRoot) | |||
| { | |||
| fragments.Add(indexElement.PerformGenerateToCFragment(navigatedPath, relativePathToRoot)); | |||
| } | |||
| else | |||
| { | |||
| fragments.Add(string.Format("{0}<a href=\"{1}{2}\">{3}</a></span></li>", 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("</ul>"); | |||
| } | |||
| else | |||
| { | |||
| // just a link | |||
| fragments.Add(string.Format("<span class=\"navigationgroup\"><i class=\"fa fa-caret-right\"></i> <a href=\"{0}{1}\">{2}</a></span>", | |||
| relativePathToRoot, HttpUtility.UrlPathEncode(this.TargetURL), this.Name)); | |||
| } | |||
| if (!this.IsRoot) | |||
| { | |||
| fragments.Add("</li>"); | |||
| } | |||
| 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 = Utils.MakeRelativePath(mdFile, _rootDirectory); | |||
| 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 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; | |||
| } | |||
| } | |||
| /// <summary> | |||
| /// Gets / sets a value indicating whether this element is the __index element | |||
| /// </summary> | |||
| public override bool IsIndexElement | |||
| { | |||
| // never an index | |||
| get { return false; } | |||
| set | |||
| { | |||
| // nop; | |||
| } | |||
| } | |||
| public bool IsRoot { get; set; } | |||
| #endregion | |||
| } | |||
| } | |||