Browse Source

Respect PathSpecification when resolving relative urls in the source files to ensure they don't result in 404

pull/78/head
Geert van Horrik 7 years ago
parent
commit
b07c2a0a09
8 changed files with 179 additions and 105 deletions
  1. +12
    -1
      src/DocNet/INavigationElementExtensions.cs
  2. +2
    -1
      src/DocNet/NotFoundNavigationElement.cs
  3. +3
    -25
      src/DocNet/SimpleNavigationElement.cs
  4. +5
    -0
      src/DocNet/StringExtensions.cs
  5. +5
    -5
      src/DocNet/UrlFormatting.cs
  6. +105
    -42
      src/DocNet/Utils.cs
  7. +39
    -31
      src/MarkdownDeep/LinkDefinition.cs
  8. +8
    -0
      src/MarkdownDeep/MarkdownDeep.cs

+ 12
- 1
src/DocNet/INavigationElementExtensions.cs View File

@@ -16,12 +16,23 @@ namespace Docnet
public static string GetFinalTargetUrl(this INavigationElement navigationElement, NavigationContext navigationContext)
{
var targetUrl = navigationElement.GetTargetURL(navigationContext);
return GetFinalTargetUrl(targetUrl, navigationContext);
}

/// <summary>
/// Gets the final URL by encoding the path and by removing the filename if it equals <c>index.htm</c>.
/// </summary>
/// <param name="targetUrl">The target URL.</param>
/// <param name="navigationContext">The navigation context.</param>
/// <returns></returns>
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);
}


+ 2
- 1
src/DocNet/NotFoundNavigationElement.cs View File

@@ -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<Heading>(), config.ConvertLocalLinks);
string.Empty, new List<Heading>(), config.ConvertLocalLinks,
new NavigationContext(config.PathSpecification, config.UrlFormatting, config.MaxLevelInToC, config.StripIndexHtm));
return htmlContent;
}



+ 3
- 25
src/DocNet/SimpleNavigationElement.cs View File

@@ -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
{
@@ -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;


+ 5
- 0
src/DocNet/StringExtensions.cs View File

@@ -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))


+ 5
- 5
src/DocNet/UrlFormatting.cs View File

@@ -1,9 +1,9 @@
namespace Docnet
{
public enum UrlFormatting
{
public enum UrlFormatting
{
None,
Strip,
Dashes
}
Strip,
Dashes
}
}

+ 105
- 42
src/DocNet/Utils.cs View File

@@ -38,34 +38,70 @@ namespace Docnet
/// Regex expression used to parse @@include(filename.html) tag.
/// </summary>
private static Regex includeRegex = new Regex(@"@@include\((.*)\)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
#endregion

/// <summary>
/// Converts the markdown to HTML.
/// </summary>
/// <param name="toConvert">The markdown string to convert.</param>
/// <param name="destinationDocumentPath">The document path (without the document filename).</param>
/// <param name="siteRoot">The site root.</param>
/// <param name="sourceDocumentFilename">the filename of the source markdown file</param>
/// <param name="createdAnchorCollector">The created anchor collector, for ToC sublinks for H2 headers.</param>
/// <param name="convertLocalLinks">if set to <c>true</c>, convert local links to md files to target files.</param>
/// <returns></returns>
public static string ConvertMarkdownToHtml(string toConvert, string destinationDocumentPath, string siteRoot, string sourceDocumentFilename,
List<Heading> createdAnchorCollector, bool convertLocalLinks)
#endregion

/// <summary>
/// Converts the markdown to HTML.
/// </summary>
/// <param name="toConvert">The markdown string to convert.</param>
/// <param name="destinationDocumentPath">The document path (without the document filename).</param>
/// <param name="siteRoot">The site root.</param>
/// <param name="sourceDocumentFilename">the filename of the source markdown file</param>
/// <param name="createdAnchorCollector">The created anchor collector, for ToC sublinks for H2 headers.</param>
/// <param name="convertLocalLinks">if set to <c>true</c>, convert local links to md files to target files.</param>
/// <param name="navigationContext">The navigation context.</param>
/// <returns></returns>
public static string ConvertMarkdownToHtml(string toConvert, string destinationDocumentPath, string siteRoot, string sourceDocumentFilename,
List<Heading> createdAnchorCollector, bool convertLocalLinks, NavigationContext navigationContext)
{
var localLinkProcessor = new Func<string, string>(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;
}

/// <summary>
/// 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
/// <returns></returns>
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;
}
}
}
}

+ 39
- 31
src/MarkdownDeep/LinkDefinition.cs View File

@@ -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("</a>");
}
}
@@ -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<string> 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<string>();
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]);


+ 8
- 0
src/MarkdownDeep/MarkdownDeep.cs View File

@@ -880,6 +880,14 @@ namespace MarkdownDeep
/// </summary>
public bool ConvertLocalLinks { get; set; }

/// <summary>
/// Gets or sets the local link processor allowing customization of the local links before being transformed.
/// </summary>
/// <value>
/// The local link processor.
/// </value>
public Func<string, string> 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)


Loading…
Cancel
Save