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("""
- First callout
- 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 ->