diff --git a/src/DocNet/Config.cs b/src/DocNet/Config.cs index 2907117..fdfdf05 100644 --- a/src/DocNet/Config.cs +++ b/src/DocNet/Config.cs @@ -131,7 +131,7 @@ namespace Docnet private void GenerateSearchDataIndex() { var collectedSearchEntries = new List(); - this.Pages.CollectSearchIndexEntries(collectedSearchEntries, new NavigatedPath()); + this.Pages.CollectSearchIndexEntries(collectedSearchEntries, new NavigatedPath(), this.PathSpecification); JObject searchIndex = new JObject(new JProperty("docs", new JArray( collectedSearchEntries.Select(e=>new JObject( @@ -164,7 +164,7 @@ namespace Docnet searchSimpleElement.ExtraScriptProducerFunc = e=> @" "; - searchSimpleElement.GenerateOutput(this, activePath); + searchSimpleElement.GenerateOutput(this, activePath, this.PathSpecification); activePath.Pop(); } @@ -264,6 +264,25 @@ namespace Docnet get { return _templateContents ?? string.Empty; } } + public PathSpecification PathSpecification + { + get + { + var pathSpecification = PathSpecification.Full; + + var pathSpecificationAsString = (string)_configData.PathSpecification; + if (!string.IsNullOrWhiteSpace(pathSpecificationAsString)) + { + if (!Enum.TryParse(pathSpecificationAsString, true, out pathSpecification)) + { + pathSpecification = PathSpecification.Full; + } + } + + return pathSpecification; + } + } + public NavigationLevel Pages { get @@ -271,7 +290,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/Docnet.csproj b/src/DocNet/Docnet.csproj index ff52d74..99b3653 100644 --- a/src/DocNet/Docnet.csproj +++ b/src/DocNet/Docnet.csproj @@ -60,10 +60,12 @@ + + diff --git a/src/DocNet/Engine.cs b/src/DocNet/Engine.cs index 28435cf..5078bd1 100644 --- a/src/DocNet/Engine.cs +++ b/src/DocNet/Engine.cs @@ -70,11 +70,14 @@ namespace Docnet Console.WriteLine("Errors occurred, can't continue!"); return null; } - if(config.Pages.IndexElement == null) + + var indexElement = config.Pages.GetIndexElement(config.PathSpecification); + if(indexElement == null) { Console.WriteLine("[ERROR] Root __index not found. The root navigationlevel is required to have an __index element"); return null; } + return config; } @@ -95,7 +98,7 @@ 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.Pages.GenerateOutput(_loadedConfig, new NavigatedPath(), _loadedConfig.PathSpecification); Console.WriteLine("Generating search index"); _loadedConfig.GenerateSearchData(); Console.WriteLine("Done!"); diff --git a/src/DocNet/INavigationElement.cs b/src/DocNet/INavigationElement.cs index 12a4695..6a524be 100644 --- a/src/DocNet/INavigationElement.cs +++ b/src/DocNet/INavigationElement.cs @@ -35,32 +35,39 @@ namespace Docnet /// /// The active configuration to use for the output. /// The active path navigated through the ToC to reach this element. - void GenerateOutput(Config activeConfig, NavigatedPath activePath); - + /// The path specification. + void GenerateOutput(Config activeConfig, NavigatedPath activePath, PathSpecification pathSpecification); /// /// 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 maximum level. + /// The path specification. /// - string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel); + string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, PathSpecification pathSpecification); /// /// 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. - void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath); + /// The path specification. + void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, PathSpecification pathSpecification); + /// + /// Gets the target URL with respect to the . + /// + /// The path specification. + /// + string GetTargetURL(PathSpecification pathSpecification); /// /// Gets a value indicating whether this element is the __index element /// bool IsIndexElement { get; set; } + bool IsAutoGenerated { get; set; } string Name { get; set; } object Value { get; set; } - string TargetURL { get; } NavigationLevel ParentContainer { get; set; } } } diff --git a/src/DocNet/INavigationElementExtensions.cs b/src/DocNet/INavigationElementExtensions.cs new file mode 100644 index 0000000..b49a3f5 --- /dev/null +++ b/src/DocNet/INavigationElementExtensions.cs @@ -0,0 +1,35 @@ +using System; +using System.Web; + +namespace Docnet +{ + public static class INavigationElementExtensions + { + private const string IndexHtmFileName = "index.htm"; + + /// + /// Gets the final URL by encoding the path and by removing the filename if it equals index.htm. + /// + /// The navigation element. + /// The path specification. + /// + public static string GetFinalTargetUrl(this INavigationElement navigationElement, PathSpecification pathSpecification) + { + var targetUrl = navigationElement.GetTargetURL(pathSpecification); + var link = HttpUtility.UrlPathEncode(targetUrl); + + // Disabled for now as discussed in #65 (https://github.com/FransBouma/DocNet/pull/65), but + // is required for #44 + //if (pathSpecification == PathSpecification.RelativeAsFolder) + //{ + // if (link.Length > IndexHtmFileName.Length && + // link.EndsWith(IndexHtmFileName, StringComparison.InvariantCultureIgnoreCase)) + // { + // link = link.Substring(0, link.Length - IndexHtmFileName.Length); + // } + //} + + return link; + } + } +} \ No newline at end of file diff --git a/src/DocNet/NavigatedPath.cs b/src/DocNet/NavigatedPath.cs index e08e76e..c93f21c 100644 --- a/src/DocNet/NavigatedPath.cs +++ b/src/DocNet/NavigatedPath.cs @@ -35,17 +35,18 @@ namespace Docnet public class NavigatedPath : Stack { /// - /// Creates the bread crumbs HTML of the elements in this path, delimited by '/' characters. + /// Creates the bread crumbs HTML of the elements in this path, delimited by '/' characters. /// /// 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. /// - public string CreateBreadCrumbsHTML(string relativePathToRoot) + public string CreateBreadCrumbsHTML(string relativePathToRoot, PathSpecification pathSpecification) { var fragments = new List(); // we enumerate a stack, which enumerates from top to bottom, so we have to reverse things first. foreach(var element in this.Reverse()) { - var targetURL = element.TargetURL; + var targetURL = element.GetTargetURL(pathSpecification); if(string.IsNullOrWhiteSpace(targetURL)) { fragments.Add(string.Format("
  • {0}
  • ", element.Name)); @@ -76,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 maximum level. + /// The path specification. /// - public string CreateToCHTML(string relativePathToRoot, int maxLevel) + public string CreateToCHTML(string relativePathToRoot, PathSpecification pathSpecification) { // 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; @@ -87,7 +88,7 @@ namespace Docnet // no root container, no TOC return string.Empty; } - return rootContainer.GenerateToCFragment(this, relativePathToRoot, maxLevel); + return rootContainer.GenerateToCFragment(this, relativePathToRoot, pathSpecification); } } } diff --git a/src/DocNet/NavigationElement.cs b/src/DocNet/NavigationElement.cs index 4a26709..11f210c 100644 --- a/src/DocNet/NavigationElement.cs +++ b/src/DocNet/NavigationElement.cs @@ -36,31 +36,39 @@ namespace Docnet /// /// The active configuration to use for the output. /// The active path navigated through the ToC to reach this element. - /// true if everything went well, false otherwise - public abstract void GenerateOutput(Config activeConfig, NavigatedPath activePath); + /// The path specification. + public abstract void GenerateOutput(Config activeConfig, NavigatedPath activePath, PathSpecification pathSpecification); /// /// 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 maximum level. + /// The path specification. /// - public abstract string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel); + public abstract string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, PathSpecification pathSpecification); /// /// 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 abstract void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath); + /// The path specification. + public abstract void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, PathSpecification pathSpecification); + /// + /// Gets the target URL with respect to the . + /// + /// The path specification. + /// + public abstract string GetTargetURL(PathSpecification pathSpecification); #region Properties - public abstract string TargetURL { get; } /// /// Gets / sets a value indicating whether this element is the __index element /// public abstract bool IsIndexElement { get; set; } + public bool IsAutoGenerated { get; set; } + public string Name { get; set; } /// /// Gets or sets the value of this element, which can either be a string or a NavigationLevel diff --git a/src/DocNet/NavigationLevel.cs b/src/DocNet/NavigationLevel.cs index 3b29373..ba1d781 100644 --- a/src/DocNet/NavigationLevel.cs +++ b/src/DocNet/NavigationLevel.cs @@ -33,30 +33,65 @@ namespace Docnet { public class NavigationLevel : NavigationElement> { - public NavigationLevel() : base() + #region Members + private readonly string _rootDirectory; + #endregion + + public NavigationLevel(string rootDirectory) + : base() { + this._rootDirectory = rootDirectory; this.Value = new List(); } public void Load(JObject dataFromFile) { - foreach(KeyValuePair child in dataFromFile) + foreach (KeyValuePair child in dataFromFile) { INavigationElement toAdd; - if(child.Value.Type == JTokenType.String) + if (child.Value.Type == JTokenType.String) { var nameToUse = child.Key; + var isIndexElement = child.Key == "__index"; - if(isIndexElement) + if (isIndexElement) { nameToUse = this.Name; } - toAdd = new SimpleNavigationElement() { Name = nameToUse, Value = child.Value.ToObject(), IsIndexElement = isIndexElement}; + + var childValue = child.Value.ToObject(); + 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() { Name = child.Key, IsRoot = false}; + var subLevel = new NavigationLevel(_rootDirectory) + { + Name = child.Key, + IsRoot = false + }; subLevel.Load((JObject)child.Value); toAdd = subLevel; } @@ -71,12 +106,13 @@ namespace Docnet /// /// The collected entries. /// The active path currently navigated. - public override void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath) + /// The path specification. + public override void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, PathSpecification pathSpecification) { activePath.Push(this); - foreach(var element in this.Value) + foreach (var element in this.Value) { - element.CollectSearchIndexEntries(collectedEntries, activePath); + element.CollectSearchIndexEntries(collectedEntries, activePath, pathSpecification); } activePath.Pop(); } @@ -87,14 +123,15 @@ namespace Docnet /// /// 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) + /// The path specification. + public override void GenerateOutput(Config activeConfig, NavigatedPath activePath, PathSpecification pathSpecification) { activePath.Push(this); int i = 0; - while(i /// 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. + /// The path specification. /// - public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel) + public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, PathSpecification pathSpecification) { var fragments = new List(); - if(!this.IsRoot) + if (!this.IsRoot) { fragments.Add("
  • "); } - if(navigatedPath.Contains(this)) + 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) + if (navigatedPath.Peek() == this && !this.IsRoot) { fragments.Add("
      "); } @@ -130,37 +167,37 @@ 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.IndexElement; - if(indexElement == null) + var indexElement = this.GetIndexElement(pathSpecification); + if (indexElement == null) { fragments.Add(string.Format("{0}{1}
    • ", elementStartTag, this.Name)); } else { - if(this.IsRoot) + if (this.IsRoot) { - fragments.Add(indexElement.PerformGenerateToCFragment(navigatedPath, relativePathToRoot, maxLevel)); + fragments.Add(indexElement.PerformGenerateToCFragment(navigatedPath, relativePathToRoot, pathSpecification)); } else { - fragments.Add(string.Format("{0}{3}", elementStartTag, relativePathToRoot, HttpUtility.UrlPathEncode(indexElement.TargetURL), - this.Name)); + fragments.Add(string.Format("{0}{3}", + elementStartTag, relativePathToRoot, indexElement.GetFinalTargetUrl(pathSpecification), this.Name)); } } // then the elements in the container. Index elements are skipped here. - foreach(var element in this.Value) + foreach (var element in this.Value) { - fragments.Add(element.GenerateToCFragment(navigatedPath, relativePathToRoot, maxLevel)); + fragments.Add(element.GenerateToCFragment(navigatedPath, relativePathToRoot, pathSpecification)); } fragments.Add("
    "); } else { // just a link - fragments.Add(string.Format(" {2}", - relativePathToRoot, HttpUtility.UrlPathEncode(this.TargetURL), this.Name)); + fragments.Add(string.Format(" {2}", + relativePathToRoot, this.GetFinalTargetUrl(pathSpecification), this.Name)); } - if(!this.IsRoot) + if (!this.IsRoot) { fragments.Add("
  • "); } @@ -168,48 +205,178 @@ namespace Docnet } - #region Properties - public override string TargetURL + private NavigationLevel CreateGeneratedLevel(string path) { - get + var root = new NavigationLevel(_rootDirectory) + { + ParentContainer = this, + IsAutoGenerated = true + }; + + foreach (var mdFile in Directory.GetFiles(path, "*.md", SearchOption.TopDirectoryOnly)) { - var defaultElement = this.IndexElement; - if(defaultElement == null) + var name = FindTitleInMdFile(mdFile); + if (string.IsNullOrWhiteSpace(name)) { - return string.Empty; + continue; } - return defaultElement.TargetURL ?? string.Empty; + + var item = new SimpleNavigationElement + { + Name = name, + Value = Path.Combine(Utils.MakeRelativePath(_rootDirectory, path), Path.GetFileName(mdFile)), + ParentContainer = root, + IsAutoGenerated = true + }; + + root.Value.Add(item); + } + + foreach (var directory in Directory.GetDirectories(path, "*", SearchOption.TopDirectoryOnly)) + { + var subDirectoryNavigationElement = CreateGeneratedLevel(directory); + subDirectoryNavigationElement.Name = new DirectoryInfo(directory).Name; + subDirectoryNavigationElement.ParentContainer = root; + + root.Value.Add(subDirectoryNavigationElement); } + + return root; } - public SimpleNavigationElement IndexElement + private string FindTitleInMdFile(string path) { - get + var title = string.Empty; + + using (var fileStream = File.OpenRead(path)) { - var toReturn = this.Value.FirstOrDefault(e => e.IsIndexElement) as SimpleNavigationElement; - if(toReturn == null) + using (var streamReader = new StreamReader(fileStream)) { - // no index element, add an artificial one. - var path = string.Empty; - if(this.ParentContainer != null) + var line = string.Empty; + while (string.IsNullOrWhiteSpace(line)) { - path = Path.GetDirectoryName(this.ParentContainer.TargetURL); + 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; + } + } } - var nameToUse = this.Name.Replace(".", "").Replace('/', '_').Replace("\\", "_").Replace(":", "").Replace(" ", ""); - if(string.IsNullOrWhiteSpace(nameToUse)) + } + } + + return title; + } + + public override string GetTargetURL(PathSpecification pathSpecification) + { + var defaultElement = this.GetIndexElement(pathSpecification); + if (defaultElement == null) + { + return string.Empty; + } + + return defaultElement.GetTargetURL(pathSpecification) ?? string.Empty; + } + + public SimpleNavigationElement GetIndexElement(PathSpecification pathSpecification) + { + var toReturn = this.Value.FirstOrDefault(e => e.IsIndexElement) as SimpleNavigationElement; + if (toReturn == null) + { + // no index element, add an artificial one. + var path = string.Empty; + + // Don't check parents when using relative paths since we need to walk the tree manually + if (pathSpecification == PathSpecification.Full) + { + if (this.ParentContainer != null) { - return null; + path = Path.GetDirectoryName(this.ParentContainer.GetTargetURL(pathSpecification)); } - toReturn = new SimpleNavigationElement() {ParentContainer = this, Value = string.Format("{0}{1}.md", path, nameToUse), Name = this.Name, IsIndexElement = true}; - this.Value.Add(toReturn); } - return toReturn; + var nameToUse = this.Name.Replace(".", "").Replace('/', '_').Replace("\\", "_").Replace(":", "").Replace(" ", ""); + if (string.IsNullOrWhiteSpace(nameToUse)) + { + return null; + } + + var value = string.Format("{0}{1}.md", path, nameToUse); + + switch (pathSpecification) + { + case PathSpecification.Full: + // Default is correct + break; + + case PathSpecification.Relative: + case PathSpecification.RelativeAsFolder: + if (!IsRoot) + { + string preferredPath = null; + + // We're making a big assumption here, but we can get the first page and assume it's + // in the right folder. + + // Find first (simple) child and use 1 level up. A SimpleNavigationElement mostly represents a folder + // thus is excellent to be used as a folder name + var firstSimpleChildPage = (SimpleNavigationElement) this.Value.FirstOrDefault(x => x is SimpleNavigationElement && !ReferenceEquals(this, x)); + if (firstSimpleChildPage != null) + { + preferredPath = Path.GetDirectoryName(firstSimpleChildPage.Value); + } + else + { + // This is representing an empty folder. Search for first child navigation that has real childs, + // then retrieve the path by going levels up. + var firstChildNavigationLevel = (NavigationLevel)this.Value.FirstOrDefault(x => x is NavigationLevel && ((NavigationLevel)x).Value.Any() && !ReferenceEquals(this, x)); + if (firstChildNavigationLevel != null) + { + var targetUrl = firstChildNavigationLevel.Value.First().GetTargetURL(pathSpecification); + + // 3 times since we need 2 parents up + preferredPath = Path.GetDirectoryName(targetUrl); + preferredPath = Path.GetDirectoryName(preferredPath); + preferredPath = Path.GetDirectoryName(preferredPath); + } + } + + if (!string.IsNullOrWhiteSpace(preferredPath)) + { + value = Path.Combine(preferredPath, "index.md"); + } + } + break; + + default: + throw new ArgumentOutOfRangeException(nameof(pathSpecification), pathSpecification, null); + } + + toReturn = new SimpleNavigationElement + { + ParentContainer = this, + Value = value, + Name = this.Name, + IsIndexElement = true + }; + + this.Value.Add(toReturn); } - } + return toReturn; + } + #region Properties /// /// Gets / sets a value indicating whether this element is the __index element /// @@ -217,7 +384,8 @@ namespace Docnet { // never an index get { return false; } - set { + set + { // nop; } } diff --git a/src/DocNet/PathSpecification.cs b/src/DocNet/PathSpecification.cs new file mode 100644 index 0000000..254c664 --- /dev/null +++ b/src/DocNet/PathSpecification.cs @@ -0,0 +1,11 @@ +namespace Docnet +{ + public enum PathSpecification + { + Full, + + Relative, + + RelativeAsFolder + } +} \ No newline at end of file diff --git a/src/DocNet/Properties/AssemblyInfo.cs b/src/DocNet/Properties/AssemblyInfo.cs index 4b7ae3c..03e610a 100644 --- a/src/DocNet/Properties/AssemblyInfo.cs +++ b/src/DocNet/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.15.0.0")] -[assembly: AssemblyFileVersion("0.15.0")] +[assembly: AssemblyVersion("0.16.0.0")] +[assembly: AssemblyFileVersion("0.16.0")] diff --git a/src/DocNet/SimpleNavigationElement.cs b/src/DocNet/SimpleNavigationElement.cs index 6ecf0bd..db9df9b 100644 --- a/src/DocNet/SimpleNavigationElement.cs +++ b/src/DocNet/SimpleNavigationElement.cs @@ -27,7 +27,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web; -using MarkdownDeep; namespace Docnet { @@ -35,14 +34,13 @@ namespace Docnet { #region Members private string _targetURLForHTML; - - private readonly List _relativeLinksOnPage; // first element in Tuple is anchor name, second is name for ToC. + private List> _relativeH2LinksOnPage; // first element in Tuple is anchor name, second is name for ToC. #endregion public SimpleNavigationElement() { - _relativeLinksOnPage = new List(); + _relativeH2LinksOnPage = new List>(); } @@ -51,53 +49,56 @@ namespace Docnet /// /// 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) + /// The path specification. + /// + public override void GenerateOutput(Config activeConfig, NavigatedPath activePath, PathSpecification pathSpecification) { // 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); } - _relativeLinksOnPage.Clear(); + _relativeH2LinksOnPage.Clear(); var sourceFile = Utils.MakeAbsolutePath(activeConfig.Source, this.Value); - var destinationFile = Utils.MakeAbsolutePath(activeConfig.Destination, this.TargetURL); + var destinationFile = Utils.MakeAbsolutePath(activeConfig.Destination, this.GetTargetURL(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, _relativeLinksOnPage, activeConfig.ConvertLocalLinks); + content = Utils.ConvertMarkdownToHtml(content, Path.GetDirectoryName(destinationFile), activeConfig.Destination, sourceFile, _relativeH2LinksOnPage, 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.AppendFormat("* [{0}]({1}{2}){3}", sibling.Name, relativePathToRoot, + sibling.GetFinalTargetUrl(pathSpecification), Environment.NewLine); } defaultMarkdown.Append(Environment.NewLine); - content = Utils.ConvertMarkdownToHtml(defaultMarkdown.ToString(), Path.GetDirectoryName(destinationFile), activeConfig.Destination, string.Empty, _relativeLinksOnPage, activeConfig.ConvertLocalLinks); + content = Utils.ConvertMarkdownToHtml(defaultMarkdown.ToString(), Path.GetDirectoryName(destinationFile), activeConfig.Destination, string.Empty, _relativeH2LinksOnPage, 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)); } @@ -109,17 +110,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, activeConfig.MaxLevelInToC)); + 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("{{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(); } @@ -131,14 +132,15 @@ namespace Docnet /// /// The collected entries. /// The active path currently navigated. - public override void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath) + /// The path specification. + public override void CollectSearchIndexEntries(List collectedEntries, NavigatedPath activePath, PathSpecification pathSpecification) { 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); + toAdd.Fill(this.MarkdownFromFile, this.GetTargetURL(pathSpecification), this.Name, activePath); collectedEntries.Add(toAdd); } activePath.Pop(); @@ -150,17 +152,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. + /// The path specification. /// - public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel) + public override string GenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, PathSpecification pathSpecification) { // index elements are rendered in the parent container. - if (this.IsIndexElement) + if(this.IsIndexElement) { return string.Empty; } - return PerformGenerateToCFragment(navigatedPath, relativePathToRoot, maxLevel, null); + return PerformGenerateToCFragment(navigatedPath, relativePathToRoot, pathSpecification); } @@ -170,17 +172,16 @@ namespace Docnet /// /// The navigated path. /// The relative path to root. - /// The maximum level. - /// The parent heading. + /// The path specification. /// - public string PerformGenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel) + public string PerformGenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, PathSpecification pathSpecification) { // 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"; @@ -189,71 +190,60 @@ namespace Docnet string.IsNullOrWhiteSpace(liClass) ? string.Empty : string.Format(" class=\"{0}\"", liClass), string.IsNullOrWhiteSpace(aClass) ? string.Empty : string.Format(" class=\"{0}\"", aClass), relativePathToRoot, - HttpUtility.UrlPathEncode(this.TargetURL), + this.GetFinalTargetUrl(pathSpecification), this.Name)); - - if (isCurrent) + if(isCurrent && _relativeH2LinksOnPage.Any()) { - var content = PerformGenerateToCFragment(navigatedPath, relativePathToRoot, maxLevel, null); - if (!string.IsNullOrWhiteSpace(content)) + // generate relative links + fragments.Add(string.Format("
      ", this.ParentContainer.IsRoot ? "currentrelativeroot" : "currentrelative")); + foreach(var p in _relativeH2LinksOnPage) { - fragments.Add(content); + fragments.Add(string.Format("
    • {1}
    • ", p.Item1, p.Item2)); } + fragments.Add("
    "); + } + else + { + fragments.Add(""); } - - fragments.Add(""); - return string.Join(Environment.NewLine, fragments.ToArray()); } - private string PerformGenerateToCFragment(NavigatedPath navigatedPath, string relativePathToRoot, int maxLevel, Heading parentHeading) + /// + /// Gets the target URL with respect to the . + /// + /// The path specification. + /// + /// + public override string GetTargetURL(PathSpecification pathSpecification) { - 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) + if (_targetURLForHTML == null) { - fragments.Add(string.Format("
      ", this.ParentContainer.IsRoot ? "currentrelativeroot" : "currentrelative")); + _targetURLForHTML = (this.Value ?? string.Empty); - // generate relative links - foreach (var heading in includedHeadings) - { - fragments.Add(string.Format("
    • {1}
    • ", heading.Id, heading.Name)); + var toReplace = ".md"; + var replacement = ".htm"; - var headingContent = PerformGenerateToCFragment(navigatedPath, relativePathToRoot, maxLevel, heading); - if (!string.IsNullOrWhiteSpace(headingContent)) + if (pathSpecification == PathSpecification.RelativeAsFolder) + { + if (!IsIndexElement && !_targetURLForHTML.EndsWith("index.md", StringComparison.InvariantCultureIgnoreCase)) { - fragments.Add(headingContent); + replacement = "/index.htm"; } } - fragments.Add("
    "); - } - - return string.Join(Environment.NewLine, fragments.ToArray()); - } - - - #region Properties - public override string TargetURL - { - get - { - if (_targetURLForHTML == null) + if (_targetURLForHTML.EndsWith(toReplace, StringComparison.InvariantCultureIgnoreCase)) { - _targetURLForHTML = (this.Value ?? string.Empty); - if (_targetURLForHTML.ToLowerInvariant().EndsWith(".md")) - { - _targetURLForHTML = _targetURLForHTML.Substring(0, _targetURLForHTML.Length - 3) + ".htm"; - } - _targetURLForHTML = _targetURLForHTML.Replace("\\", "/"); + _targetURLForHTML = _targetURLForHTML.Substring(0, _targetURLForHTML.Length - toReplace.Length) + replacement; } - return _targetURLForHTML; + + _targetURLForHTML = _targetURLForHTML.Replace("\\", "/"); } - } + return _targetURLForHTML; + } + #region Properties /// /// Gets / sets a value indicating whether this element is the __index element /// diff --git a/src/MarkdownDeep/MardownDeep.cs b/src/MarkdownDeep/MardownDeep.cs index 0e2eaa9..d851caf 100644 --- a/src/MarkdownDeep/MardownDeep.cs +++ b/src/MarkdownDeep/MardownDeep.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; @@ -281,8 +282,10 @@ namespace MarkdownDeep } // Override to supply the size of an image - public virtual bool OnGetImageSize(string url, bool TitledImage, out int width, out int height) + public virtual bool OnGetImageSize(string url, bool TitledImage, out int width, out int height, out string finalUrl) { + finalUrl = url; + if (GetImageSizeFunc != null) { var info = new ImageInfo() { url = url, titled_image=TitledImage }; @@ -316,30 +319,51 @@ namespace MarkdownDeep url=url.Substring(1); } - str=str + "\\" + url.Replace("/", "\\"); + var success = false; + // Because PathSpecification.RelativeAsFolder creates an additional layer of directories, + // this trial & error code was implemented to ensure that images could be found + var count = 0; + while (count < 2) + { + //Create an image object from the uploaded file + try + { + var fileName = str + "\\"; + var currentUrl = url; - // + for (int i = 0; i < count; i++) + { + currentUrl = "../" + currentUrl; + } - //Create an image object from the uploaded file - try - { - var img = System.Drawing.Image.FromFile(str); - width=img.Width; - height=img.Height; + fileName += currentUrl.Replace("/", "\\"); + + if (File.Exists(fileName)) + { + var img = System.Drawing.Image.FromFile(fileName); + width = img.Width; + height = img.Height; + finalUrl = currentUrl; + + if (MaxImageWidth != 0 && width > MaxImageWidth) + { + height = (int)((double)height * (double)MaxImageWidth / (double)width); + width = MaxImageWidth; + } - if (MaxImageWidth != 0 && width>MaxImageWidth) + success = true; + break; + } + } + catch (Exception) { - height=(int)((double)height * (double)MaxImageWidth / (double)width); - width=MaxImageWidth; } - return true; - } - catch (Exception) - { - return false; + count++; } + + return success; } @@ -388,9 +412,11 @@ namespace MarkdownDeep } // Try to determine width and height + var url = tag.attributes["src"]; int width, height; - if (OnGetImageSize(tag.attributes["src"], TitledImage, out width, out height)) + if (OnGetImageSize(url, TitledImage, out width, out height, out url)) { + tag.attributes["src"] = url; tag.attributes["width"] = width.ToString(); tag.attributes["height"] = height.ToString(); }