diff --git a/src/DocNet/Config.cs b/src/DocNet/Config.cs index f67046f..8fcbcee 100644 --- a/src/DocNet/Config.cs +++ b/src/DocNet/Config.cs @@ -180,7 +180,10 @@ namespace Docnet searchSimpleElement.ExtraScriptProducerFunc = (e,c,n) => @" "; - searchSimpleElement.GenerateOutput(this, activePath, navigationContext); + + // Force custom navigation context because this should end up in the root + searchSimpleElement.GenerateOutput(this, activePath, new NavigationContext(PathSpecification.Full, UrlFormatting.None, 0, false)); + activePath.Pop(); } diff --git a/src/DocNet/INavigationElementExtensions.cs b/src/DocNet/INavigationElementExtensions.cs index 09579aa..d10f976 100644 --- a/src/DocNet/INavigationElementExtensions.cs +++ b/src/DocNet/INavigationElementExtensions.cs @@ -16,12 +16,23 @@ namespace Docnet public static string GetFinalTargetUrl(this INavigationElement navigationElement, NavigationContext navigationContext) { var targetUrl = navigationElement.GetTargetURL(navigationContext); + return GetFinalTargetUrl(targetUrl, navigationContext); + } + + /// + /// Gets the final URL by encoding the path and by removing the filename if it equals index.htm. + /// + /// The target URL. + /// The navigation context. + /// + public static string GetFinalTargetUrl(this string targetUrl, NavigationContext navigationContext) + { var link = HttpUtility.UrlPathEncode(targetUrl); if (navigationContext.StripIndexHtm) { if (link.Length > IndexHtmFileName.Length && - link.EndsWith(IndexHtmFileName, StringComparison.InvariantCultureIgnoreCase)) + link.EndsWith(IndexHtmFileName, StringComparison.InvariantCultureIgnoreCase)) { link = link.Substring(0, link.Length - IndexHtmFileName.Length); } diff --git a/src/DocNet/NotFoundNavigationElement.cs b/src/DocNet/NotFoundNavigationElement.cs index 4545ebd..9d6f1b1 100644 --- a/src/DocNet/NotFoundNavigationElement.cs +++ b/src/DocNet/NotFoundNavigationElement.cs @@ -42,7 +42,8 @@ namespace Docnet var destinationFile = Utils.MakeAbsolutePath(config.Destination, this.GetTargetURL(navigationContext)); var htmlContent = Utils.ConvertMarkdownToHtml(markdownContent, Path.GetDirectoryName(destinationFile), config.Destination, - string.Empty, new List(), config.ConvertLocalLinks); + string.Empty, new List(), config.ConvertLocalLinks, + new NavigationContext(config.PathSpecification, config.UrlFormatting, config.MaxLevelInToC, config.StripIndexHtm)); return htmlContent; } diff --git a/src/DocNet/SimpleNavigationElement.cs b/src/DocNet/SimpleNavigationElement.cs index e8722db..d80f2ed 100644 --- a/src/DocNet/SimpleNavigationElement.cs +++ b/src/DocNet/SimpleNavigationElement.cs @@ -72,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, _relativeLinksOnPage, activeConfig.ConvertLocalLinks); + content = Utils.ConvertMarkdownToHtml(content, Path.GetDirectoryName(destinationFile), activeConfig.Destination, sourceFile, _relativeLinksOnPage, activeConfig.ConvertLocalLinks, navigationContext); } else { @@ -95,7 +95,7 @@ namespace Docnet sibling.GetFinalTargetUrl(navigationContext), 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, _relativeLinksOnPage, activeConfig.ConvertLocalLinks, navigationContext); } else { @@ -112,7 +112,7 @@ 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("{{RelativeSourceFileName}}", Utils.MakeRelativePathForUri(activeConfig.Source, sourceFile).TrimEnd('/')); sb.Replace("{{RelativeTargetFileName}}", Utils.MakeRelativePathForUri(activeConfig.Destination, destinationFile).TrimEnd('/')); sb.Replace("{{Breadcrumbs}}", activePath.CreateBreadCrumbsHTML(relativePathToRoot, navigationContext)); sb.Replace("{{ToC}}", activePath.CreateToCHTML(relativePathToRoot, navigationContext)); @@ -226,29 +226,7 @@ namespace Docnet { if (_targetURLForHTML == null) { - var toReplace = "mdext"; - var replacement = ".htm"; - - var value = (this.Value ?? string.Empty); - - // Replace with custom extension because url formatting might optimize the extension - value = value.Replace(".md", toReplace); - _targetURLForHTML = value.ApplyUrlFormatting(navigationContext.UrlFormatting); - - if (navigationContext.PathSpecification == PathSpecification.RelativeAsFolder) - { - if (!IsIndexElement && !_targetURLForHTML.EndsWith($"index{toReplace}", StringComparison.InvariantCultureIgnoreCase)) - { - replacement = "/index.htm"; - } - } - - if (_targetURLForHTML.EndsWith(toReplace, StringComparison.InvariantCultureIgnoreCase)) - { - _targetURLForHTML = _targetURLForHTML.Substring(0, _targetURLForHTML.Length - toReplace.Length) + replacement; - } - - _targetURLForHTML = _targetURLForHTML.Replace("\\", "/"); + _targetURLForHTML = Utils.ResolveTargetURL(this.Value ?? string.Empty, IsIndexElement, navigationContext); } return _targetURLForHTML; diff --git a/src/DocNet/StringExtensions.cs b/src/DocNet/StringExtensions.cs index 502f768..98d8122 100644 --- a/src/DocNet/StringExtensions.cs +++ b/src/DocNet/StringExtensions.cs @@ -37,6 +37,11 @@ namespace Docnet for(var i = 0; i < splitted.Length; i++) { var splittedValue = splitted[i]; + if (string.Equals(splittedValue, ".") || string.Equals(splittedValue, "..")) + { + continue; + } + splittedValue = regEx.Replace(splittedValue, replacementValue).Replace(" ", replacementValue); if (!string.IsNullOrEmpty(replacementValue)) diff --git a/src/DocNet/UrlFormatting.cs b/src/DocNet/UrlFormatting.cs index 0953938..b4c7597 100644 --- a/src/DocNet/UrlFormatting.cs +++ b/src/DocNet/UrlFormatting.cs @@ -1,9 +1,9 @@ namespace Docnet { - public enum UrlFormatting - { + public enum UrlFormatting + { None, - Strip, - Dashes - } + Strip, + Dashes + } } \ No newline at end of file diff --git a/src/DocNet/Utils.cs b/src/DocNet/Utils.cs index 1aefc4e..5befa1b 100644 --- a/src/DocNet/Utils.cs +++ b/src/DocNet/Utils.cs @@ -38,34 +38,70 @@ namespace Docnet /// Regex expression used to parse @@include(filename.html) tag. /// private static Regex includeRegex = new Regex(@"@@include\((.*)\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - #endregion - - /// - /// Converts the markdown to HTML. - /// - /// The markdown string to convert. - /// The document path (without the document filename). - /// The site root. - /// the filename of the source markdown file - /// The created anchor collector, for ToC sublinks for H2 headers. - /// 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) + #endregion + + /// + /// Converts the markdown to HTML. + /// + /// The markdown string to convert. + /// The document path (without the document filename). + /// The site root. + /// the filename of the source markdown file + /// The created anchor collector, for ToC sublinks for H2 headers. + /// if set to true, convert local links to md files to target files. + /// The navigation context. + /// + public static string ConvertMarkdownToHtml(string toConvert, string destinationDocumentPath, string siteRoot, string sourceDocumentFilename, + List createdAnchorCollector, bool convertLocalLinks, NavigationContext navigationContext) { + var localLinkProcessor = new Func(s => + { + var result = s; + + if (!string.IsNullOrWhiteSpace(result)) + { + switch (navigationContext.PathSpecification) + { + case PathSpecification.Full: + break; + + case PathSpecification.Relative: + break; + + case PathSpecification.RelativeAsFolder: + // Step 1: we need to move up 1 additional folder (get out of current subfolder) + var relativeAsFolderIndex = result.StartsWith("./") ? 2 : 0; + result = result.Insert(relativeAsFolderIndex, "../"); + + // Step 2: we need an additional layer to go into (filename is now a folder) + result = ResolveTargetURL(result, false, navigationContext); + + // Step 3: get the final url + result = result.GetFinalTargetUrl(navigationContext); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(navigationContext.PathSpecification), navigationContext.PathSpecification, null); + } + } + + return result; + }); + var parser = new MarkdownDeep.Markdown - { - ExtraMode = true, - GitHubCodeBlocks = true, - AutoHeadingIDs = true, - NewWindowForExternalLinks = true, - DocNetMode = true, - ConvertLocalLinks = convertLocalLinks, - DestinationDocumentLocation = destinationDocumentPath, - DocumentRoot = siteRoot, - SourceDocumentFilename = sourceDocumentFilename, - HtmlClassTitledImages = "figure", - }; + { + ExtraMode = true, + GitHubCodeBlocks = true, + AutoHeadingIDs = true, + NewWindowForExternalLinks = true, + DocNetMode = true, + ConvertLocalLinks = convertLocalLinks, + LocalLinkProcessor = localLinkProcessor, + DestinationDocumentLocation = destinationDocumentPath, + DocumentRoot = siteRoot, + SourceDocumentFilename = sourceDocumentFilename, + HtmlClassTitledImages = "figure", + }; var toReturn = parser.Transform(toConvert); @@ -74,6 +110,33 @@ namespace Docnet return toReturn; } + public static string ResolveTargetURL(string sourceFileName, bool isIndexElement, NavigationContext navigationContext) + { + var toReplace = "mdext"; + var replacement = ".htm"; + + var value = sourceFileName; + + // Replace with custom extension because url formatting might optimize the extension + value = value.Replace(".md", toReplace); + var targetUrl = value.ApplyUrlFormatting(navigationContext.UrlFormatting); + + if (navigationContext.PathSpecification == PathSpecification.RelativeAsFolder) + { + if (!isIndexElement && !targetUrl.EndsWith($"index{toReplace}", StringComparison.InvariantCultureIgnoreCase)) + { + replacement = "/index.htm"; + } + } + + if (targetUrl.EndsWith(toReplace, StringComparison.InvariantCultureIgnoreCase)) + { + targetUrl = targetUrl.Substring(0, targetUrl.Length - toReplace.Length) + replacement; + } + + targetUrl = targetUrl.Replace("\\", "/"); + return targetUrl; + } /// /// Copies directories and files, eventually recursively. From MSDN. @@ -85,26 +148,26 @@ namespace Docnet { // Get the subdirectories for the specified directory. DirectoryInfo sourceFolder = new DirectoryInfo(sourceFolderName); - if(!sourceFolder.Exists) + if (!sourceFolder.Exists) { throw new DirectoryNotFoundException("Source directory does not exist or could not be found: " + sourceFolderName); } DirectoryInfo[] sourceFoldersToCopy = sourceFolder.GetDirectories(); // If the destination directory doesn't exist, create it. - if(!Directory.Exists(destinationFolderName)) + if (!Directory.Exists(destinationFolderName)) { Directory.CreateDirectory(destinationFolderName); } // Get the files in the directory and copy them to the new location. - foreach(FileInfo file in sourceFolder.GetFiles()) + foreach (FileInfo file in sourceFolder.GetFiles()) { file.CopyTo(Path.Combine(destinationFolderName, file.Name), true); } - if(copySubFolders) + if (copySubFolders) { - foreach(DirectoryInfo subFolder in sourceFoldersToCopy) + foreach (DirectoryInfo subFolder in sourceFoldersToCopy) { Utils.DirectoryCopy(subFolder.FullName, Path.Combine(destinationFolderName, subFolder.Name), copySubFolders); } @@ -121,11 +184,11 @@ namespace Docnet /// public static string MakeAbsolutePath(string rootPath, string toMakeAbsolute) { - if(string.IsNullOrWhiteSpace(toMakeAbsolute)) + if (string.IsNullOrWhiteSpace(toMakeAbsolute)) { return rootPath; } - if(Path.IsPathRooted(toMakeAbsolute)) + if (Path.IsPathRooted(toMakeAbsolute)) { return toMakeAbsolute; } @@ -141,12 +204,12 @@ namespace Docnet public static void CreateFoldersIfRequired(string fullPath) { string folderToCheck = Path.GetDirectoryName(fullPath); - if(string.IsNullOrWhiteSpace(folderToCheck)) + if (string.IsNullOrWhiteSpace(folderToCheck)) { // nothing to do, no folder to emit return; } - if(!Directory.Exists(folderToCheck)) + if (!Directory.Exists(folderToCheck)) { Directory.CreateDirectory(folderToCheck); } @@ -163,20 +226,20 @@ namespace Docnet public static string MakeRelativePath(string fromPath, string toPath) { var fromPathToUse = fromPath; - if(string.IsNullOrEmpty(fromPathToUse)) + if (string.IsNullOrEmpty(fromPathToUse)) { return string.Empty; } var toPathToUse = toPath; - if(string.IsNullOrEmpty(toPathToUse)) + if (string.IsNullOrEmpty(toPathToUse)) { return string.Empty; } - if(fromPathToUse.Last() != Path.DirectorySeparatorChar) + if (fromPathToUse.Last() != Path.DirectorySeparatorChar) { fromPathToUse += Path.DirectorySeparatorChar; } - if(toPathToUse.Last() != Path.DirectorySeparatorChar) + if (toPathToUse.Last() != Path.DirectorySeparatorChar) { toPathToUse += Path.DirectorySeparatorChar; } @@ -184,7 +247,7 @@ namespace Docnet var fromUri = new Uri(Uri.UnescapeDataString(Path.GetFullPath(fromPathToUse))); var toUri = new Uri(Uri.UnescapeDataString(Path.GetFullPath(toPathToUse))); - if(fromUri.Scheme != toUri.Scheme) + if (fromUri.Scheme != toUri.Scheme) { // path can't be made relative. return toPathToUse; @@ -193,7 +256,7 @@ namespace Docnet var relativeUri = fromUri.MakeRelativeUri(toUri); string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); - if(toUri.Scheme.ToUpperInvariant() == "FILE") + if (toUri.Scheme.ToUpperInvariant() == "FILE") { relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); } @@ -242,5 +305,5 @@ namespace Docnet } return content; } - } + } } diff --git a/src/MarkdownDeep/LinkDefinition.cs b/src/MarkdownDeep/LinkDefinition.cs index e5f74a5..198764a 100644 --- a/src/MarkdownDeep/LinkDefinition.cs +++ b/src/MarkdownDeep/LinkDefinition.cs @@ -58,24 +58,32 @@ namespace MarkdownDeep { HtmlTag tag = new HtmlTag("a"); - var url = this.Url; - - if (m.DocNetMode && m.ConvertLocalLinks) - { - // A few requirements before we can convert local links: - // 1. Link contains .md - // 2. Link is relative - // 3. Link is included in the index - var index = url.LastIndexOf(".md", StringComparison.OrdinalIgnoreCase); - if (index >= 0) - { - Uri uri; - if(Uri.TryCreate(url, UriKind.Relative, out uri)) + var url = this.Url; + + if (m.DocNetMode && m.ConvertLocalLinks) + { + // A few requirements before we can convert local links: + // 1. Link contains .md + // 2. Link is relative + // 3. Link is included in the index + var index = url.LastIndexOf(".md", StringComparison.OrdinalIgnoreCase); + if (index >= 0) + { + var linkProcessor = m.LocalLinkProcessor; + if (linkProcessor != null) + { + url = linkProcessor(url); + } + else { - url = String.Concat(url.Substring(0, index), ".htm", url.Substring(index + ".md".Length)); + Uri uri; + if (Uri.TryCreate(url, UriKind.Relative, out uri)) + { + url = String.Concat(url.Substring(0, index), ".htm", url.Substring(index + ".md".Length)); + } } - } - } + } + } // encode url StringBuilder sb = m.GetStringBuilder(); @@ -83,14 +91,14 @@ namespace MarkdownDeep tag.attributes["href"] = sb.ToString(); // encode title - if (!String.IsNullOrEmpty(this.Title )) + if (!String.IsNullOrEmpty(this.Title)) { sb.Length = 0; Utils.SmartHtmlEncodeAmpsAndAngles(sb, this.Title); tag.attributes["title"] = sb.ToString(); } - if(specialAttributes.Any()) + if (specialAttributes.Any()) { LinkDefinition.HandleSpecialAttributes(specialAttributes, sb, tag); } @@ -101,7 +109,7 @@ namespace MarkdownDeep // Render the opening tag tag.RenderOpening(b); - b.Append(link_text); // Link text already escaped by SpanFormatter + b.Append(link_text); // Link text already escaped by SpanFormatter b.Append(""); } } @@ -131,7 +139,7 @@ namespace MarkdownDeep Utils.SmartHtmlEncodeAmpsAndAngles(sb, Title); tag.attributes["title"] = sb.ToString(); } - if(specialAttributes.Any()) + if (specialAttributes.Any()) { LinkDefinition.HandleSpecialAttributes(specialAttributes, sb, tag); } @@ -153,9 +161,9 @@ namespace MarkdownDeep // Parse a link definition internal static LinkDefinition ParseLinkDefinition(StringScanner p, bool ExtraMode) { - int savepos=p.Position; + int savepos = p.Position; var l = ParseLinkDefinitionInternal(p, ExtraMode); - if (l==null) + if (l == null) p.Position = savepos; return l; @@ -181,7 +189,7 @@ namespace MarkdownDeep return null; // Parse the url and title - var link=ParseLinkTarget(p, id, ExtraMode); + var link = ParseLinkTarget(p, id, ExtraMode); // and trailing whitespace p.SkipLinespace(); @@ -236,7 +244,7 @@ namespace MarkdownDeep int paren_depth = 1; while (!p.Eol) { - char ch=p.Current; + char ch = p.Current; if (char.IsWhiteSpace(ch)) break; if (id == null) @@ -246,7 +254,7 @@ namespace MarkdownDeep else if (ch == ')') { paren_depth--; - if (paren_depth==0) + if (paren_depth == 0) break; } } @@ -275,7 +283,7 @@ namespace MarkdownDeep char delim; switch (p.Current) { - case '\'': + case '\'': case '\"': delim = p.Current; break; @@ -349,27 +357,27 @@ namespace MarkdownDeep private static void HandleSpecialAttributes(List specialAttributes, StringBuilder sb, HtmlTag tag) { string id = specialAttributes.FirstOrDefault(s => s.StartsWith("#")); - if(id != null && id.Length > 1) + if (id != null && id.Length > 1) { sb.Length = 0; Utils.SmartHtmlEncodeAmpsAndAngles(sb, id.Substring(1)); tag.attributes["id"] = sb.ToString(); } var cssClasses = new List(); - foreach(var cssClass in specialAttributes.Where(s => s.StartsWith(".") && s.Length > 1)) + foreach (var cssClass in specialAttributes.Where(s => s.StartsWith(".") && s.Length > 1)) { sb.Length = 0; Utils.SmartHtmlEncodeAmpsAndAngles(sb, cssClass.Substring(1)); cssClasses.Add(sb.ToString()); } - if(cssClasses.Any()) + if (cssClasses.Any()) { tag.attributes["class"] = string.Join(" ", cssClasses.ToArray()); } - foreach(var nameValuePair in specialAttributes.Where(s => s.Contains("=") && s.Length > 2 && !s.StartsWith(".") && !s.StartsWith("#"))) + foreach (var nameValuePair in specialAttributes.Where(s => s.Contains("=") && s.Length > 2 && !s.StartsWith(".") && !s.StartsWith("#"))) { var pair = nameValuePair.Split('='); - if(pair.Length == 2) + if (pair.Length == 2) { sb.Length = 0; Utils.SmartHtmlEncodeAmpsAndAngles(sb, pair[0]); diff --git a/src/MarkdownDeep/MarkdownDeep.cs b/src/MarkdownDeep/MarkdownDeep.cs index d851caf..8f83ff9 100644 --- a/src/MarkdownDeep/MarkdownDeep.cs +++ b/src/MarkdownDeep/MarkdownDeep.cs @@ -880,6 +880,14 @@ namespace MarkdownDeep /// public bool ConvertLocalLinks { get; set; } + /// + /// Gets or sets the local link processor allowing customization of the local links before being transformed. + /// + /// + /// The local link processor. + /// + public Func LocalLinkProcessor { get; set; } + // When set, all html block level elements automatically support // markdown syntax within them. // (Similar to Pandoc's handling of markdown in html)