diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e09833356..fc3c8a992 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -5,10 +5,6 @@ on: permissions: contents: read - packages: read - id-token: write - pull-requests: write - deployments: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -19,8 +15,15 @@ env: jobs: build: - runs-on: ubuntu-latest - steps: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + steps: - uses: actions/checkout@v4 - name: Bootstrap Action Workspace @@ -28,17 +31,10 @@ jobs: uses: ./.github/actions/bootstrap - name: Build - run: ./build.sh + run: dotnet run --project build -c release - name: Test - run: ./build.sh test + run: dotnet run --project build -c release -- test - name: Publish AOT - run: ./build.sh publishbinaries - - - uses: actions/upload-artifact@v4 - with: - name: docs-builder-binary - path: .artifacts/publish/docs-builder/release/docs-builder - if-no-files-found: error - retention-days: 1 + run: dotnet run --project build -c release -- publishbinaries diff --git a/docs-builder.sln b/docs-builder.sln index d6f0c1f8e..d8d67fc4c 100644 --- a/docs-builder.sln +++ b/docs-builder.sln @@ -102,7 +102,6 @@ Global {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Debug|Any CPU.Build.0 = Debug|Any CPU {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|Any CPU.ActiveCfg = Release|Any CPU - {10857974-6CF1-42B5-B793-AAA988BD7348}.Release|Any CPU.Build.0 = Release|Any CPU {28350800-B44B-479B-86E2-1D39E321C0B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {28350800-B44B-479B-86E2-1D39E321C0B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {28350800-B44B-479B-86E2-1D39E321C0B4}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Elastic.Markdown.Refactor/Move.cs b/src/Elastic.Markdown.Refactor/Move.cs index a20b031e5..71af0dff3 100644 --- a/src/Elastic.Markdown.Refactor/Move.cs +++ b/src/Elastic.Markdown.Refactor/Move.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Elastic.Markdown.IO; using Microsoft.Extensions.Logging; @@ -64,6 +65,9 @@ private async Task SetupChanges(ChangeSet changeSet, Cancel ctx) var fullPath = Path.GetFullPath(Path.Combine(sourceDirectory, originalPath)); var relativePath = Path.GetRelativePath(targetDirectory, fullPath); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + relativePath = relativePath.Replace('\\', '/'); + newPath = originalPath.StartsWith("./", OrdinalIgnoreCase) && !relativePath.StartsWith("./", OrdinalIgnoreCase) ? "./" + relativePath : relativePath; @@ -258,6 +262,15 @@ string targetPath var absolutStyleSource = $"/{relativeToDocsFolder}"; var relativeToDocsFolderTarget = Path.GetRelativePath(documentationSet.SourceDirectory.FullName, targetPath); var absoluteStyleTarget = $"/{relativeToDocsFolderTarget}"; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + relativeSource = relativeSource.Replace('\\', '/'); + relativeSourceWithDotSlash = relativeSourceWithDotSlash.Replace('\\', '/'); + absolutStyleSource = absolutStyleSource.Replace('\\', '/'); + absoluteStyleTarget = absoluteStyleTarget.Replace('\\', '/'); + } + return ( relativeSource, relativeSourceWithDotSlash, @@ -298,6 +311,9 @@ private string ReplaceLinks( : $"[{match.Groups[1].Value}]({relativeTarget}{anchor})"; } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + newLink = newLink.Replace('\\', '/'); + var lineNumber = content[..match.Index].Count(c => c == '\n') + 1; var columnNumber = match.Index - content.LastIndexOf('\n', match.Index); if (!_linkModifications.ContainsKey(changeSet)) diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index 869b99b18..548e0e297 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using System.Runtime.InteropServices; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Helpers; using Elastic.Markdown.IO.Navigation; @@ -95,6 +96,8 @@ public string Url get { var relativePath = RelativePathUrl; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + relativePath = relativePath.Replace('\\', '/'); return Path.GetFileName(relativePath) == "index.md" ? $"{UrlPathPrefix}/{relativePath.Remove(relativePath.LastIndexOf("index.md", StringComparison.Ordinal), "index.md".Length)}" : $"{UrlPathPrefix}/{relativePath.Remove(relativePath.LastIndexOf(SourceFile.Extension, StringComparison.Ordinal), SourceFile.Extension.Length)}"; diff --git a/src/Elastic.Markdown/IO/State/LinkReference.cs b/src/Elastic.Markdown/IO/State/LinkReference.cs index c5a0a6b14..96c52e4c8 100644 --- a/src/Elastic.Markdown/IO/State/LinkReference.cs +++ b/src/Elastic.Markdown/IO/State/LinkReference.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Runtime.InteropServices; using System.Text.Json; using System.Text.Json.Serialization; using Elastic.Markdown.IO.Discovery; @@ -70,7 +71,9 @@ public static LinkReference Create(DocumentationSet set) var crossLinks = set.Build.Collector.CrossLinks.ToHashSet().ToArray(); var links = set.MarkdownFiles.Values .Select(m => (m.RelativePath, File: m)) - .ToDictionary(k => k.RelativePath, v => + .ToDictionary(k => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? k.RelativePath.Replace('\\', '/') + : k.RelativePath, v => { var anchors = v.File.Anchors.Count == 0 ? null : v.File.Anchors.ToArray(); return new LinkMetadata { Anchors = anchors, Hidden = v.File.Hidden }; diff --git a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs index e298d1ca3..e0cacf619 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.IO.Abstractions; +using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Helpers; @@ -271,6 +272,10 @@ private static void UpdateLinkUrl(LinkInline link, string url, ParserContext con if (!string.IsNullOrWhiteSpace(url) && !string.IsNullOrWhiteSpace(urlPathPrefix)) url = $"{urlPathPrefix.TrimEnd('/')}{url}"; + // When running on Windows, path traversal results must be normalized prior to being used in a URL + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + url = url.Replace('\\', '/'); + link.Url = string.IsNullOrEmpty(anchor) ? url : $"{url}#{anchor}"; } diff --git a/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs b/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs index bba7ed365..af2f3c0b2 100644 --- a/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs +++ b/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs @@ -118,7 +118,7 @@ public void ParsesMagicCallOuts() => Block!.CallOuts [Fact] public void RendersExpectedHtml() => - Html.Should().Contain(""" + Html.ReplaceLineEndings().Should().Contain("""
var x = 1; 1
@@ -132,7 +132,7 @@ public void RendersExpectedHtml() =>
 		                      
  • Marking the first callout
  • Marking the second callout
  • - """); + """.ReplaceLineEndings()); [Fact] @@ -367,12 +367,12 @@ public void ParsesCallouts() => Block!.CallOuts [Fact] public void RenderedHtmlContainsCallouts() => - Html.Should().Contain(""" + Html.ReplaceLineEndings().Should().Contain("""
    1. First callout
    2. Second callout
    - """); + """.ReplaceLineEndings()); } public class CodeBlockWithMultipleCommentTypesThenList(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp", @@ -426,10 +426,10 @@ public void ParsesCallouts() => Block!.CallOuts [Fact] public void RendersIntermediateParagraph() => - Html.Should().Contain(""" + Html.ReplaceLineEndings().Should().Contain("""

    This is an intermediate paragraph

      - """); + """.ReplaceLineEndings()); } public class CodeBlockWithCommentBlocksTwoParagraphsThenList(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp", diff --git a/tests/Elastic.Markdown.Tests/DocSet/BreadCrumbTests.cs b/tests/Elastic.Markdown.Tests/DocSet/BreadCrumbTests.cs index 1316c5d81..0b4279034 100644 --- a/tests/Elastic.Markdown.Tests/DocSet/BreadCrumbTests.cs +++ b/tests/Elastic.Markdown.Tests/DocSet/BreadCrumbTests.cs @@ -12,7 +12,7 @@ public class BreadCrumbTests(ITestOutputHelper output) : NavigationTestsBase(out [Fact] public void ParsesATableOfContents() { - var doc = Generator.DocumentationSet.Files.FirstOrDefault(f => f.RelativePath == "testing/nested/index.md") as MarkdownFile; + var doc = Generator.DocumentationSet.Files.FirstOrDefault(f => f.RelativePath == Path.Combine("testing", "nested", "index.md")) as MarkdownFile; doc.Should().NotBeNull(); diff --git a/tests/Elastic.Markdown.Tests/DocSet/NavigationTests.cs b/tests/Elastic.Markdown.Tests/DocSet/NavigationTests.cs index a01991ec4..64e52f786 100644 --- a/tests/Elastic.Markdown.Tests/DocSet/NavigationTests.cs +++ b/tests/Elastic.Markdown.Tests/DocSet/NavigationTests.cs @@ -17,14 +17,14 @@ public void ParsesNestedFoldersAndPrefixesPaths() { Configuration.ImplicitFolders.Should().NotBeNullOrEmpty(); Configuration.ImplicitFolders.Should() - .Contain("testing/nested"); + .Contain(Path.Combine("testing", "nested")); } [Fact] public void ParsesFilesAndPrefixesPaths() => Configuration.Files.Should() .Contain("index.md") - .And.Contain("syntax/index.md"); + .And.Contain(Path.Combine("syntax", "index.md")); [Fact] public void ParsesRedirects() diff --git a/tests/Elastic.Markdown.Tests/DocSet/NestedTocTests.cs b/tests/Elastic.Markdown.Tests/DocSet/NestedTocTests.cs index d89f5934d..cc8855b6c 100644 --- a/tests/Elastic.Markdown.Tests/DocSet/NestedTocTests.cs +++ b/tests/Elastic.Markdown.Tests/DocSet/NestedTocTests.cs @@ -12,7 +12,7 @@ public class NestedTocTests(ITestOutputHelper output) : NavigationTestsBase(outp [Fact] public void InjectsNestedTocsIntoDocumentationSet() { - var doc = Generator.DocumentationSet.Files.FirstOrDefault(f => f.RelativePath == "development/index.md") as MarkdownFile; + var doc = Generator.DocumentationSet.Files.FirstOrDefault(f => f.RelativePath == Path.Combine("development", "index.md")) as MarkdownFile; doc.Should().NotBeNull(); diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs index 351981ba8..b344f372e 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs @@ -111,11 +111,11 @@ public class InlineAnchorInHeading(ITestOutputHelper output) : BlockTest // language=html - Html.Should().Be( + Html.ReplaceLineEndings().TrimEnd().Should().Be( """ - """.TrimEnd() + """.ReplaceLineEndings().TrimEnd() ); } @@ -128,11 +128,11 @@ public class ExplicitSlugInHeader(ITestOutputHelper output) : BlockTest // language=html - Html.Should().Be( + Html.ReplaceLineEndings().TrimEnd().Should().Be( """ - """.TrimEnd() + """.ReplaceLineEndings().TrimEnd() ); } diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs index d2409987f..7f4c6c9e1 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs @@ -267,7 +267,7 @@ public class CommentedNonExistingLinks2(ITestOutputHelper output) : LinkTestBase [Fact] public void GeneratesHtml() => // language=html - Html.TrimEnd().Should().Be(""" + Html.ReplaceLineEndings().TrimEnd().Should().Be("""

      Links:

      • Special Requirements
      • @@ -275,7 +275,7 @@ public void GeneratesHtml() => - """); + """.ReplaceLineEndings()); [Fact] public void HasErrors() => Collector.Diagnostics.Should().HaveCount(0); diff --git a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs index c8d1f4807..e4f0cfd63 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information using System.IO.Abstractions.TestingHelpers; +using System.Runtime.InteropServices; using Elastic.Markdown.IO; using FluentAssertions; using JetBrains.Annotations; @@ -103,8 +104,10 @@ protected InlineTest( // ReSharper disable once VirtualMemberCallInConstructor // nasty but sub implementations won't use class state. AddToFileSystem(FileSystem); - - var root = FileSystem.DirectoryInfo.New(Path.Combine(Paths.Root.FullName, "docs/")); + var baseRootPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Paths.Root.FullName.Replace('\\', '/') + : Paths.Root.FullName; + var root = FileSystem.DirectoryInfo.New($"{baseRootPath}/docs/"); FileSystem.GenerateDocSetYaml(root, globalVariables); Collector = new TestDiagnosticsCollector(output); diff --git a/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs b/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs index ee41e4770..a2af9f44c 100644 --- a/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs +++ b/tests/Elastic.Markdown.Tests/Mover/MoverTests.cs @@ -28,15 +28,15 @@ public async Task RelativeLinks() linkModifications.Should().HaveCount(3); - Path.GetRelativePath(".", linkModifications[0].SourceFile).Should().Be("testing/mover/first-page.md"); + Path.GetRelativePath(".", linkModifications[0].SourceFile).Should().Be(Path.Combine("testing", "mover", "first-page.md")); linkModifications[0].OldLink.Should().Be("[Link to second page](second-page.md)"); linkModifications[0].NewLink.Should().Be("[Link to second page](../testing/mover/second-page.md)"); - Path.GetRelativePath(".", linkModifications[1].SourceFile).Should().Be("testing/mover/second-page.md"); + Path.GetRelativePath(".", linkModifications[1].SourceFile).Should().Be(Path.Combine("testing", "mover", "second-page.md")); linkModifications[1].OldLink.Should().Be("[Link to first page](first-page.md)"); linkModifications[1].NewLink.Should().Be("[Link to first page](../../new-folder/hello-world.md)"); - Path.GetRelativePath(".", linkModifications[2].SourceFile).Should().Be("testing/mover/second-page.md"); + Path.GetRelativePath(".", linkModifications[2].SourceFile).Should().Be(Path.Combine("testing", "mover", "second-page.md")); linkModifications[2].OldLink.Should().Be("[Absolut link to first page](/testing/mover/first-page.md)"); linkModifications[2].NewLink.Should().Be("[Absolut link to first page](/new-folder/hello-world.md)"); } @@ -56,15 +56,15 @@ public async Task MoveToFolder() var linkModifications = mover.LinkModifications[changeSet]; linkModifications.Should().HaveCount(3); - Path.GetRelativePath(".", linkModifications[0].SourceFile).Should().Be("testing/mover/first-page.md"); + Path.GetRelativePath(".", linkModifications[0].SourceFile).Should().Be(Path.Combine("testing", "mover", "first-page.md")); linkModifications[0].OldLink.Should().Be("[Link to second page](second-page.md)"); linkModifications[0].NewLink.Should().Be("[Link to second page](../testing/mover/second-page.md)"); - Path.GetRelativePath(".", linkModifications[1].SourceFile).Should().Be("testing/mover/second-page.md"); + Path.GetRelativePath(".", linkModifications[1].SourceFile).Should().Be(Path.Combine("testing", "mover", "second-page.md")); linkModifications[1].OldLink.Should().Be("[Link to first page](first-page.md)"); linkModifications[1].NewLink.Should().Be("[Link to first page](../../new-folder/first-page.md)"); - Path.GetRelativePath(".", linkModifications[2].SourceFile).Should().Be("testing/mover/second-page.md"); + Path.GetRelativePath(".", linkModifications[2].SourceFile).Should().Be(Path.Combine("testing", "mover", "second-page.md")); linkModifications[2].OldLink.Should().Be("[Absolut link to first page](/testing/mover/first-page.md)"); linkModifications[2].NewLink.Should().Be("[Absolut link to first page](/new-folder/first-page.md)"); } @@ -84,15 +84,15 @@ public async Task MoveFolderToFolder() var linkModifications = mover.LinkModifications[changeSet]; linkModifications.Should().HaveCount(3); - Path.GetRelativePath(".", linkModifications[0].SourceFile).Should().Be("testing/mover/first-page.md"); + Path.GetRelativePath(".", linkModifications[0].SourceFile).Should().Be(Path.Combine("testing", "mover", "first-page.md")); linkModifications[0].OldLink.Should().Be("[Link to second page](second-page.md)"); linkModifications[0].NewLink.Should().Be("[Link to second page](../testing/mover/second-page.md)"); - Path.GetRelativePath(".", linkModifications[1].SourceFile).Should().Be("testing/mover/second-page.md"); + Path.GetRelativePath(".", linkModifications[1].SourceFile).Should().Be(Path.Combine("testing", "mover", "second-page.md")); linkModifications[1].OldLink.Should().Be("[Link to first page](first-page.md)"); linkModifications[1].NewLink.Should().Be("[Link to first page](../../new-folder/first-page.md)"); - Path.GetRelativePath(".", linkModifications[2].SourceFile).Should().Be("testing/mover/second-page.md"); + Path.GetRelativePath(".", linkModifications[2].SourceFile).Should().Be(Path.Combine("testing", "mover", "second-page.md")); linkModifications[2].OldLink.Should().Be("[Absolut link to first page](/testing/mover/first-page.md)"); linkModifications[2].NewLink.Should().Be("[Absolut link to first page](/new-folder/first-page.md)"); } diff --git a/tests/authoring/Framework/MarkdownResultsAssertions.fs b/tests/authoring/Framework/MarkdownResultsAssertions.fs index 4749d1a68..52a745a75 100644 --- a/tests/authoring/Framework/MarkdownResultsAssertions.fs +++ b/tests/authoring/Framework/MarkdownResultsAssertions.fs @@ -11,6 +11,7 @@ open DiffPlex.DiffBuilder.Model open FsUnit.Xunit open JetBrains.Annotations open Xunit.Sdk +open System.IO [] module ResultsAssertions = @@ -50,7 +51,7 @@ module ResultsAssertions = let result = results.MarkdownResults - |> Seq.tryFind (fun m -> m.File.RelativePath = file) + |> Seq.tryFind (fun m -> m.File.RelativePath = (string file).Replace('/', Path.DirectorySeparatorChar)) match result with | None ->