diff --git a/src/DocFxTocGenerator/Properties/launchSettings.json b/src/DocFxTocGenerator/Properties/launchSettings.json new file mode 100644 index 0000000..129c698 --- /dev/null +++ b/src/DocFxTocGenerator/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "DocFxTocGenerator": { + "commandName": "Project", + "commandLineArgs": "-d \"c:\\temp\\docfxindex\" --index" + } + } +} \ No newline at end of file diff --git a/src/DocLinkChecker/DocLinkChecker.Test/HyperlinkTests.cs b/src/DocLinkChecker/DocLinkChecker.Test/HyperlinkTests.cs index 57ab102..9a20586 100644 --- a/src/DocLinkChecker/DocLinkChecker.Test/HyperlinkTests.cs +++ b/src/DocLinkChecker/DocLinkChecker.Test/HyperlinkTests.cs @@ -2,6 +2,7 @@ { using Bogus; using DocLinkChecker.Enums; + using DocLinkChecker.Helpers; using DocLinkChecker.Interfaces; using DocLinkChecker.Models; using DocLinkChecker.Services; @@ -11,6 +12,7 @@ using Moq; using Moq.Contrib.HttpClient; using System; + using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -224,21 +226,23 @@ public async void ValidateLocalLinkHeadingShouldNotHaveErrors() service.Errors.Should().BeEmpty(); } - [Fact] - public async void ValidateLocalLinkHeadingInSameDocumentShouldNotHaveErrors() + [Theory] + [InlineData("First header", "first-header", "#first-header")] + [InlineData("`Second header`", "second-header", "#second-header")] + public async void ValidateLocalLinkHeadingInSameDocumentShouldNotHaveErrors(string title, string id, string reference) { // Arrange _config.DocumentationFiles.SourceFolder = _fileServiceMock.Root; string source = $"{_fileServiceMock.Root}\\general\\another-sample.md"; LinkValidatorService service = new LinkValidatorService(_serviceProvider, _config, _fileService, _console); - service.Headings.Add(new(source, 99, 1, "First Header", "first-header")); + service.Headings.Add(new(source, 99, 1, title, id)); //Act int line = 432; int column = 771; - Hyperlink link = new Hyperlink(source, line, column, $"#first-header"); + Hyperlink link = new Hyperlink(source, line, column, reference); await service.VerifyHyperlink(link); // Assert @@ -291,10 +295,64 @@ public async void ValidateLocalLinkWithFullPathShouldHaveErrors() service.Errors.Should().NotBeEmpty(); service.Errors.First().Line.Should().Be(line); service.Errors.First().Column.Should().Be(column); - service.Errors.First().Severity.Should().Be(Enums.MarkdownErrorSeverity.Error); + service.Errors.First().Severity.Should().Be(MarkdownErrorSeverity.Error); service.Errors.First().Message.Should().Contain("Full path not allowed"); } + [Theory] + [InlineData("[!code-csharp[](src/sample.cs)]")] + [InlineData("[!code-csharp[](src/sample.cs#region)]")] + [InlineData("[!code-csharp[](src/sample.cs#L8-11)]")] + [InlineData("[!code-csharp[](src/sample.cs?name=MainLoop)]")] + [InlineData("[!code-csharp[](src/sample.cs?highlight=3,5)]")] + [InlineData("[!INCLUDE [the defined way to include](getting-started/README.md)]")] + [InlineData("[!include [using lowercase to include](getting-started/README.md)]")] + public async void ValidateFileInclusionLinksShouldNotHaveErrors(string codeLink) + { + // Arrange + _config.DocumentationFiles.SourceFolder = _fileServiceMock.Root; + + LinkValidatorService service = new LinkValidatorService(_serviceProvider, _config, _fileService, _console); + + //Act + (List objects, List errors) result = + MarkdownHelper.ParseMarkdownString($"{_fileServiceMock.Root}/start-document.md", codeLink, false); + + Hyperlink link = (Hyperlink)result.objects.FirstOrDefault(result => result is Hyperlink); + await service.VerifyHyperlink(link); + + // Assert + service.Errors.Should().BeEmpty(); + } + + [Theory] + [InlineData("[!code-csharp[](src/sample_NOT_EXISTING.cs)]")] + [InlineData("[!code-csharp[](src/sample_NOT_EXISTING.cs#region)]")] + [InlineData("[!code-csharp[](src/sample_NOT_EXISTING.cs#L8-11)]")] + [InlineData("[!code-csharp[](src/sample_NOT_EXISTING.cs?name=MainLoop)]")] + [InlineData("[!code-csharp[](src/sample_NOT_EXISTING.cs?highlight=3,5)]")] + [InlineData("[!INCLUDE [the defined way to include](getting-started/README_NOT_EXISTING.md)]")] + [InlineData("[!include [using lowercase to include](getting-started/README_NOT_EXISTING.md)]")] + public async void ValidateFileInclusionLinksShouldHaveErrors(string codeLink) + { + // Arrange + _config.DocumentationFiles.SourceFolder = _fileServiceMock.Root; + + LinkValidatorService service = new LinkValidatorService(_serviceProvider, _config, _fileService, _console); + + //Act + (List objects, List errors) result = + MarkdownHelper.ParseMarkdownString($"{_fileServiceMock.Root}/start-document.md", codeLink, false); + + Hyperlink link = (Hyperlink)result.objects.FirstOrDefault(result => result is Hyperlink); + await service.VerifyHyperlink(link); + + // Assert + service.Errors.Should().NotBeEmpty(); + service.Errors.First().Severity.Should().Be(MarkdownErrorSeverity.Error); + service.Errors.First().Message.Should().Contain("Not found"); + } + private ICustomConsoleLogger GetMockedConsoleLogger() { Mock console = new Mock(); diff --git a/src/DocLinkChecker/DocLinkChecker.Test/MarkdownTests.cs b/src/DocLinkChecker/DocLinkChecker.Test/MarkdownTests.cs index 9c713d6..0e5cb3e 100644 --- a/src/DocLinkChecker/DocLinkChecker.Test/MarkdownTests.cs +++ b/src/DocLinkChecker/DocLinkChecker.Test/MarkdownTests.cs @@ -93,6 +93,8 @@ public void FindAllHeadingsWithUnicodeCharacters() .AddHeading("ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789", 2) .AddParagraphs(1) .AddHeading("UNICODE-!@#$%^&*+=~`<>,.?/:;€|Æäßéóčúįǯ-CHARS", 2) + .AddParagraphs(1) + .AddHeading("`Commented header`", 2) .AddParagraphs(1); var result = MarkdownHelper.ParseMarkdownString(string.Empty, markdown, true); @@ -101,10 +103,27 @@ public void FindAllHeadingsWithUnicodeCharacters() .OfType() .ToList(); - headings.Count.Should().Be(4); + headings.Count.Should().Be(5); headings[1].Id.Should().Be("abcdefghijklmnopqrstuvwxyz-0123456789"); headings[2].Id.Should().Be("abcdefghijklmnopqrstuvwxyz-0123456789"); headings[3].Id.Should().Be("unicode-æäßéóčúįǯ-chars"); + headings[4].Id.Should().Be("commented-header"); + } + + [Fact] + public void FindAllFileInclusionLinks() + { + string markdown = string.Empty + .AddHeading("Test file inclusion links", 1) + .AddParagraphs(1).AddLink("!code-csharp[](Program.cs)"); + + var result = MarkdownHelper.ParseMarkdownString(string.Empty, markdown, true); + + var links = result.objects + .OfType() + .ToList(); + + links.Count.Should().Be(1); } [Fact] diff --git a/src/DocLinkChecker/DocLinkChecker.Test/MockFileService.cs b/src/DocLinkChecker/DocLinkChecker.Test/MockFileService.cs index 7c9c15e..e570847 100644 --- a/src/DocLinkChecker/DocLinkChecker.Test/MockFileService.cs +++ b/src/DocLinkChecker/DocLinkChecker.Test/MockFileService.cs @@ -63,6 +63,22 @@ public void FillDemoSet() Files.Add($"{Root}\\general\\images\\nature.jpeg", ""); Files.Add($"{Root}\\general\\images\\another-image.png", ""); + + Files.Add($"{Root}\\src", null); + Files.Add($"{Root}\\src\\sample.cs", @"namespace MySampleApp; + +public class SampleClass +{ + public void SampleMethod() + { + // + foreach(var thing in list) + { + // Do Stuff + } + // + } +}"); } public void DeleteFile(string path) diff --git a/src/DocLinkChecker/DocLinkChecker/Helpers/MarkdownHelper.cs b/src/DocLinkChecker/DocLinkChecker/Helpers/MarkdownHelper.cs index 1979c30..efb4a3b 100644 --- a/src/DocLinkChecker/DocLinkChecker/Helpers/MarkdownHelper.cs +++ b/src/DocLinkChecker/DocLinkChecker/Helpers/MarkdownHelper.cs @@ -1,9 +1,7 @@ namespace DocLinkChecker.Helpers { using System; - using System.Collections; using System.Collections.Generic; - using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -12,7 +10,6 @@ using DocLinkChecker.Models; using Markdig; using Markdig.Extensions.Tables; - using Markdig.Renderers.Html; using Markdig.Syntax; using Markdig.Syntax.Inlines; @@ -61,6 +58,25 @@ public static (List objects, List errors) .ToList(); if (links != null) { + // Support for DocFx specific links. See https://dotnet.github.io/docfx/docs/markdown.html + // File inclusion links and Code references + var filerefs = links.Where(x => x.Url.StartsWith("!code-", StringComparison.OrdinalIgnoreCase) || + x.Url.StartsWith("!INCLUDE", StringComparison.OrdinalIgnoreCase)); + foreach (var fileref in filerefs) + { + string url = Regex.Match(fileref.Url, @"\\((.*?)\\)").Value; + fileref.Url = url; + } + + // Video inclusion links + var videorefs = links.Where(x => x.Url.StartsWith("!Video")); + foreach (var videoref in videorefs) + { + string url = Regex.Match(videoref.Url, @"!Video (.*)").Value; + videoref.Url = url; + videoref.LinkType = HyperlinkType.Webpage; + } + objects.AddRange(links); } @@ -76,6 +92,11 @@ public static (List objects, List errors) { title = markdown.Substring(child.Span.Start, x.Span.Length - (child.Span.Start - x.Span.Start)); } + else if (x.Inline.FirstChild != null) + { + // fallback for complex headers, like "# `text with quotes`" and such + title = markdown.Substring(x.Inline.FirstChild.Span.Start, x.Span.Length - (x.Inline.FirstChild.Span.Start - x.Span.Start)); + } // custom generation of the id string id = title.ToLower(); diff --git a/src/DocLinkChecker/DocLinkChecker/Models/Hyperlink.cs b/src/DocLinkChecker/DocLinkChecker/Models/Hyperlink.cs index 9b109b4..7ab906d 100644 --- a/src/DocLinkChecker/DocLinkChecker/Models/Hyperlink.cs +++ b/src/DocLinkChecker/DocLinkChecker/Models/Hyperlink.cs @@ -106,6 +106,12 @@ public string UrlTopic if (IsLocal) { int pos = Url.IndexOf("#"); + if (pos == -1) + { + // if we don't have a header delimiter, we might have a url delimiter + pos = Url.IndexOf("?"); + } + return pos == -1 ? string.Empty : Url.Substring(pos + 1); } @@ -123,6 +129,12 @@ public string UrlWithoutTopic if (IsLocal) { int pos = Url.IndexOf("#"); + if (pos == -1) + { + // if we don't have a header delimiter, we might have a url delimiter + pos = Url.IndexOf("?"); + } + switch (pos) { case -1: @@ -149,6 +161,11 @@ public string UrlFullPath if (IsLocal) { int pos = Url.IndexOf("#"); + if (pos == -1) + { + // if we don't have a header delimiter, we might have a url delimiter + pos = Url.IndexOf("?"); + } // we want to know that the link is not starting with a # for local reference. // if local reference, return the filename otherwise the calculated path. diff --git a/src/DocLinkChecker/DocLinkChecker/Services/LinkValidatorService.cs b/src/DocLinkChecker/DocLinkChecker/Services/LinkValidatorService.cs index 4d7a711..1233c7c 100644 --- a/src/DocLinkChecker/DocLinkChecker/Services/LinkValidatorService.cs +++ b/src/DocLinkChecker/DocLinkChecker/Services/LinkValidatorService.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Net; - using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using DocLinkChecker.Enums; @@ -324,7 +323,10 @@ private Task VerifyLocalHyperlink(Hyperlink hyperlink) break; } - if (!string.IsNullOrEmpty(hyperlink.UrlTopic)) + // if the link references a markdown file and references a header, + // we check if it exists. + if (string.Compare(Path.GetExtension(hyperlink.UrlFullPath), ".md", true) == 0 && + !string.IsNullOrEmpty(hyperlink.UrlTopic)) { // validate if heading exists in file if (Headings