From 7954e9917edf51953a1d000f3dda31e249647f09 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 24 Feb 2025 18:32:27 +0100 Subject: [PATCH 01/45] stage --- docs-builder.sln | 7 ++ .../Dockerfile | 11 +++ ...umentation.Lambda.LinkIndexUploader.csproj | 32 +++++++ .../Function.cs | 86 +++++++++++++++++++ .../aws-lambda-tools-defaults.json | 18 ++++ .../SourceGenerationContext.cs | 2 +- 6 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/Elastic.Documentation.Lambda.LinkIndexUploader/Dockerfile create mode 100644 src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj create mode 100644 src/Elastic.Documentation.Lambda.LinkIndexUploader/Function.cs create mode 100644 src/Elastic.Documentation.Lambda.LinkIndexUploader/aws-lambda-tools-defaults.json diff --git a/docs-builder.sln b/docs-builder.sln index f5da68c18..bc6a69f7f 100644 --- a/docs-builder.sln +++ b/docs-builder.sln @@ -60,6 +60,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "validate-inbound-local", "v actions\validate-inbound-local\action.yml = actions\validate-inbound-local\action.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Documentation.Lambda.LinkIndexUploader", "src\Elastic.Documentation.Lambda.LinkIndexUploader\Elastic.Documentation.Lambda.LinkIndexUploader.csproj", "{C559D52D-100B-4B2B-BE87-2344D835761D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -106,6 +108,10 @@ Global {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA}.Release|Any CPU.Build.0 = Release|Any CPU + {C559D52D-100B-4B2B-BE87-2344D835761D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C559D52D-100B-4B2B-BE87-2344D835761D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C559D52D-100B-4B2B-BE87-2344D835761D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C559D52D-100B-4B2B-BE87-2344D835761D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {4D198E25-C211-41DC-9E84-B15E89BD7048} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A} @@ -119,5 +125,6 @@ Global {CFEE9FAD-9E0C-4C0E-A0C2-B97D594C14B5} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7} {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A} {6E2ED6CC-AFC1-4E58-965D-6AEC500EBB46} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7} + {C559D52D-100B-4B2B-BE87-2344D835761D} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A} EndGlobalSection EndGlobal diff --git a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Dockerfile b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Dockerfile new file mode 100644 index 000000000..176a936eb --- /dev/null +++ b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Dockerfile @@ -0,0 +1,11 @@ +# You can also pull these images from DockerHub amazon/aws-lambda-dotnet:8 +FROM mcr.microsoft.com/dotnet/runtime:9.0 + +# Set the image's internal work directory +WORKDIR /var/task + +# Copy function code to Lambda-defined environment variable +COPY "bin/Release/net9.0/linux-x64" . + +# Set the entrypoint to the bootstrap +ENTRYPOINT ["/usr/bin/dotnet", "exec", "/var/task/bootstrap.dll"] \ No newline at end of file diff --git a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj new file mode 100644 index 000000000..10f32f144 --- /dev/null +++ b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj @@ -0,0 +1,32 @@ + + + Exe + net9.0 + enable + enable + true + + Lambda + + true + true + true + true + false + Linux + + + + + + + + + + + + + .dockerignore + + + \ No newline at end of file diff --git a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Function.cs b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Function.cs new file mode 100644 index 000000000..2de4d276b --- /dev/null +++ b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Function.cs @@ -0,0 +1,86 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using System.Text.Json.Serialization; +using Amazon.S3; +using Amazon.S3.Model; +using Elastic.Markdown; +using Elastic.Markdown.CrossLinks; + +namespace Elastic.Documentation.Lambda.LinkIndexUploader; + +[SuppressMessage("Naming", "CA1716:Identifiers should not match keywords")] +public class Function +{ + /// + /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It + /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and + /// the JSON serializer to use for converting Lambda JSON format to the .NET types. + /// + private static async Task Main() + { + var handler = FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } + + public static async Task FunctionHandler(string input, ILambdaContext context) + { + var sw = Stopwatch.StartNew(); + IAmazonS3 client = new AmazonS3Client(); + var bucketName = "elastic-docs-link-index"; + var request = new ListObjectsV2Request { BucketName = bucketName, MaxKeys = 5 }; + + var linkIndex = new LinkIndex { Repositories = [] }; + try + { + ListObjectsV2Response response; + do + { + response = await client.ListObjectsV2Async(request, CancellationToken.None); + foreach (var obj in response.S3Objects) + { + if (!obj.Key.StartsWith("elastic/", StringComparison.OrdinalIgnoreCase)) + continue; + + var tokens = obj.Key.Split('/'); + if (tokens.Length < 3) + continue; + + var repository = tokens[1]; + var branch = tokens[2]; + + var entry = new LinkIndexEntry { Repository = repository, Branch = branch, ETag = obj.ETag.Trim('"'), Path = obj.Key }; + if (linkIndex.Repositories.TryGetValue(repository, out var existingEntry)) + existingEntry[branch] = entry; + else + linkIndex.Repositories.Add(repository, new Dictionary { { branch, entry } }); + Console.WriteLine(entry); + } + + // If the response is truncated, set the request ContinuationToken + // from the NextContinuationToken property of the response. + request.ContinuationToken = response.NextContinuationToken; + } while (response.IsTruncated); + } + catch (AmazonS3Exception ex) + { + return $"Error encountered on server. Message:'{ex.Message}' getting list of objects."; + } + + var json = LinkIndex.Serialize(linkIndex); + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + await client.UploadObjectFromStreamAsync(bucketName, "link-index.json", stream, new Dictionary(), CancellationToken.None); + return $"Finished in {sw}"; + + } +} diff --git a/src/Elastic.Documentation.Lambda.LinkIndexUploader/aws-lambda-tools-defaults.json b/src/Elastic.Documentation.Lambda.LinkIndexUploader/aws-lambda-tools-defaults.json new file mode 100644 index 000000000..483547d7b --- /dev/null +++ b/src/Elastic.Documentation.Lambda.LinkIndexUploader/aws-lambda-tools-defaults.json @@ -0,0 +1,18 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "function-runtime": "provided.al2023", + "function-memory-size": 512, + "function-timeout": 180, + "function-handler": "Elastic.Documentation.Lambda.LinkIndexUploader", + "msbuild-parameters": "--self-contained true", + "package-type": "image", + "docker-host-build-output-dir": "./bin/Release/lambda-publish" +} \ No newline at end of file diff --git a/src/Elastic.Markdown/SourceGenerationContext.cs b/src/Elastic.Markdown/SourceGenerationContext.cs index 8e1769b96..7b07758b8 100644 --- a/src/Elastic.Markdown/SourceGenerationContext.cs +++ b/src/Elastic.Markdown/SourceGenerationContext.cs @@ -17,4 +17,4 @@ namespace Elastic.Markdown; [JsonSerializable(typeof(GitCheckoutInformation))] [JsonSerializable(typeof(LinkIndex))] [JsonSerializable(typeof(LinkIndexEntry))] -internal sealed partial class SourceGenerationContext : JsonSerializerContext; +public sealed partial class SourceGenerationContext : JsonSerializerContext; From 0e453e389a620eac965f2b1eea3b1f4ceb027f11 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 26 Feb 2025 19:57:12 +0100 Subject: [PATCH 02/45] Simplify function --- .../Dockerfile | 6 +- ...umentation.Lambda.LinkIndexUploader.csproj | 12 ++- .../Function.cs | 86 ------------------- .../Program.cs | 82 ++++++++++++++++++ .../aws-lambda-tools-defaults.json | 5 +- 5 files changed, 91 insertions(+), 100 deletions(-) delete mode 100644 src/Elastic.Documentation.Lambda.LinkIndexUploader/Function.cs create mode 100644 src/Elastic.Documentation.Lambda.LinkIndexUploader/Program.cs diff --git a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Dockerfile b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Dockerfile index 176a936eb..d6993c097 100644 --- a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Dockerfile +++ b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Dockerfile @@ -1,11 +1,11 @@ # You can also pull these images from DockerHub amazon/aws-lambda-dotnet:8 -FROM mcr.microsoft.com/dotnet/runtime:9.0 +FROM mcr.microsoft.com/dotnet/sdk:9.0 # Set the image's internal work directory WORKDIR /var/task # Copy function code to Lambda-defined environment variable -COPY "bin/Release/net9.0/linux-x64" . +COPY "bin/Release/lambda-publish" . # Set the entrypoint to the bootstrap -ENTRYPOINT ["/usr/bin/dotnet", "exec", "/var/task/bootstrap.dll"] \ No newline at end of file +ENTRYPOINT ["/usr/bin/dotnet", "exec", "/var/task/Elastic.Documentation.Lambda.LinkIndexUploader.dll"] \ No newline at end of file diff --git a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj index 10f32f144..820523fab 100644 --- a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj +++ b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj @@ -5,7 +5,7 @@ enable enable true - + Lambda true @@ -14,9 +14,12 @@ true false Linux + + false + true - + @@ -24,9 +27,4 @@ - - - .dockerignore - - \ No newline at end of file diff --git a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Function.cs b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Function.cs deleted file mode 100644 index 2de4d276b..000000000 --- a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Function.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// 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.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using Amazon.Lambda.Core; -using Amazon.Lambda.RuntimeSupport; -using Amazon.Lambda.Serialization.SystemTextJson; -using System.Text.Json.Serialization; -using Amazon.S3; -using Amazon.S3.Model; -using Elastic.Markdown; -using Elastic.Markdown.CrossLinks; - -namespace Elastic.Documentation.Lambda.LinkIndexUploader; - -[SuppressMessage("Naming", "CA1716:Identifiers should not match keywords")] -public class Function -{ - /// - /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It - /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and - /// the JSON serializer to use for converting Lambda JSON format to the .NET types. - /// - private static async Task Main() - { - var handler = FunctionHandler; - await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) - .Build() - .RunAsync(); - } - - public static async Task FunctionHandler(string input, ILambdaContext context) - { - var sw = Stopwatch.StartNew(); - IAmazonS3 client = new AmazonS3Client(); - var bucketName = "elastic-docs-link-index"; - var request = new ListObjectsV2Request { BucketName = bucketName, MaxKeys = 5 }; - - var linkIndex = new LinkIndex { Repositories = [] }; - try - { - ListObjectsV2Response response; - do - { - response = await client.ListObjectsV2Async(request, CancellationToken.None); - foreach (var obj in response.S3Objects) - { - if (!obj.Key.StartsWith("elastic/", StringComparison.OrdinalIgnoreCase)) - continue; - - var tokens = obj.Key.Split('/'); - if (tokens.Length < 3) - continue; - - var repository = tokens[1]; - var branch = tokens[2]; - - var entry = new LinkIndexEntry { Repository = repository, Branch = branch, ETag = obj.ETag.Trim('"'), Path = obj.Key }; - if (linkIndex.Repositories.TryGetValue(repository, out var existingEntry)) - existingEntry[branch] = entry; - else - linkIndex.Repositories.Add(repository, new Dictionary { { branch, entry } }); - Console.WriteLine(entry); - } - - // If the response is truncated, set the request ContinuationToken - // from the NextContinuationToken property of the response. - request.ContinuationToken = response.NextContinuationToken; - } while (response.IsTruncated); - } - catch (AmazonS3Exception ex) - { - return $"Error encountered on server. Message:'{ex.Message}' getting list of objects."; - } - - var json = LinkIndex.Serialize(linkIndex); - - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - await client.UploadObjectFromStreamAsync(bucketName, "link-index.json", stream, new Dictionary(), CancellationToken.None); - return $"Finished in {sw}"; - - } -} diff --git a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Program.cs b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Program.cs new file mode 100644 index 000000000..1507f9dd6 --- /dev/null +++ b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Program.cs @@ -0,0 +1,82 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// 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.Diagnostics; +using System.Text; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.S3; +using Amazon.S3.Model; +using Elastic.Markdown.CrossLinks; + +await LambdaBootstrapBuilder.Create(Handler) + .Build() + .RunAsync(); + +static async Task Handler(ILambdaContext context) +{ + var sw = Stopwatch.StartNew(); + IAmazonS3 client = new AmazonS3Client(); + var bucketName = "elastic-docs-link-index"; + var request = new ListObjectsV2Request + { + BucketName = bucketName, + MaxKeys = 5 + }; + + var linkIndex = new LinkIndex + { + Repositories = [] + }; + try + { + ListObjectsV2Response response; + do + { + response = await client.ListObjectsV2Async(request, CancellationToken.None); + foreach (var obj in response.S3Objects) + { + if (!obj.Key.StartsWith("elastic/", StringComparison.OrdinalIgnoreCase)) + continue; + + var tokens = obj.Key.Split('/'); + if (tokens.Length < 3) + continue; + + var repository = tokens[1]; + var branch = tokens[2]; + + var entry = new LinkIndexEntry + { + Repository = repository, + Branch = branch, + ETag = obj.ETag.Trim('"'), + Path = obj.Key + }; + if (linkIndex.Repositories.TryGetValue(repository, out var existingEntry)) + existingEntry[branch] = entry; + else + linkIndex.Repositories.Add(repository, new Dictionary + { + { branch, entry } + }); + Console.WriteLine(entry); + } + + // If the response is truncated, set the request ContinuationToken + // from the NextContinuationToken property of the response. + request.ContinuationToken = response.NextContinuationToken; + } while (response.IsTruncated); + } + catch (AmazonS3Exception ex) + { + return $"Error encountered on server. Message:'{ex.Message}' getting list of objects."; + } + + var json = LinkIndex.Serialize(linkIndex); + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + await client.UploadObjectFromStreamAsync(bucketName, "link-index.json", stream, new Dictionary(), CancellationToken.None); + return $"Finished in {sw}"; +} diff --git a/src/Elastic.Documentation.Lambda.LinkIndexUploader/aws-lambda-tools-defaults.json b/src/Elastic.Documentation.Lambda.LinkIndexUploader/aws-lambda-tools-defaults.json index 483547d7b..1d95749a0 100644 --- a/src/Elastic.Documentation.Lambda.LinkIndexUploader/aws-lambda-tools-defaults.json +++ b/src/Elastic.Documentation.Lambda.LinkIndexUploader/aws-lambda-tools-defaults.json @@ -8,11 +8,8 @@ "profile": "", "region": "", "configuration": "Release", - "function-runtime": "provided.al2023", + "function-runtime": "provided.al2", "function-memory-size": 512, "function-timeout": 180, "function-handler": "Elastic.Documentation.Lambda.LinkIndexUploader", - "msbuild-parameters": "--self-contained true", - "package-type": "image", - "docker-host-build-output-dir": "./bin/Release/lambda-publish" } \ No newline at end of file From 3d2a08bcc44be9e491a823079114ab6d39b4a36f Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 26 Feb 2025 20:16:09 +0100 Subject: [PATCH 03/45] ensure binary is named bootstrap --- .../Elastic.Documentation.Lambda.LinkIndexUploader.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj index 820523fab..f85bc3d4b 100644 --- a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj +++ b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj @@ -6,6 +6,7 @@ enable true + bootstrap Lambda true From c021aeaabee228c06abac16b60c3760eb5da1eab Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 26 Feb 2025 20:57:38 +0100 Subject: [PATCH 04/45] add lambda docker build --- lambda.DockerFile | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 lambda.DockerFile diff --git a/lambda.DockerFile b/lambda.DockerFile new file mode 100644 index 000000000..91474f1d5 --- /dev/null +++ b/lambda.DockerFile @@ -0,0 +1,21 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:2023 AS base + +WORKDIR /app + +RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc + +RUN curl -o /etc/yum.repos.d/microsoft-prod.repo https://packages.microsoft.com/config/fedora/39/prod.repo + +RUN dnf update -y +RUN dnf install dotnet-sdk-9.0 + +COPY ./src/ ./src/ +COPY ./*.sln ./ + +ENV DOTNET_NOLOGO=true +ENV DOTNET_CLI_TELEMETRY_OPTOUT=true + +RUN dotnet restore + +COPY ./src/ ./src/ +RUN dotnet publish ./src/Elastic.Documenation.Lambda.LinkIndexUploader -r linux-x64 -c Release -o ./out/upload-handler \ No newline at end of file From f015b1b0f8a8782e007762680f6cd2a748c81f3b Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 27 Feb 2025 09:24:29 +0100 Subject: [PATCH 05/45] update lambda build --- Directory.Build.props | 2 +- lambda.DockerFile | 19 ++++++++++++++----- ...umentation.Lambda.LinkIndexUploader.csproj | 6 +----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index a49f44bc9..d2138cdc9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -31,4 +31,4 @@ - \ No newline at end of file + diff --git a/lambda.DockerFile b/lambda.DockerFile index 91474f1d5..5374e99f9 100644 --- a/lambda.DockerFile +++ b/lambda.DockerFile @@ -7,15 +7,24 @@ RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc RUN curl -o /etc/yum.repos.d/microsoft-prod.repo https://packages.microsoft.com/config/fedora/39/prod.repo RUN dnf update -y -RUN dnf install dotnet-sdk-9.0 +RUN dnf install -y dotnet-sdk-9.0 +RUN dnf install -y npm +RUN dnf install -y git +RUN dnf install -y clang -COPY ./src/ ./src/ -COPY ./*.sln ./ +#COPY ./src/ ./src/ +#COPY ./tests/ ./tests/ +#COPY ./docs/*.csproj ./docs/ +#COPY ./.github/*.csproj ./.github/ +#COPY ./build/*.fsproj ./build/ +#COPY ./*.sln ./ + +COPY . . ENV DOTNET_NOLOGO=true ENV DOTNET_CLI_TELEMETRY_OPTOUT=true RUN dotnet restore -COPY ./src/ ./src/ -RUN dotnet publish ./src/Elastic.Documenation.Lambda.LinkIndexUploader -r linux-x64 -c Release -o ./out/upload-handler \ No newline at end of file +RUN dotnet publish src/Elastic.Documentation.Lambda.LinkIndexUploader -r linux-x64 -c Release + diff --git a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj index f85bc3d4b..a24fc32ca 100644 --- a/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj +++ b/src/Elastic.Documentation.Lambda.LinkIndexUploader/Elastic.Documentation.Lambda.LinkIndexUploader.csproj @@ -14,10 +14,6 @@ true true false - Linux - - false - true @@ -28,4 +24,4 @@ - \ No newline at end of file + From 4564519a663e856fa3c42023db29b5f674690e40 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 26 Feb 2025 15:43:05 +0100 Subject: [PATCH 06/45] Add `free-disk-space` input (#614) * Add `free-disk-space` input * Use exact commit --- .github/workflows/preview-build.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 79a057db2..1b6bfd809 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -25,6 +25,11 @@ on: type: string default: '**' required: false + free-disk-space: + description: 'Free disk space before running the build' + type: string + default: 'false' + required: false permissions: id-token: write @@ -53,8 +58,8 @@ jobs: files: ${{ inputs.path-pattern != '' && inputs.path-pattern || '**' }} - name: Free Disk Space - if: github.event_name != 'merge_group' - uses: jlumbroso/free-disk-space@main + if: ${{ inputs.free-disk-space != '' && inputs.free-disk-space || 'false' }} + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 with: tool-cache: false From d255f9adf6aa52eeb7ec2d68b286be3b91af7c0e Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 26 Feb 2025 16:01:03 +0100 Subject: [PATCH 07/45] Simply if check on Free Disk Space (#616) --- .github/workflows/preview-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 1b6bfd809..177ccc604 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -58,7 +58,7 @@ jobs: files: ${{ inputs.path-pattern != '' && inputs.path-pattern || '**' }} - name: Free Disk Space - if: ${{ inputs.free-disk-space != '' && inputs.free-disk-space || 'false' }} + if: ${{ inputs.free-disk-space != 'false' }} uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 with: tool-cache: false From 552bf6d612efa2c23b48c92f8da1f7791c62b448 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 26 Feb 2025 16:48:54 +0100 Subject: [PATCH 08/45] Default CurrentUrlPath to empty string while we investigate further (#618) --- src/Elastic.Markdown/IO/DocumentationFile.cs | 1 - src/Elastic.Markdown/Myst/ParserContext.cs | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Elastic.Markdown/IO/DocumentationFile.cs b/src/Elastic.Markdown/IO/DocumentationFile.cs index 5bc44b80a..b4b9dd6b3 100644 --- a/src/Elastic.Markdown/IO/DocumentationFile.cs +++ b/src/Elastic.Markdown/IO/DocumentationFile.cs @@ -53,4 +53,3 @@ public record SnippetFile(IFileInfo SourceFile, IDirectoryInfo RootPath) } public record SnippetAnchors(string[] Anchors, IReadOnlyCollection TableOfContentItems); - diff --git a/src/Elastic.Markdown/Myst/ParserContext.cs b/src/Elastic.Markdown/Myst/ParserContext.cs index c025ea03c..bcf05dbd0 100644 --- a/src/Elastic.Markdown/Myst/ParserContext.cs +++ b/src/Elastic.Markdown/Myst/ParserContext.cs @@ -75,9 +75,11 @@ public ParserContext(ParserState state) CurrentUrlPath = DocumentationFileLookup(parentPath ?? MarkdownSourcePath) is MarkdownFile md ? md.Url - : SkipValidation - ? string.Empty - : throw new Exception($"Unable to find documentation file for {(parentPath ?? MarkdownSourcePath).FullName}"); + : string.Empty; + if (SkipValidation && string.IsNullOrEmpty(CurrentUrlPath)) + { + //TODO investigate this deeper. + } if (YamlFrontMatter?.Properties is not { Count: > 0 }) Substitutions = Configuration.Substitutions; From b733643c20c392148f905e25a6a8bf7ef6a06d1b Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 26 Feb 2025 17:01:09 +0100 Subject: [PATCH 09/45] Fix `free-disk-space` input attempt 2 (#617) --- .github/workflows/preview-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 177ccc604..20452e42a 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -58,7 +58,7 @@ jobs: files: ${{ inputs.path-pattern != '' && inputs.path-pattern || '**' }} - name: Free Disk Space - if: ${{ inputs.free-disk-space != 'false' }} + if: fromJSON(inputs.free-disk-space != '' && inputs.free-disk-space || 'false') uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 with: tool-cache: false From d148715cd7adb1a34a0f1bf790cd8a81936822aa Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 26 Feb 2025 17:21:08 +0100 Subject: [PATCH 10/45] Just compare with repository name for now (#619) --- .github/workflows/preview-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 20452e42a..2c8291161 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -58,7 +58,7 @@ jobs: files: ${{ inputs.path-pattern != '' && inputs.path-pattern || '**' }} - name: Free Disk Space - if: fromJSON(inputs.free-disk-space != '' && inputs.free-disk-space || 'false') + if: github.repository == 'elastic/asciidocalypse' uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 with: tool-cache: false From b1bb06dacb277ac76d29abcee03f238da9df55aa Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 26 Feb 2025 22:08:46 +0100 Subject: [PATCH 11/45] Add update-reference-index action (#622) --- actions/generator/action.yml | 16 ---------------- actions/update-reference-index/action.yml | 10 ++++++++++ docs-builder.sln | 13 +++++++++++++ 3 files changed, 23 insertions(+), 16 deletions(-) delete mode 100644 actions/generator/action.yml create mode 100644 actions/update-reference-index/action.yml diff --git a/actions/generator/action.yml b/actions/generator/action.yml deleted file mode 100644 index 6eec713bb..000000000 --- a/actions/generator/action.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: 'Documentation Generator' -description: 'Generates a random yet deterministic documentation set' - -branding: - icon: 'filter' - color: 'red' - -inputs: - output: - description: 'Path to output the documentation' - required: false - -runs: - using: 'docker' - image: "docker://ghcr.io/elastic/docs-generator:edge" - diff --git a/actions/update-reference-index/action.yml b/actions/update-reference-index/action.yml new file mode 100644 index 000000000..aa50c0c82 --- /dev/null +++ b/actions/update-reference-index/action.yml @@ -0,0 +1,10 @@ +name: 'Update Reference Index' +description: 'Updates links-index.json with the latest pointers to all links.json files' + +runs: + using: "composite" + steps: + - name: Update Reference Index + run: | + curl -v https://kaqcb6pumme57zlb63wmoqcjxq0fhkdl.lambda-url.us-east-1.on.aws + shell: bash \ No newline at end of file diff --git a/docs-builder.sln b/docs-builder.sln index a65acb415..9309814e5 100644 --- a/docs-builder.sln +++ b/docs-builder.sln @@ -60,6 +60,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "validate-inbound-local", "v actions\validate-inbound-local\action.yml = actions\validate-inbound-local\action.yml EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "update-link-index", "update-link-index", "{6554F917-73CE-4B3D-9101-F28EAA762C6B}" + ProjectSection(SolutionItems) = preProject + actions\update-link-index\action.yml = actions\update-link-index\action.yml + actions\update-link-index\README.md = actions\update-link-index\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "update-reference-index", "update-reference-index", "{9FEC15F6-13F8-40B1-A66A-EB054E49E680}" + ProjectSection(SolutionItems) = preProject + actions\update-reference-index\action.yml = actions\update-reference-index\action.yml + EndProjectSection +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Documentation.Lambda.LinkIndexUploader", "src\Elastic.Documentation.Lambda.LinkIndexUploader\Elastic.Documentation.Lambda.LinkIndexUploader.csproj", "{C559D52D-100B-4B2B-BE87-2344D835761D}" EndProject Global @@ -125,6 +136,8 @@ Global {CFEE9FAD-9E0C-4C0E-A0C2-B97D594C14B5} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7} {4CCE599A-B9FE-4DF2-8763-34CF0A99D4AA} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A} {6E2ED6CC-AFC1-4E58-965D-6AEC500EBB46} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7} + {6554F917-73CE-4B3D-9101-F28EAA762C6B} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7} + {9FEC15F6-13F8-40B1-A66A-EB054E49E680} = {245023D2-D3CA-47B9-831D-DAB91A2FFDC7} {C559D52D-100B-4B2B-BE87-2344D835761D} = {BE6011CC-1200-4957-B01F-FCCA10C5CF5A} EndGlobalSection EndGlobal From a84d015949caba4711d0a14d5af337373b5a22bf Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 26 Feb 2025 22:17:39 +0100 Subject: [PATCH 12/45] Update links-index.json whenever a PR get's closed (#623) --- .github/workflows/preview-cleanup.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/preview-cleanup.yml b/.github/workflows/preview-cleanup.yml index eeed40496..105387b75 100644 --- a/.github/workflows/preview-cleanup.yml +++ b/.github/workflows/preview-cleanup.yml @@ -51,3 +51,6 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} run: | aws s3 rm "s3://elastic-docs-v3-website-preview/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" --recursive + + - name: 'Update Reference Index' + uses: elastic/docs-builder/actions/update-reference-index@main \ No newline at end of file From 2512e28a47d734195412d2098fbf8db898eee90b Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 26 Feb 2025 22:22:59 +0100 Subject: [PATCH 13/45] Update links-index.json whenever we push a links.json (#624) --- .github/workflows/preview-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 2c8291161..80c6ecccc 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -158,6 +158,10 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.s3-upload.outcome == 'success' uses: elastic/docs-builder/actions/update-link-index@main + - name: Update Reference Index + if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.s3-upload.outcome == 'success' + uses: elastic/docs-builder/actions/update-reference-index@main + - name: Update deployment status uses: actions/github-script@v7 if: always() && steps.deployment.outputs.result From 199d4277011a168fab79c100453061c159d4ad6f Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 27 Feb 2025 09:35:52 +0100 Subject: [PATCH 14/45] Continue-on-error temporarily for validate-inbound-links (#627) --- .github/workflows/preview-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 80c6ecccc..8f2e91d1e 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -140,6 +140,7 @@ jobs: - name: 'Validate Inbound Links' uses: elastic/docs-builder/actions/validate-inbound-local@main + continue-on-error: true if: ${{ !cancelled() && (steps.deployment.outputs.result || (steps.check-files.outputs.any_modified == 'true' && github.event_name == 'merge_group')) }} - uses: elastic/docs-builder/.github/actions/aws-auth@main From a63dc2e96ef90dd815d8414cf580138be9aec476 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 27 Feb 2025 10:24:40 +0100 Subject: [PATCH 15/45] docs-preview: Use `since_last_remote_commit: true` for changed-files action (#628) --- .github/workflows/preview-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 8f2e91d1e..4c59cc98a 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -56,6 +56,7 @@ jobs: uses: tj-actions/changed-files@d6e91a2266cdb9d62096cebf1e8546899c6aa18f # v45.0.6 with: files: ${{ inputs.path-pattern != '' && inputs.path-pattern || '**' }} + since_last_remote_commit: true - name: Free Disk Space if: github.repository == 'elastic/asciidocalypse' From 68221f900467b3c140427de79814bae3a8784531 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 27 Feb 2025 10:46:39 +0100 Subject: [PATCH 16/45] On ci skip if /docs folder has no docset.yml silently (#630) * On ci skip if /docs folder has no docset.yml silently * skip false not in finally * Add log message --- action.yml | 3 +++ src/docs-builder/Cli/Commands.cs | 34 +++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/action.yml b/action.yml index 435febaf8..9ce187976 100644 --- a/action.yml +++ b/action.yml @@ -12,6 +12,9 @@ inputs: strict: description: 'Treat warnings as errors' required: false +outputs: + skip: + description: "hint from the documentation tool to skip the docs build for this PR" runs: using: 'docker' diff --git a/src/docs-builder/Cli/Commands.cs b/src/docs-builder/Cli/Commands.cs index 6e1b29bb7..aa2e3c624 100644 --- a/src/docs-builder/Cli/Commands.cs +++ b/src/docs-builder/Cli/Commands.cs @@ -74,12 +74,36 @@ public async Task Generate( pathPrefix ??= githubActionsService.GetInput("prefix"); var fileSystem = new FileSystem(); var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); - var context = new BuildContext(collector, fileSystem, fileSystem, path, output) + + var runningOnCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")); + BuildContext context; + try + { + context = new BuildContext(collector, fileSystem, fileSystem, path, output) + { + UrlPathPrefix = pathPrefix, + Force = force ?? false, + AllowIndexing = allowIndexing != null + }; + } + // On CI, we are running on merge commit which may have changes against an older + // docs folder (this can happen on out of date PR's). + // At some point in the future we can remove this try catch + catch (Exception e) when (runningOnCi && e.Message.StartsWith("Can not locate docset.yml file in")) { - UrlPathPrefix = pathPrefix, - Force = force ?? false, - AllowIndexing = allowIndexing != null - }; + var outputDirectory = !string.IsNullOrWhiteSpace(output) + ? fileSystem.DirectoryInfo.New(output) + : fileSystem.DirectoryInfo.New(Path.Combine(Paths.Root.FullName, ".artifacts/docs/html")); + // we temporarily do not error when pointed to a non documentation folder. + _ = fileSystem.Directory.CreateDirectory(outputDirectory.FullName); + + ConsoleApp.Log($"Skipping build as we are running on a merge commit and the docs folder is out of date and has no docset.yml. {e.Message}"); + + await githubActionsService.SetOutputAsync("skip", "true"); + return 0; + } + if (runningOnCi) + await githubActionsService.SetOutputAsync("skip", "false"); var set = new DocumentationSet(context, logger); var generator = new DocumentationGenerator(set, logger); await generator.GenerateAll(ctx); From 31af4f9db008607136238c96b3c4e1a8513c2843 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 27 Feb 2025 10:48:19 +0100 Subject: [PATCH 17/45] =?UTF-8?q?Revert=20"docs-preview:=20Use=20`since=5F?= =?UTF-8?q?last=5Fremote=5Fcommit:=20true`=20for=20changed-files=20?= =?UTF-8?q?=E2=80=A6"=20(#629)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit ed3381779f2e5f9d59da52fa55e03d889f90c8bc. --- .github/workflows/preview-build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 4c59cc98a..8f2e91d1e 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -56,7 +56,6 @@ jobs: uses: tj-actions/changed-files@d6e91a2266cdb9d62096cebf1e8546899c6aa18f # v45.0.6 with: files: ${{ inputs.path-pattern != '' && inputs.path-pattern || '**' }} - since_last_remote_commit: true - name: Free Disk Space if: github.repository == 'elastic/asciidocalypse' From defe51dca65465f12827acc3225fb4ce0df5f26c Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 27 Feb 2025 10:58:01 +0100 Subject: [PATCH 18/45] Feature: allow the docs-build action to hint to subsequent steps to skip (#631) --- .github/workflows/preview-build.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 8f2e91d1e..b8c68808c 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -133,22 +133,23 @@ jobs: - name: Build documentation if: github.repository != 'elastic/docs-builder' && (steps.deployment.outputs.result || (steps.check-files.outputs.any_modified == 'true' && github.event_name == 'merge_group')) uses: elastic/docs-builder@main + id: docs-build continue-on-error: ${{ fromJSON(inputs.continue-on-error != '' && inputs.continue-on-error || 'false') }} with: prefix: ${{ env.PATH_PREFIX }} strict: ${{ fromJSON(inputs.strict != '' && inputs.strict || 'true') }} - name: 'Validate Inbound Links' + if: ${{ !cancelled() && steps.docs-build.outputs.skip != 'true' && (steps.deployment.outputs.result || (steps.check-files.outputs.any_modified == 'true' && github.event_name == 'merge_group')) }} uses: elastic/docs-builder/actions/validate-inbound-local@main continue-on-error: true - if: ${{ !cancelled() && (steps.deployment.outputs.result || (steps.check-files.outputs.any_modified == 'true' && github.event_name == 'merge_group')) }} - uses: elastic/docs-builder/.github/actions/aws-auth@main - if: ${{ !cancelled() && steps.deployment.outputs.result }} + if: ${{ !cancelled() && steps.docs-build.outputs.skip != 'true' && steps.deployment.outputs.result }} - name: Upload to S3 id: s3-upload - if: ${{ !cancelled() && steps.deployment.outputs.result }} + if: ${{ !cancelled() && steps.docs-build.outputs.skip != 'true' && steps.deployment.outputs.result }} run: | aws s3 sync .artifacts/docs/html "s3://elastic-docs-v3-website-preview${PATH_PREFIX}" --delete aws cloudfront create-invalidation \ From a9fcb3baecaae4fe5762246eb264c4e617cdf472 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 27 Feb 2025 11:14:12 +0100 Subject: [PATCH 19/45] Set correct deployment status (#632) --- .github/workflows/preview-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index b8c68808c..d6a3de785 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -175,7 +175,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, deployment_id: ${{ steps.deployment.outputs.result }}, - state: "${{ steps.s3-upload.outcome == 'success' && 'success' || 'failure' }}", + state: "${{ steps.docs-build.outputs.skip == 'true' && 'inactive' || (steps.s3-upload.outcome == 'success' && 'success' || 'failure') }}", environment_url: `https://docs-v3-preview.elastic.dev${process.env.PATH_PREFIX}`, log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, }) From 4c7b972a83abd26b57cc7c571a3179ec8daff810 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 27 Feb 2025 13:42:36 +0100 Subject: [PATCH 20/45] Optimize preview build to avoid checkout on pull_request* events (#633) --- .github/workflows/preview-build.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index d6a3de785..b657dc28e 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -46,17 +46,24 @@ jobs: steps: - name: Checkout + if: contains(fromJSON('["push", "merge_group"]'), github.event_name) uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha || github.ref }} - name: Get changed files - if: startsWith(github.event_name, 'pull_request') || github.event_name == 'merge_group' + if: contains(fromJSON('["merge_group", "pull_request", "pull_request_target"]'), github.event_name) id: check-files uses: tj-actions/changed-files@d6e91a2266cdb9d62096cebf1e8546899c6aa18f # v45.0.6 with: files: ${{ inputs.path-pattern != '' && inputs.path-pattern || '**' }} - + + - name: Checkout + if: startsWith(github.event_name, 'pull_request') && steps.check-files.outputs.any_modified == 'true' + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + - name: Free Disk Space if: github.repository == 'elastic/asciidocalypse' uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 From b11ef542fe5156fe7cdb3839d9ddc1035c6f82f2 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 27 Feb 2025 14:48:48 +0100 Subject: [PATCH 21/45] Add `PrimaryNav` feature (#604) * Add latest header design and adjust pages navigation * Fix unintended merge changes * fix * Pass markdownparser to directive html renderer * Change feature flag from UpperCamelCase to kebab-case * Update docs/_docset.yml * Refactor feature flags --------- Co-authored-by: Martijn Laarman --- docs/_docset.yml | 4 + src/Elastic.Markdown/Assets/pages-nav.ts | 4 +- src/Elastic.Markdown/Assets/styles.css | 9 +- src/Elastic.Markdown/Helpers/Htmx.cs | 45 +++- .../IO/Configuration/ConfigurationFile.cs | 9 + .../IO/Configuration/FeatureFlags.cs | 11 + src/Elastic.Markdown/IO/MarkdownFile.cs | 11 +- .../Myst/Directives/DirectiveHtmlRenderer.cs | 18 +- .../Directives/DirectiveMarkdownExtension.cs | 8 +- src/Elastic.Markdown/Myst/MarkdownParser.cs | 31 +-- .../Myst/Renderers/HtmxLinkInlineRenderer.cs | 15 +- src/Elastic.Markdown/Slices/HtmlWriter.cs | 59 ++++- src/Elastic.Markdown/Slices/Index.cshtml | 2 + .../Slices/Layout/_Breadcrumbs.cshtml | 12 +- .../Slices/Layout/_Header.cshtml | 211 ++++++++++++++++-- .../Slices/Layout/_PagesNav.cshtml | 2 +- .../Slices/Layout/_PrevNextNav.cshtml | 8 +- .../Slices/Layout/_PrimaryNav.cshtml | 58 +++++ .../Layout/_PrimaryNavDropdownItem.cshtml | 14 ++ .../Slices/Layout/_TableOfContents.cshtml | 2 +- .../Slices/Layout/_TocTree.cshtml | 13 +- .../Slices/Layout/_TocTreeNav.cshtml | 54 ++--- src/Elastic.Markdown/Slices/_Layout.cshtml | 2 +- src/Elastic.Markdown/Slices/_ViewModels.cs | 37 +++ .../_static/elasticsearch-logo-color-64px.svg | 12 + .../_static/logo-elastic-horizontal-color.svg | 23 ++ .../_static/observability-logo-color-64px.svg | 12 + .../_static/security-logo-color-64px.svg | 12 + src/docs-builder/Cli/Commands.cs | 34 +-- src/docs-builder/Http/DocumentationWebHost.cs | 53 +++++ .../Directives/DirectiveBaseTests.cs | 2 +- .../Inline/AnchorLinkTests.cs | 10 +- .../Inline/DirectiveBlockLinkTests.cs | 2 +- .../Inline/InlineAnchorTests.cs | 2 +- .../Inline/InlineLinkTests.cs | 12 +- .../Inline/InlneBaseTests.cs | 2 +- 36 files changed, 657 insertions(+), 158 deletions(-) create mode 100644 src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs create mode 100644 src/Elastic.Markdown/Slices/Layout/_PrimaryNav.cshtml create mode 100644 src/Elastic.Markdown/Slices/Layout/_PrimaryNavDropdownItem.cshtml create mode 100644 src/Elastic.Markdown/_static/elasticsearch-logo-color-64px.svg create mode 100644 src/Elastic.Markdown/_static/logo-elastic-horizontal-color.svg create mode 100644 src/Elastic.Markdown/_static/observability-logo-color-64px.svg create mode 100644 src/Elastic.Markdown/_static/security-logo-color-64px.svg diff --git a/docs/_docset.yml b/docs/_docset.yml index 9434bb0f6..5a6c83126 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -9,6 +9,10 @@ subs: serverless-short: Serverless ece: "Elastic Cloud Enterprise" eck: "Elastic Cloud on Kubernetes" + +features: + primary-nav: false + toc: - file: index.md - hidden: developer-notes.md diff --git a/src/Elastic.Markdown/Assets/pages-nav.ts b/src/Elastic.Markdown/Assets/pages-nav.ts index ba8b8ff21..a5864cde0 100644 --- a/src/Elastic.Markdown/Assets/pages-nav.ts +++ b/src/Elastic.Markdown/Assets/pages-nav.ts @@ -15,9 +15,9 @@ function scrollCurrentNaviItemIntoView(nav: HTMLElement, delay: number) { const currentNavItem = $('.current', nav); expandAllParents(currentNavItem); setTimeout(() => { - if (currentNavItem && !isElementInViewport(currentNavItem)) { currentNavItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); + window.scrollTo(0, 0); } }, delay); } @@ -36,7 +36,7 @@ export function initNav() { if (!pagesNav) { return; } - const navItems = $$('a[href="' + window.location.pathname + '"]', pagesNav); + const navItems = $$('a[href="' + window.location.pathname + '"], a[href="' + window.location.pathname + '/"]', pagesNav); navItems.forEach(el => { el.classList.add('current'); }); diff --git a/src/Elastic.Markdown/Assets/styles.css b/src/Elastic.Markdown/Assets/styles.css index 112eddee4..472d55bd4 100644 --- a/src/Elastic.Markdown/Assets/styles.css +++ b/src/Elastic.Markdown/Assets/styles.css @@ -72,8 +72,9 @@ .sidebar { .sidebar-nav { - @apply sticky top-22 z-30 overflow-y-auto; + @apply sticky top-21 z-30 overflow-y-auto; max-height: calc(100vh - var(--spacing) * 22); + scrollbar-gutter: stable; } .sidebar-link { @@ -81,7 +82,9 @@ text-ink-light hover:text-black text-sm - leading-[1.2em] + text-wrap + inline-block + leading-[1.3em] tracking-[-0.02em]; } } @@ -169,7 +172,7 @@ } #pages-nav .current { - @apply text-blue-elastic!; + @apply font-semibold text-blue-elastic!; } .markdown-content { diff --git a/src/Elastic.Markdown/Helpers/Htmx.cs b/src/Elastic.Markdown/Helpers/Htmx.cs index 050af241e..9e6422fb1 100644 --- a/src/Elastic.Markdown/Helpers/Htmx.cs +++ b/src/Elastic.Markdown/Helpers/Htmx.cs @@ -2,9 +2,50 @@ // 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.Text; +using Elastic.Markdown.IO.Configuration; + namespace Elastic.Markdown.Helpers; -public class Htmx +public static class Htmx { - public static string GetHxSelectOob() => "#markdown-content,#toc-nav,#prev-next-nav,#breadcrumbs"; + public static string GetHxSelectOob(FeatureFlags features, string? pathPrefix, string currentUrl, string targetUrl) + { + HashSet selectTargets = + [ + "#primary-nav", "#secondary-nav", "#markdown-content", "#toc-nav", "#prev-next-nav", "#breadcrumbs" + ]; + if (!HasSameTopLevelGroup(pathPrefix, currentUrl, targetUrl) && features.IsPrimaryNavEnabled) + _ = selectTargets.Add("#pages-nav"); + return string.Join(',', selectTargets); + } + + public static bool HasSameTopLevelGroup(string? pathPrefix, string currentUrl, string targetUrl) + { + var startIndex = pathPrefix?.Length ?? 0; + var currentSegments = GetSegments(currentUrl[startIndex..].Trim('/')); + var targetSegments = GetSegments(targetUrl[startIndex..].Trim('/')); + return currentSegments.Length >= 1 && targetSegments.Length >= 1 && currentSegments[0] == targetSegments[0]; + } + + public static string GetPreload() => "true"; + + public static string GetHxSwap() => "none"; + public static string GetHxPushUrl() => "true"; + public static string GetHxIndicator() => "#htmx-indicator"; + + private static string[] GetSegments(string url) => url.Split('/'); + + public static string GetHxAttributes(FeatureFlags features, string? pathPrefix, string currentUrl, string targetUrl) + { + + var attributes = new StringBuilder(); + _ = attributes.Append($" hx-get={targetUrl}"); + _ = attributes.Append($" hx-select-oob={GetHxSelectOob(features, pathPrefix, currentUrl, targetUrl)}"); + _ = attributes.Append($" hx-swap={GetHxSwap()}"); + _ = attributes.Append($" hx-push-url={GetHxPushUrl()}"); + _ = attributes.Append($" hx-indicator={GetHxIndicator()}"); + _ = attributes.Append($" preload={GetPreload()}"); + return attributes.ToString(); + } } diff --git a/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs b/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs index 081252678..97e0afe36 100644 --- a/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs +++ b/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs @@ -31,6 +31,10 @@ public record ConfigurationFile : DocumentationFile private readonly Dictionary _substitutions = new(StringComparer.OrdinalIgnoreCase); public IReadOnlyDictionary Substitutions => _substitutions; + private readonly Dictionary _features = new(StringComparer.OrdinalIgnoreCase); + private FeatureFlags? _featureFlags; + public FeatureFlags Features => _featureFlags ??= new FeatureFlags(_features); + public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildContext context, int depth = 0, string parentPath = "") : base(sourceFile, rootPath) { @@ -79,6 +83,9 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon TableOfContents = entries; break; + case "features": + _features = reader.ReadDictionary(entry.Entry).ToDictionary(k => k.Key, v => bool.Parse(v.Value), StringComparer.OrdinalIgnoreCase); + break; case "external_hosts": reader.EmitWarning($"{entry.Key} has been deprecated and will be removed", entry.Key); break; @@ -97,6 +104,8 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon Globs = [.. ImplicitFolders.Select(f => Glob.Parse($"{f}/*.md"))]; } + public bool IsFeatureEnabled(string feature) => _features.TryGetValue(feature, out var enabled) && enabled; + private List ReadChildren(YamlStreamReader reader, KeyValuePair entry, string parentPath) { var entries = new List(); diff --git a/src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs b/src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs new file mode 100644 index 000000000..57055de93 --- /dev/null +++ b/src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs @@ -0,0 +1,11 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// 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 + +namespace Elastic.Markdown.IO.Configuration; + +public class FeatureFlags(Dictionary featureFlags) +{ + public bool IsPrimaryNavEnabled => IsEnabled("primary-nav"); + private bool IsEnabled(string key) => featureFlags.TryGetValue(key, out var value) && value; +} diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index 94c57e716..e99acc238 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -316,7 +316,7 @@ private YamlFrontMatter ReadYamlFrontMatter(string raw) } - public static string CreateHtml(MarkdownDocument document) + public string CreateHtml(MarkdownDocument document) { //we manually render title and optionally append an applies block embedded in yaml front matter. var h1 = document.Descendants().FirstOrDefault(h => h.Level == 1); @@ -324,4 +324,13 @@ public static string CreateHtml(MarkdownDocument document) _ = document.Remove(h1); return document.ToHtml(MarkdownParser.Pipeline); } + + public static string CreateHtml(MarkdownDocument document, MarkdownParser parser) + { + //we manually render title and optionally append an applies block embedded in yaml front matter. + var h1 = document.Descendants().FirstOrDefault(h => h.Level == 1); + if (h1 is not null) + _ = document.Remove(h1); + return document.ToHtml(parser.Pipeline); + } } diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs index 89b18c96a..1453980d2 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs @@ -21,7 +21,7 @@ namespace Elastic.Markdown.Myst.Directives; /// An HTML renderer for a . /// /// -public class DirectiveHtmlRenderer : HtmlObjectRenderer +public class DirectiveHtmlRenderer(MarkdownParser markdownParser) : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlock) { @@ -62,10 +62,10 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo if (includeBlock.Literal) WriteLiteralIncludeBlock(renderer, includeBlock); else - WriteIncludeBlock(renderer, includeBlock); + WriteIncludeBlock(renderer, includeBlock, markdownParser); return; case SettingsBlock settingsBlock: - WriteSettingsBlock(renderer, settingsBlock); + WriteSettingsBlock(renderer, settingsBlock, markdownParser); return; default: // if (!string.IsNullOrEmpty(directiveBlock.Info) && !directiveBlock.Info.StartsWith('{')) @@ -219,28 +219,24 @@ private static void WriteLiteralIncludeBlock(HtmlRenderer renderer, IncludeBlock } } - private static void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block) + private static void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block, MarkdownParser parser) { if (!block.Found || block.IncludePath is null) return; - var parser = new MarkdownParser(block.Build, block.Context); var snippet = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath); var parentPath = block.Context.MarkdownSourcePath; var document = parser.ParseSnippetAsync(snippet, parentPath, block.Context.YamlFrontMatter, default).GetAwaiter().GetResult(); - var html = document.ToHtml(MarkdownParser.Pipeline); + var html = document.ToHtml(parser.Pipeline); _ = renderer.Write(html); } - private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block) + private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block, MarkdownParser parser) { if (!block.Found || block.IncludePath is null) return; - var parser = new MarkdownParser(block.Build, block.Context); - var file = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath); - YamlSettings? settings; try { @@ -264,7 +260,7 @@ private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock bloc RenderMarkdown = s => { var document = parser.ParseEmbeddedMarkdown(s, block.IncludeFrom, block.Context.YamlFrontMatter); - var html = document.ToHtml(MarkdownParser.Pipeline); + var html = document.ToHtml(parser.Pipeline); return html; } }); diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs index 4f990e3b8..c4246baa6 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs @@ -13,9 +13,9 @@ namespace Elastic.Markdown.Myst.Directives; public static class DirectiveMarkdownBuilderExtensions { - public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder pipeline) + public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder pipeline, MarkdownParser markdownParser) { - pipeline.Extensions.AddIfNotAlready(); + pipeline.Extensions.AddIfNotAlready(new DirectiveMarkdownExtension(markdownParser)); return pipeline; } } @@ -24,7 +24,7 @@ public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder /// Extension to allow custom containers. /// /// -public class DirectiveMarkdownExtension : IMarkdownExtension +public class DirectiveMarkdownExtension(MarkdownParser markdownParser) : IMarkdownExtension { public void Setup(MarkdownPipelineBuilder pipeline) { @@ -53,7 +53,7 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) if (!renderer.ObjectRenderers.Contains()) { // Must be inserted before CodeBlockRenderer - _ = renderer.ObjectRenderers.InsertBefore(new DirectiveHtmlRenderer()); + _ = renderer.ObjectRenderers.InsertBefore(new DirectiveHtmlRenderer(markdownParser)); } _ = renderer.ObjectRenderers.Replace(new SectionedHeadingRenderer()); diff --git a/src/Elastic.Markdown/Myst/MarkdownParser.cs b/src/Elastic.Markdown/Myst/MarkdownParser.cs index 67a7e9093..befb60bba 100644 --- a/src/Elastic.Markdown/Myst/MarkdownParser.cs +++ b/src/Elastic.Markdown/Myst/MarkdownParser.cs @@ -4,6 +4,7 @@ using System.IO.Abstractions; using Cysharp.IO; +using Elastic.Markdown.IO.Configuration; using Elastic.Markdown.Myst.CodeBlocks; using Elastic.Markdown.Myst.Comments; using Elastic.Markdown.Myst.Directives; @@ -101,33 +102,33 @@ private static async Task ParseAsync( } // ReSharper disable once InconsistentNaming - private static MarkdownPipeline? MinimalPipelineCached; - private static MarkdownPipeline MinimalPipeline + private MarkdownPipeline? _minimalPipelineCached; + private MarkdownPipeline MinimalPipeline { get { - if (MinimalPipelineCached is not null) - return MinimalPipelineCached; + if (_minimalPipelineCached is not null) + return _minimalPipelineCached; var builder = new MarkdownPipelineBuilder() .UseYamlFrontMatter() .UseInlineAnchors() .UseHeadingsWithSlugs() - .UseDirectives(); + .UseDirectives(this); _ = builder.BlockParsers.TryRemove(); - MinimalPipelineCached = builder.Build(); - return MinimalPipelineCached; + _minimalPipelineCached = builder.Build(); + return _minimalPipelineCached; } } // ReSharper disable once InconsistentNaming - private static MarkdownPipeline? PipelineCached; - public static MarkdownPipeline Pipeline + private MarkdownPipeline? _pipelineCached; + public MarkdownPipeline Pipeline { get { - if (PipelineCached is not null) - return PipelineCached; + if (_pipelineCached is not null) + return _pipelineCached; var builder = new MarkdownPipelineBuilder() .UseInlineAnchors() @@ -141,15 +142,15 @@ public static MarkdownPipeline Pipeline .UseYamlFrontMatter() .UseGridTables() .UsePipeTables() - .UseDirectives() + .UseDirectives(this) .UseDefinitionLists() .UseEnhancedCodeBlocks() - .UseHtmxLinkInlineRenderer() + .UseHtmxLinkInlineRenderer(Build) .DisableHtml() .UseHardBreaks(); _ = builder.BlockParsers.TryRemove(); - PipelineCached = builder.Build(); - return PipelineCached; + _pipelineCached = builder.Build(); + return _pipelineCached; } } diff --git a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs index 094bd432b..8fae9da96 100644 --- a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Markdown.Helpers; +using Elastic.Markdown.IO.Configuration; using Markdig; using Markdig.Renderers; using Markdig.Renderers.Html.Inlines; @@ -10,7 +11,7 @@ namespace Elastic.Markdown.Myst.Renderers; -public class HtmxLinkInlineRenderer : LinkInlineRenderer +public class HtmxLinkInlineRenderer(BuildContext build) : LinkInlineRenderer { protected override void Write(HtmlRenderer renderer, LinkInline link) { @@ -30,11 +31,11 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) _ = renderer.Write(" hx-get=\""); _ = renderer.WriteEscapeUrl(link.GetDynamicUrl != null ? link.GetDynamicUrl() ?? link.Url : link.Url); _ = renderer.Write('"'); - _ = renderer.Write($" hx-select-oob=\"{Htmx.GetHxSelectOob()}\""); + _ = renderer.Write($" hx-select-oob=\"{Htmx.GetHxSelectOob(build.Configuration.Features, build.UrlPathPrefix, currentUrl, link.Url)}\""); _ = renderer.Write(" hx-swap=\"none\""); _ = renderer.Write(" hx-push-url=\"true\""); _ = renderer.Write(" hx-indicator=\"#htmx-indicator\""); - _ = renderer.Write(" preload=\"mouseover\""); + _ = renderer.Write($" preload=\"{Htmx.GetPreload()}\""); if (!string.IsNullOrEmpty(link.Title)) { @@ -62,14 +63,14 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) public static class CustomLinkInlineRendererExtensions { - public static MarkdownPipelineBuilder UseHtmxLinkInlineRenderer(this MarkdownPipelineBuilder pipeline) + public static MarkdownPipelineBuilder UseHtmxLinkInlineRenderer(this MarkdownPipelineBuilder pipeline, BuildContext build) { - pipeline.Extensions.AddIfNotAlready(); + pipeline.Extensions.AddIfNotAlready(new HtmxLinkInlineRendererExtension(build)); return pipeline; } } -public class HtmxLinkInlineRendererExtension : IMarkdownExtension +public class HtmxLinkInlineRendererExtension(BuildContext build) : IMarkdownExtension { public void Setup(MarkdownPipelineBuilder pipeline) { @@ -81,7 +82,7 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) if (renderer is HtmlRenderer htmlRenderer) { _ = htmlRenderer.ObjectRenderers.RemoveAll(x => x is LinkInlineRenderer); - htmlRenderer.ObjectRenderers.Add(new HtmxLinkInlineRenderer()); + htmlRenderer.ObjectRenderers.Add(new HtmxLinkInlineRenderer(build)); } } } diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index c4e5a1d1e..55f16caf5 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; using Elastic.Markdown.IO; +using Elastic.Markdown.IO.Configuration; +using Elastic.Markdown.IO.Navigation; using Markdig.Syntax; using RazorSlices; @@ -12,17 +14,25 @@ public class HtmlWriter(DocumentationSet documentationSet, IFileSystem writeFile { private DocumentationSet DocumentationSet { get; } = documentationSet; - private async Task RenderNavigation(MarkdownFile markdown, Cancel ctx = default) + private async Task RenderNavigation(ConfigurationFile configuration, string topLevelGroupId, MarkdownFile markdown, Cancel ctx = default) { + var group = DocumentationSet.Tree.NavigationItems + .OfType() + .FirstOrDefault(i => i.Group.Id == topLevelGroupId)?.Group; + var slice = Layout._TocTree.Create(new NavigationViewModel { - Tree = DocumentationSet.Tree, - CurrentDocument = markdown + Title = group?.Index?.NavigationTitle ?? DocumentationSet.Tree.Index!.NavigationTitle!, + TitleUrl = group?.Index?.Url ?? DocumentationSet.Tree.Index!.Url, + Tree = group ?? DocumentationSet.Tree, + CurrentDocument = markdown, + IsRoot = topLevelGroupId == DocumentationSet.Tree.Id, + Features = configuration.Features }); return await slice.RenderAsync(cancellationToken: ctx); } - private string? _renderedNavigation; + private readonly Dictionary _renderedNavigationCache = []; public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = default) { @@ -30,11 +40,41 @@ public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = defau return await RenderLayout(markdown, document, ctx); } + private static string GetTopLevelGroupId(MarkdownFile markdown) => + markdown.YieldParentGroups().Length > 1 + ? markdown.YieldParentGroups()[^2] + : markdown.YieldParentGroups()[0]; + public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument document, Cancel ctx = default) { - var html = MarkdownFile.CreateHtml(document); + var html = markdown.CreateHtml(document); await DocumentationSet.Tree.Resolve(ctx); - _renderedNavigation ??= await RenderNavigation(markdown, ctx); + + var topLevelNavigationItems = DocumentationSet.Tree.NavigationItems + .OfType() + .Select(i => i.Group); + + string? navigationHtml; + + if (DocumentationSet.Configuration.Features.IsPrimaryNavEnabled) + { + var topLevelGroupId = GetTopLevelGroupId(markdown); + if (!_renderedNavigationCache.TryGetValue(topLevelGroupId, out var value)) + { + value = await RenderNavigation(DocumentationSet.Configuration, topLevelGroupId, markdown, ctx); + _renderedNavigationCache[topLevelGroupId] = value; + } + navigationHtml = value; + } + else + { + if (!_renderedNavigationCache.TryGetValue("root", out var value)) + { + value = await RenderNavigation(DocumentationSet.Configuration, DocumentationSet.Tree.Id, markdown, ctx); + _renderedNavigationCache["root"] = value; + } + navigationHtml = value; + } var previous = DocumentationSet.GetPrevious(markdown); var next = DocumentationSet.GetNext(markdown); @@ -44,6 +84,7 @@ public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument d var path = Path.Combine(DocumentationSet.RelativeSourcePath, markdown.RelativePath); var editUrl = $"https://github.com/elastic/{remote}/edit/{branch}/{path}"; + var slice = Index.Create(new IndexViewModel { Title = markdown.Title ?? "[TITLE NOT SET]", @@ -54,11 +95,13 @@ public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument d CurrentDocument = markdown, PreviousDocument = previous, NextDocument = next, - NavigationHtml = _renderedNavigation, + TopLevelNavigationItems = [.. topLevelNavigationItems], + NavigationHtml = navigationHtml, UrlPathPrefix = markdown.UrlPathPrefix, Applies = markdown.YamlFrontMatter?.AppliesTo, GithubEditUrl = editUrl, - AllowIndexing = DocumentationSet.Build.AllowIndexing && !markdown.Hidden + AllowIndexing = DocumentationSet.Build.AllowIndexing && !markdown.Hidden, + Features = DocumentationSet.Configuration.Features }); return await slice.RenderAsync(cancellationToken: ctx); } diff --git a/src/Elastic.Markdown/Slices/Index.cshtml b/src/Elastic.Markdown/Slices/Index.cshtml index 85dfdb3e9..ec51e5529 100644 --- a/src/Elastic.Markdown/Slices/Index.cshtml +++ b/src/Elastic.Markdown/Slices/Index.cshtml @@ -11,9 +11,11 @@ Previous = Model.PreviousDocument, Next = Model.NextDocument, NavigationHtml = Model.NavigationHtml, + TopLevelNavigationItems = Model.TopLevelNavigationItems, UrlPathPrefix = Model.UrlPathPrefix, GithubEditUrl = Model.GithubEditUrl, AllowIndexing = Model.AllowIndexing, + Features = Model.Features }; }
diff --git a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml index 43999e8d6..cc71473ce 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml @@ -4,13 +4,13 @@
  • Elastic @@ -24,11 +24,11 @@ itemprop="item" href="@item.Url" hx-get="@item.Url" - hx-select-oob="@Htmx.GetHxSelectOob()" + hx-select-oob="@Htmx.GetHxSelectOob(Model.Features, Model.UrlPathPrefix, item.Url, Model.CurrentDocument.Url)" hx-swap="none" hx-push-url="true" hx-indicator="#htmx-indicator" - preload="mouseover" + preload="@Htmx.GetPreload()" > @item.NavigationTitle diff --git a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml index 08c58a81a..408653e35 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml @@ -1,27 +1,196 @@ +@using Elastic.Markdown.Helpers @inherits RazorSlice -
    -
    -
    - - Elastic - -
    -
  • } else if (item is GroupNavigation folder) { var g = folder.Group; - const int initialExpandLevel = 1; - var shouldInitiallyExpand = g.Depth <= initialExpandLevel; -
  • -
  • + @if (g.NavigationItems.Count > 0) { -
  • "; var start = html.IndexOf(find, StringComparison.Ordinal); Html = start >= 0 diff --git a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs index 526cd281b..309545177 100644 --- a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs @@ -75,7 +75,7 @@ [Sub Requirements](testing/req.md#sub-requirements) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] @@ -93,7 +93,7 @@ [Sub Requirements](testing/req.md#new-reqs) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] @@ -110,7 +110,7 @@ public class ExternalPageAnchorAutoTitleTests(ITestOutputHelper output) : Anchor public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Special Requirements > Sub Requirements

    """ + """

    Special Requirements > Sub Requirements

    """ ); [Fact] @@ -146,7 +146,7 @@ [Sub Requirements](testing/req.md#sub-requirements2) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] @@ -165,7 +165,7 @@ [Heading inside dropdown](testing/req.md#heading-inside-dropdown) public void GeneratesHtml() => // language=html Html.Should().Contain( - """Heading inside dropdown""" + """Heading inside dropdown""" ); [Fact] public void HasError() => Collector.Diagnostics.Should().HaveCount(0); diff --git a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs index d5549f6c2..6d570c2c0 100644 --- a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs @@ -66,7 +66,7 @@ [Sub Requirements](testing/req.md#hint_ref) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs index df6669b9b..f5c95cae2 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs @@ -200,7 +200,7 @@ [Sub Requirements](testing/req.md#custom-anchor) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs index c7d08cb01..999ac3656 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs @@ -41,7 +41,7 @@ public class InlineLinkTests(ITestOutputHelper output) : LinkTestBase(output, public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Elasticsearch

    """ + """

    Elasticsearch

    """ ); [Fact] @@ -58,7 +58,7 @@ public class LinkToPageTests(ITestOutputHelper output) : LinkTestBase(output, public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Requirements

    """ + """

    Requirements

    """ ); [Fact] @@ -78,7 +78,7 @@ public class InsertPageTitleTests(ITestOutputHelper output) : LinkTestBase(outpu public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Special Requirements

    """ + """

    Special Requirements

    """ ); [Fact] @@ -100,7 +100,7 @@ public class LinkReferenceTest(ITestOutputHelper output) : LinkTestBase(output, public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    test

    """ + """

    test

    """ ); [Fact] @@ -231,10 +231,10 @@ public void GeneratesHtml() => Html.TrimEnd().Should().Be("""

    Links:

    """); diff --git a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs index 2ef781cd7..700c51004 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs @@ -129,7 +129,7 @@ public virtual async ValueTask InitializeAsync() await Set.LinkResolver.FetchLinks(); Document = await File.ParseFullAsync(default); - var html = MarkdownFile.CreateHtml(Document).AsSpan(); + var html = File.CreateHtml(Document).AsSpan(); var find = "\n"; var start = html.IndexOf(find, StringComparison.Ordinal); Html = start >= 0 && !TestingFullDocument From 36b6f0a3effed63e056d344f6bb0142fd6f4b660 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 27 Feb 2025 15:06:42 +0100 Subject: [PATCH 22/45] Revert "Add `PrimaryNav` feature (#604)" (#635) This reverts commit 8edd22a6da06dfe0bb8484d7a78142a5f2d9024d. --- docs/_docset.yml | 4 - src/Elastic.Markdown/Assets/pages-nav.ts | 4 +- src/Elastic.Markdown/Assets/styles.css | 9 +- src/Elastic.Markdown/Helpers/Htmx.cs | 45 +--- .../IO/Configuration/ConfigurationFile.cs | 9 - .../IO/Configuration/FeatureFlags.cs | 11 - src/Elastic.Markdown/IO/MarkdownFile.cs | 11 +- .../Myst/Directives/DirectiveHtmlRenderer.cs | 18 +- .../Directives/DirectiveMarkdownExtension.cs | 8 +- src/Elastic.Markdown/Myst/MarkdownParser.cs | 31 ++- .../Myst/Renderers/HtmxLinkInlineRenderer.cs | 15 +- src/Elastic.Markdown/Slices/HtmlWriter.cs | 59 +---- src/Elastic.Markdown/Slices/Index.cshtml | 2 - .../Slices/Layout/_Breadcrumbs.cshtml | 12 +- .../Slices/Layout/_Header.cshtml | 211 ++---------------- .../Slices/Layout/_PagesNav.cshtml | 2 +- .../Slices/Layout/_PrevNextNav.cshtml | 8 +- .../Slices/Layout/_PrimaryNav.cshtml | 58 ----- .../Layout/_PrimaryNavDropdownItem.cshtml | 14 -- .../Slices/Layout/_TableOfContents.cshtml | 2 +- .../Slices/Layout/_TocTree.cshtml | 13 +- .../Slices/Layout/_TocTreeNav.cshtml | 54 +++-- src/Elastic.Markdown/Slices/_Layout.cshtml | 2 +- src/Elastic.Markdown/Slices/_ViewModels.cs | 37 --- .../_static/elasticsearch-logo-color-64px.svg | 12 - .../_static/logo-elastic-horizontal-color.svg | 23 -- .../_static/observability-logo-color-64px.svg | 12 - .../_static/security-logo-color-64px.svg | 12 - src/docs-builder/Cli/Commands.cs | 34 ++- src/docs-builder/Http/DocumentationWebHost.cs | 53 ----- .../Directives/DirectiveBaseTests.cs | 2 +- .../Inline/AnchorLinkTests.cs | 10 +- .../Inline/DirectiveBlockLinkTests.cs | 2 +- .../Inline/InlineAnchorTests.cs | 2 +- .../Inline/InlineLinkTests.cs | 12 +- .../Inline/InlneBaseTests.cs | 2 +- 36 files changed, 158 insertions(+), 657 deletions(-) delete mode 100644 src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs delete mode 100644 src/Elastic.Markdown/Slices/Layout/_PrimaryNav.cshtml delete mode 100644 src/Elastic.Markdown/Slices/Layout/_PrimaryNavDropdownItem.cshtml delete mode 100644 src/Elastic.Markdown/_static/elasticsearch-logo-color-64px.svg delete mode 100644 src/Elastic.Markdown/_static/logo-elastic-horizontal-color.svg delete mode 100644 src/Elastic.Markdown/_static/observability-logo-color-64px.svg delete mode 100644 src/Elastic.Markdown/_static/security-logo-color-64px.svg diff --git a/docs/_docset.yml b/docs/_docset.yml index 5a6c83126..9434bb0f6 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -9,10 +9,6 @@ subs: serverless-short: Serverless ece: "Elastic Cloud Enterprise" eck: "Elastic Cloud on Kubernetes" - -features: - primary-nav: false - toc: - file: index.md - hidden: developer-notes.md diff --git a/src/Elastic.Markdown/Assets/pages-nav.ts b/src/Elastic.Markdown/Assets/pages-nav.ts index a5864cde0..ba8b8ff21 100644 --- a/src/Elastic.Markdown/Assets/pages-nav.ts +++ b/src/Elastic.Markdown/Assets/pages-nav.ts @@ -15,9 +15,9 @@ function scrollCurrentNaviItemIntoView(nav: HTMLElement, delay: number) { const currentNavItem = $('.current', nav); expandAllParents(currentNavItem); setTimeout(() => { + if (currentNavItem && !isElementInViewport(currentNavItem)) { currentNavItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); - window.scrollTo(0, 0); } }, delay); } @@ -36,7 +36,7 @@ export function initNav() { if (!pagesNav) { return; } - const navItems = $$('a[href="' + window.location.pathname + '"], a[href="' + window.location.pathname + '/"]', pagesNav); + const navItems = $$('a[href="' + window.location.pathname + '"]', pagesNav); navItems.forEach(el => { el.classList.add('current'); }); diff --git a/src/Elastic.Markdown/Assets/styles.css b/src/Elastic.Markdown/Assets/styles.css index 472d55bd4..112eddee4 100644 --- a/src/Elastic.Markdown/Assets/styles.css +++ b/src/Elastic.Markdown/Assets/styles.css @@ -72,9 +72,8 @@ .sidebar { .sidebar-nav { - @apply sticky top-21 z-30 overflow-y-auto; + @apply sticky top-22 z-30 overflow-y-auto; max-height: calc(100vh - var(--spacing) * 22); - scrollbar-gutter: stable; } .sidebar-link { @@ -82,9 +81,7 @@ text-ink-light hover:text-black text-sm - text-wrap - inline-block - leading-[1.3em] + leading-[1.2em] tracking-[-0.02em]; } } @@ -172,7 +169,7 @@ } #pages-nav .current { - @apply font-semibold text-blue-elastic!; + @apply text-blue-elastic!; } .markdown-content { diff --git a/src/Elastic.Markdown/Helpers/Htmx.cs b/src/Elastic.Markdown/Helpers/Htmx.cs index 9e6422fb1..050af241e 100644 --- a/src/Elastic.Markdown/Helpers/Htmx.cs +++ b/src/Elastic.Markdown/Helpers/Htmx.cs @@ -2,50 +2,9 @@ // 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.Text; -using Elastic.Markdown.IO.Configuration; - namespace Elastic.Markdown.Helpers; -public static class Htmx +public class Htmx { - public static string GetHxSelectOob(FeatureFlags features, string? pathPrefix, string currentUrl, string targetUrl) - { - HashSet selectTargets = - [ - "#primary-nav", "#secondary-nav", "#markdown-content", "#toc-nav", "#prev-next-nav", "#breadcrumbs" - ]; - if (!HasSameTopLevelGroup(pathPrefix, currentUrl, targetUrl) && features.IsPrimaryNavEnabled) - _ = selectTargets.Add("#pages-nav"); - return string.Join(',', selectTargets); - } - - public static bool HasSameTopLevelGroup(string? pathPrefix, string currentUrl, string targetUrl) - { - var startIndex = pathPrefix?.Length ?? 0; - var currentSegments = GetSegments(currentUrl[startIndex..].Trim('/')); - var targetSegments = GetSegments(targetUrl[startIndex..].Trim('/')); - return currentSegments.Length >= 1 && targetSegments.Length >= 1 && currentSegments[0] == targetSegments[0]; - } - - public static string GetPreload() => "true"; - - public static string GetHxSwap() => "none"; - public static string GetHxPushUrl() => "true"; - public static string GetHxIndicator() => "#htmx-indicator"; - - private static string[] GetSegments(string url) => url.Split('/'); - - public static string GetHxAttributes(FeatureFlags features, string? pathPrefix, string currentUrl, string targetUrl) - { - - var attributes = new StringBuilder(); - _ = attributes.Append($" hx-get={targetUrl}"); - _ = attributes.Append($" hx-select-oob={GetHxSelectOob(features, pathPrefix, currentUrl, targetUrl)}"); - _ = attributes.Append($" hx-swap={GetHxSwap()}"); - _ = attributes.Append($" hx-push-url={GetHxPushUrl()}"); - _ = attributes.Append($" hx-indicator={GetHxIndicator()}"); - _ = attributes.Append($" preload={GetPreload()}"); - return attributes.ToString(); - } + public static string GetHxSelectOob() => "#markdown-content,#toc-nav,#prev-next-nav,#breadcrumbs"; } diff --git a/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs b/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs index 97e0afe36..081252678 100644 --- a/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs +++ b/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs @@ -31,10 +31,6 @@ public record ConfigurationFile : DocumentationFile private readonly Dictionary _substitutions = new(StringComparer.OrdinalIgnoreCase); public IReadOnlyDictionary Substitutions => _substitutions; - private readonly Dictionary _features = new(StringComparer.OrdinalIgnoreCase); - private FeatureFlags? _featureFlags; - public FeatureFlags Features => _featureFlags ??= new FeatureFlags(_features); - public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildContext context, int depth = 0, string parentPath = "") : base(sourceFile, rootPath) { @@ -83,9 +79,6 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon TableOfContents = entries; break; - case "features": - _features = reader.ReadDictionary(entry.Entry).ToDictionary(k => k.Key, v => bool.Parse(v.Value), StringComparer.OrdinalIgnoreCase); - break; case "external_hosts": reader.EmitWarning($"{entry.Key} has been deprecated and will be removed", entry.Key); break; @@ -104,8 +97,6 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon Globs = [.. ImplicitFolders.Select(f => Glob.Parse($"{f}/*.md"))]; } - public bool IsFeatureEnabled(string feature) => _features.TryGetValue(feature, out var enabled) && enabled; - private List ReadChildren(YamlStreamReader reader, KeyValuePair entry, string parentPath) { var entries = new List(); diff --git a/src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs b/src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs deleted file mode 100644 index 57055de93..000000000 --- a/src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// 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 - -namespace Elastic.Markdown.IO.Configuration; - -public class FeatureFlags(Dictionary featureFlags) -{ - public bool IsPrimaryNavEnabled => IsEnabled("primary-nav"); - private bool IsEnabled(string key) => featureFlags.TryGetValue(key, out var value) && value; -} diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index e99acc238..94c57e716 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -316,7 +316,7 @@ private YamlFrontMatter ReadYamlFrontMatter(string raw) } - public string CreateHtml(MarkdownDocument document) + public static string CreateHtml(MarkdownDocument document) { //we manually render title and optionally append an applies block embedded in yaml front matter. var h1 = document.Descendants().FirstOrDefault(h => h.Level == 1); @@ -324,13 +324,4 @@ public string CreateHtml(MarkdownDocument document) _ = document.Remove(h1); return document.ToHtml(MarkdownParser.Pipeline); } - - public static string CreateHtml(MarkdownDocument document, MarkdownParser parser) - { - //we manually render title and optionally append an applies block embedded in yaml front matter. - var h1 = document.Descendants().FirstOrDefault(h => h.Level == 1); - if (h1 is not null) - _ = document.Remove(h1); - return document.ToHtml(parser.Pipeline); - } } diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs index 1453980d2..89b18c96a 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs @@ -21,7 +21,7 @@ namespace Elastic.Markdown.Myst.Directives; /// An HTML renderer for a . /// /// -public class DirectiveHtmlRenderer(MarkdownParser markdownParser) : HtmlObjectRenderer +public class DirectiveHtmlRenderer : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlock) { @@ -62,10 +62,10 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo if (includeBlock.Literal) WriteLiteralIncludeBlock(renderer, includeBlock); else - WriteIncludeBlock(renderer, includeBlock, markdownParser); + WriteIncludeBlock(renderer, includeBlock); return; case SettingsBlock settingsBlock: - WriteSettingsBlock(renderer, settingsBlock, markdownParser); + WriteSettingsBlock(renderer, settingsBlock); return; default: // if (!string.IsNullOrEmpty(directiveBlock.Info) && !directiveBlock.Info.StartsWith('{')) @@ -219,24 +219,28 @@ private static void WriteLiteralIncludeBlock(HtmlRenderer renderer, IncludeBlock } } - private static void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block, MarkdownParser parser) + private static void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block) { if (!block.Found || block.IncludePath is null) return; + var parser = new MarkdownParser(block.Build, block.Context); var snippet = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath); var parentPath = block.Context.MarkdownSourcePath; var document = parser.ParseSnippetAsync(snippet, parentPath, block.Context.YamlFrontMatter, default).GetAwaiter().GetResult(); - var html = document.ToHtml(parser.Pipeline); + var html = document.ToHtml(MarkdownParser.Pipeline); _ = renderer.Write(html); } - private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block, MarkdownParser parser) + private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block) { if (!block.Found || block.IncludePath is null) return; + var parser = new MarkdownParser(block.Build, block.Context); + var file = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath); + YamlSettings? settings; try { @@ -260,7 +264,7 @@ private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock bloc RenderMarkdown = s => { var document = parser.ParseEmbeddedMarkdown(s, block.IncludeFrom, block.Context.YamlFrontMatter); - var html = document.ToHtml(parser.Pipeline); + var html = document.ToHtml(MarkdownParser.Pipeline); return html; } }); diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs index c4246baa6..4f990e3b8 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs @@ -13,9 +13,9 @@ namespace Elastic.Markdown.Myst.Directives; public static class DirectiveMarkdownBuilderExtensions { - public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder pipeline, MarkdownParser markdownParser) + public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder pipeline) { - pipeline.Extensions.AddIfNotAlready(new DirectiveMarkdownExtension(markdownParser)); + pipeline.Extensions.AddIfNotAlready(); return pipeline; } } @@ -24,7 +24,7 @@ public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder /// Extension to allow custom containers. /// /// -public class DirectiveMarkdownExtension(MarkdownParser markdownParser) : IMarkdownExtension +public class DirectiveMarkdownExtension : IMarkdownExtension { public void Setup(MarkdownPipelineBuilder pipeline) { @@ -53,7 +53,7 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) if (!renderer.ObjectRenderers.Contains()) { // Must be inserted before CodeBlockRenderer - _ = renderer.ObjectRenderers.InsertBefore(new DirectiveHtmlRenderer(markdownParser)); + _ = renderer.ObjectRenderers.InsertBefore(new DirectiveHtmlRenderer()); } _ = renderer.ObjectRenderers.Replace(new SectionedHeadingRenderer()); diff --git a/src/Elastic.Markdown/Myst/MarkdownParser.cs b/src/Elastic.Markdown/Myst/MarkdownParser.cs index befb60bba..67a7e9093 100644 --- a/src/Elastic.Markdown/Myst/MarkdownParser.cs +++ b/src/Elastic.Markdown/Myst/MarkdownParser.cs @@ -4,7 +4,6 @@ using System.IO.Abstractions; using Cysharp.IO; -using Elastic.Markdown.IO.Configuration; using Elastic.Markdown.Myst.CodeBlocks; using Elastic.Markdown.Myst.Comments; using Elastic.Markdown.Myst.Directives; @@ -102,33 +101,33 @@ private static async Task ParseAsync( } // ReSharper disable once InconsistentNaming - private MarkdownPipeline? _minimalPipelineCached; - private MarkdownPipeline MinimalPipeline + private static MarkdownPipeline? MinimalPipelineCached; + private static MarkdownPipeline MinimalPipeline { get { - if (_minimalPipelineCached is not null) - return _minimalPipelineCached; + if (MinimalPipelineCached is not null) + return MinimalPipelineCached; var builder = new MarkdownPipelineBuilder() .UseYamlFrontMatter() .UseInlineAnchors() .UseHeadingsWithSlugs() - .UseDirectives(this); + .UseDirectives(); _ = builder.BlockParsers.TryRemove(); - _minimalPipelineCached = builder.Build(); - return _minimalPipelineCached; + MinimalPipelineCached = builder.Build(); + return MinimalPipelineCached; } } // ReSharper disable once InconsistentNaming - private MarkdownPipeline? _pipelineCached; - public MarkdownPipeline Pipeline + private static MarkdownPipeline? PipelineCached; + public static MarkdownPipeline Pipeline { get { - if (_pipelineCached is not null) - return _pipelineCached; + if (PipelineCached is not null) + return PipelineCached; var builder = new MarkdownPipelineBuilder() .UseInlineAnchors() @@ -142,15 +141,15 @@ public MarkdownPipeline Pipeline .UseYamlFrontMatter() .UseGridTables() .UsePipeTables() - .UseDirectives(this) + .UseDirectives() .UseDefinitionLists() .UseEnhancedCodeBlocks() - .UseHtmxLinkInlineRenderer(Build) + .UseHtmxLinkInlineRenderer() .DisableHtml() .UseHardBreaks(); _ = builder.BlockParsers.TryRemove(); - _pipelineCached = builder.Build(); - return _pipelineCached; + PipelineCached = builder.Build(); + return PipelineCached; } } diff --git a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs index 8fae9da96..094bd432b 100644 --- a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information using Elastic.Markdown.Helpers; -using Elastic.Markdown.IO.Configuration; using Markdig; using Markdig.Renderers; using Markdig.Renderers.Html.Inlines; @@ -11,7 +10,7 @@ namespace Elastic.Markdown.Myst.Renderers; -public class HtmxLinkInlineRenderer(BuildContext build) : LinkInlineRenderer +public class HtmxLinkInlineRenderer : LinkInlineRenderer { protected override void Write(HtmlRenderer renderer, LinkInline link) { @@ -31,11 +30,11 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) _ = renderer.Write(" hx-get=\""); _ = renderer.WriteEscapeUrl(link.GetDynamicUrl != null ? link.GetDynamicUrl() ?? link.Url : link.Url); _ = renderer.Write('"'); - _ = renderer.Write($" hx-select-oob=\"{Htmx.GetHxSelectOob(build.Configuration.Features, build.UrlPathPrefix, currentUrl, link.Url)}\""); + _ = renderer.Write($" hx-select-oob=\"{Htmx.GetHxSelectOob()}\""); _ = renderer.Write(" hx-swap=\"none\""); _ = renderer.Write(" hx-push-url=\"true\""); _ = renderer.Write(" hx-indicator=\"#htmx-indicator\""); - _ = renderer.Write($" preload=\"{Htmx.GetPreload()}\""); + _ = renderer.Write(" preload=\"mouseover\""); if (!string.IsNullOrEmpty(link.Title)) { @@ -63,14 +62,14 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) public static class CustomLinkInlineRendererExtensions { - public static MarkdownPipelineBuilder UseHtmxLinkInlineRenderer(this MarkdownPipelineBuilder pipeline, BuildContext build) + public static MarkdownPipelineBuilder UseHtmxLinkInlineRenderer(this MarkdownPipelineBuilder pipeline) { - pipeline.Extensions.AddIfNotAlready(new HtmxLinkInlineRendererExtension(build)); + pipeline.Extensions.AddIfNotAlready(); return pipeline; } } -public class HtmxLinkInlineRendererExtension(BuildContext build) : IMarkdownExtension +public class HtmxLinkInlineRendererExtension : IMarkdownExtension { public void Setup(MarkdownPipelineBuilder pipeline) { @@ -82,7 +81,7 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) if (renderer is HtmlRenderer htmlRenderer) { _ = htmlRenderer.ObjectRenderers.RemoveAll(x => x is LinkInlineRenderer); - htmlRenderer.ObjectRenderers.Add(new HtmxLinkInlineRenderer(build)); + htmlRenderer.ObjectRenderers.Add(new HtmxLinkInlineRenderer()); } } } diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index 55f16caf5..c4e5a1d1e 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -3,8 +3,6 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; using Elastic.Markdown.IO; -using Elastic.Markdown.IO.Configuration; -using Elastic.Markdown.IO.Navigation; using Markdig.Syntax; using RazorSlices; @@ -14,25 +12,17 @@ public class HtmlWriter(DocumentationSet documentationSet, IFileSystem writeFile { private DocumentationSet DocumentationSet { get; } = documentationSet; - private async Task RenderNavigation(ConfigurationFile configuration, string topLevelGroupId, MarkdownFile markdown, Cancel ctx = default) + private async Task RenderNavigation(MarkdownFile markdown, Cancel ctx = default) { - var group = DocumentationSet.Tree.NavigationItems - .OfType() - .FirstOrDefault(i => i.Group.Id == topLevelGroupId)?.Group; - var slice = Layout._TocTree.Create(new NavigationViewModel { - Title = group?.Index?.NavigationTitle ?? DocumentationSet.Tree.Index!.NavigationTitle!, - TitleUrl = group?.Index?.Url ?? DocumentationSet.Tree.Index!.Url, - Tree = group ?? DocumentationSet.Tree, - CurrentDocument = markdown, - IsRoot = topLevelGroupId == DocumentationSet.Tree.Id, - Features = configuration.Features + Tree = DocumentationSet.Tree, + CurrentDocument = markdown }); return await slice.RenderAsync(cancellationToken: ctx); } - private readonly Dictionary _renderedNavigationCache = []; + private string? _renderedNavigation; public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = default) { @@ -40,41 +30,11 @@ public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = defau return await RenderLayout(markdown, document, ctx); } - private static string GetTopLevelGroupId(MarkdownFile markdown) => - markdown.YieldParentGroups().Length > 1 - ? markdown.YieldParentGroups()[^2] - : markdown.YieldParentGroups()[0]; - public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument document, Cancel ctx = default) { - var html = markdown.CreateHtml(document); + var html = MarkdownFile.CreateHtml(document); await DocumentationSet.Tree.Resolve(ctx); - - var topLevelNavigationItems = DocumentationSet.Tree.NavigationItems - .OfType() - .Select(i => i.Group); - - string? navigationHtml; - - if (DocumentationSet.Configuration.Features.IsPrimaryNavEnabled) - { - var topLevelGroupId = GetTopLevelGroupId(markdown); - if (!_renderedNavigationCache.TryGetValue(topLevelGroupId, out var value)) - { - value = await RenderNavigation(DocumentationSet.Configuration, topLevelGroupId, markdown, ctx); - _renderedNavigationCache[topLevelGroupId] = value; - } - navigationHtml = value; - } - else - { - if (!_renderedNavigationCache.TryGetValue("root", out var value)) - { - value = await RenderNavigation(DocumentationSet.Configuration, DocumentationSet.Tree.Id, markdown, ctx); - _renderedNavigationCache["root"] = value; - } - navigationHtml = value; - } + _renderedNavigation ??= await RenderNavigation(markdown, ctx); var previous = DocumentationSet.GetPrevious(markdown); var next = DocumentationSet.GetNext(markdown); @@ -84,7 +44,6 @@ public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument d var path = Path.Combine(DocumentationSet.RelativeSourcePath, markdown.RelativePath); var editUrl = $"https://github.com/elastic/{remote}/edit/{branch}/{path}"; - var slice = Index.Create(new IndexViewModel { Title = markdown.Title ?? "[TITLE NOT SET]", @@ -95,13 +54,11 @@ public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument d CurrentDocument = markdown, PreviousDocument = previous, NextDocument = next, - TopLevelNavigationItems = [.. topLevelNavigationItems], - NavigationHtml = navigationHtml, + NavigationHtml = _renderedNavigation, UrlPathPrefix = markdown.UrlPathPrefix, Applies = markdown.YamlFrontMatter?.AppliesTo, GithubEditUrl = editUrl, - AllowIndexing = DocumentationSet.Build.AllowIndexing && !markdown.Hidden, - Features = DocumentationSet.Configuration.Features + AllowIndexing = DocumentationSet.Build.AllowIndexing && !markdown.Hidden }); return await slice.RenderAsync(cancellationToken: ctx); } diff --git a/src/Elastic.Markdown/Slices/Index.cshtml b/src/Elastic.Markdown/Slices/Index.cshtml index ec51e5529..85dfdb3e9 100644 --- a/src/Elastic.Markdown/Slices/Index.cshtml +++ b/src/Elastic.Markdown/Slices/Index.cshtml @@ -11,11 +11,9 @@ Previous = Model.PreviousDocument, Next = Model.NextDocument, NavigationHtml = Model.NavigationHtml, - TopLevelNavigationItems = Model.TopLevelNavigationItems, UrlPathPrefix = Model.UrlPathPrefix, GithubEditUrl = Model.GithubEditUrl, AllowIndexing = Model.AllowIndexing, - Features = Model.Features }; }
    diff --git a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml index cc71473ce..43999e8d6 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml @@ -4,13 +4,13 @@
  • Elastic @@ -24,11 +24,11 @@ itemprop="item" href="@item.Url" hx-get="@item.Url" - hx-select-oob="@Htmx.GetHxSelectOob(Model.Features, Model.UrlPathPrefix, item.Url, Model.CurrentDocument.Url)" + hx-select-oob="@Htmx.GetHxSelectOob()" hx-swap="none" hx-push-url="true" hx-indicator="#htmx-indicator" - preload="@Htmx.GetPreload()" + preload="mouseover" > @item.NavigationTitle diff --git a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml index 408653e35..08c58a81a 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml @@ -1,196 +1,27 @@ -@using Elastic.Markdown.Helpers @inherits RazorSlice - -@{ - string GetHxAttributes(string url) - { - return Htmx.GetHxAttributes(Model.Features, Model.UrlPathPrefix, Model.CurrentDocument.Url, url); - } - - var primaryNavViewModel = new PrimaryNavViewModel - { - Items = - [ - new PrimaryNavItemViewModel - { - Title = "Get Started", - HtmxAttributes = GetHxAttributes(Model.Link("/get-started")), - Url = Model.Link("/get-started"), - }, - new PrimaryNavItemViewModel - { - Title = "Solutions and use cases", - Url = Model.Link("/solutions"), - HtmxAttributes = GetHxAttributes(Model.Link("/solutions")), - DropdownItems = [ - new PrimaryNavDropdownItemViewModel - { - IconPath = Model.Static("observability-logo-color-64px.svg"), - IconAlt = "Observability logo", - Title = "Observability", - Description = "Unify app and infrastructure visibility to proactively resolve issues.", - Url = Model.Link("/solutions/observability"), - HtmxAttributes = Htmx.GetHxAttributes( - Model.Features, - Model.CurrentDocument.UrlPathPrefix, - Model.CurrentDocument.Url, - Model.Link("/solutions/security") - ) - }, - new PrimaryNavDropdownItemViewModel - { - IconPath = Model.Static("security-logo-color-64px.svg"), - IconAlt = "Security logo", - Title = "Security", - Description = "Protect, investigate, and respond to cyber threats with AI-driven security analytics.", - Url = Model.Link("/solutions/security"), - HtmxAttributes = GetHxAttributes(Model.Link("/solutions/security")) - }, - new PrimaryNavDropdownItemViewModel - { - IconPath = Model.Static("elasticsearch-logo-color-64px.svg"), - IconAlt = "Search logo", - Title = "Search", - Description = "Discover a world of AI possibilities — built with the power of search.", - Url = Model.Link("/solutions/search"), - HtmxAttributes = GetHxAttributes(Model.Link("/solutions/search")) - } - ] - }, - new PrimaryNavItemViewModel - { - Title = "Work with the stack", - DropdownItems = [ - new PrimaryNavDropdownItemViewModel - { - Title = "Manage data", - Description = "Discover a world of AI possibilities — built with the power of search.", - Url = Model.Link("/manage-data"), - HtmxAttributes = GetHxAttributes(Model.Link("/manage-data")) - }, - new PrimaryNavDropdownItemViewModel - { - Title = "Explore and analyze", - Description = "Unify app and infrastructure visibility to proactively resolve issues.", - Url = Model.Link("/explore-analyze"), - HtmxAttributes = GetHxAttributes(Model.Link("/explore-analyze")) - }, - new PrimaryNavDropdownItemViewModel - { - Title = "Deploy and manage", - Description = "Unify app and infrastructure visibility to proactively resolve issues.", - Url = Model.Link("/deploy-manage"), - HtmxAttributes = GetHxAttributes(Model.Link("/deploy-manage")) - - }, - new PrimaryNavDropdownItemViewModel - { - Title = "Manage your cloud", - Description = "Unify app and infrastructure visibility to proactively resolve issues.", - Url = Model.Link("/cloud-account"), - HtmxAttributes = GetHxAttributes(Model.Link("/cloud-account")) - }, - ] - }, - new PrimaryNavItemViewModel - { - Title = "Troubleshoot", - HtmxAttributes = GetHxAttributes(Model.Link("/troubleshoot")), - Url = Model.Link("/troubleshoot"), - }, - ] - }; -} - -
    - -
    -
    -
    -
    -
    - - Elastic - -
    - @if (Model.Features.IsPrimaryNavEnabled) - { - @await RenderPartialAsync(_PrimaryNav.Create(primaryNavViewModel)) - } - else - { -
    - } -
  • "; var start = html.IndexOf(find, StringComparison.Ordinal); Html = start >= 0 diff --git a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs index 309545177..526cd281b 100644 --- a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs @@ -75,7 +75,7 @@ [Sub Requirements](testing/req.md#sub-requirements) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] @@ -93,7 +93,7 @@ [Sub Requirements](testing/req.md#new-reqs) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] @@ -110,7 +110,7 @@ public class ExternalPageAnchorAutoTitleTests(ITestOutputHelper output) : Anchor public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Special Requirements > Sub Requirements

    """ + """

    Special Requirements > Sub Requirements

    """ ); [Fact] @@ -146,7 +146,7 @@ [Sub Requirements](testing/req.md#sub-requirements2) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] @@ -165,7 +165,7 @@ [Heading inside dropdown](testing/req.md#heading-inside-dropdown) public void GeneratesHtml() => // language=html Html.Should().Contain( - """Heading inside dropdown""" + """Heading inside dropdown""" ); [Fact] public void HasError() => Collector.Diagnostics.Should().HaveCount(0); diff --git a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs index 6d570c2c0..d5549f6c2 100644 --- a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs @@ -66,7 +66,7 @@ [Sub Requirements](testing/req.md#hint_ref) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs index f5c95cae2..df6669b9b 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs @@ -200,7 +200,7 @@ [Sub Requirements](testing/req.md#custom-anchor) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs index 999ac3656..c7d08cb01 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs @@ -41,7 +41,7 @@ public class InlineLinkTests(ITestOutputHelper output) : LinkTestBase(output, public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Elasticsearch

    """ + """

    Elasticsearch

    """ ); [Fact] @@ -58,7 +58,7 @@ public class LinkToPageTests(ITestOutputHelper output) : LinkTestBase(output, public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Requirements

    """ + """

    Requirements

    """ ); [Fact] @@ -78,7 +78,7 @@ public class InsertPageTitleTests(ITestOutputHelper output) : LinkTestBase(outpu public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Special Requirements

    """ + """

    Special Requirements

    """ ); [Fact] @@ -100,7 +100,7 @@ public class LinkReferenceTest(ITestOutputHelper output) : LinkTestBase(output, public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    test

    """ + """

    test

    """ ); [Fact] @@ -231,10 +231,10 @@ public void GeneratesHtml() => Html.TrimEnd().Should().Be("""

    Links:

    """); diff --git a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs index 700c51004..2ef781cd7 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs @@ -129,7 +129,7 @@ public virtual async ValueTask InitializeAsync() await Set.LinkResolver.FetchLinks(); Document = await File.ParseFullAsync(default); - var html = File.CreateHtml(Document).AsSpan(); + var html = MarkdownFile.CreateHtml(Document).AsSpan(); var find = "\n"; var start = html.IndexOf(find, StringComparison.Ordinal); Html = start >= 0 && !TestingFullDocument From 2494506f38e6a7ff2fac1fbe5456fc2846693004 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 27 Feb 2025 16:01:35 +0100 Subject: [PATCH 23/45] Add smoke test (#637) * Add smoke test * fix --- .github/workflows/smoke-test.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/smoke-test.yml diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml new file mode 100644 index 000000000..6a93b7030 --- /dev/null +++ b/.github/workflows/smoke-test.yml @@ -0,0 +1,28 @@ +name: smoke-tests + +on: + pull_request: ~ + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - repository: elastic/docs-content + - repository: elastic/apm-agent-android + - repository: elastic/cloud-on-k8s + + steps: + - uses: actions/checkout@v4 + - name: Bootstrap Action Workspace + uses: ./.github/actions/bootstrap + + - uses: actions/checkout@v4 + with: + repository: ${{ matrix.repository }} + path: test-repo + + - name: Build documentation + run: | + dotnet run --project src/docs-builder -- --strict --path-prefix "/docs" -p test-repo From 04b29d7012557e20da56fe17a3cefafcf3e67324 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 27 Feb 2025 17:58:14 +0100 Subject: [PATCH 24/45] Run free-disk-space action in security-docs (#639) --- .github/workflows/preview-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index b657dc28e..f9793c0a9 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -65,7 +65,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.ref }} - name: Free Disk Space - if: github.repository == 'elastic/asciidocalypse' + if: contains(fromJSON('["elastic/asciidocalypse", "elastic/security-docs"]'), github.repository) uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 with: tool-cache: false From 9e445c4c7af1e30bf7ecffd172cf8c06c9806421 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 27 Feb 2025 18:04:30 +0100 Subject: [PATCH 25/45] Add `primary-nav` feature (#636) * Add latest header design and adjust pages navigation * Fix unintended merge changes * fix * Pass markdownparser to directive html renderer * Change feature flag from UpperCamelCase to kebab-case * Update docs/_docset.yml * Refactor feature flags * Fix case where targetUrl is null or empty * Revert Commands.cs * Don't fail-fast on matrix * Handle the case where there is no root index.md file * Fix * test * ok * fix * Fix --------- Co-authored-by: Martijn Laarman --- .github/workflows/smoke-test.yml | 1 + docs/_docset.yml | 4 + src/Elastic.Markdown/Assets/pages-nav.ts | 4 +- src/Elastic.Markdown/Assets/styles.css | 9 +- src/Elastic.Markdown/Helpers/Htmx.cs | 65 +++++- .../IO/Configuration/ConfigurationFile.cs | 9 + .../IO/Configuration/FeatureFlags.cs | 11 + src/Elastic.Markdown/IO/MarkdownFile.cs | 11 +- .../Myst/Directives/DirectiveHtmlRenderer.cs | 18 +- .../Directives/DirectiveMarkdownExtension.cs | 8 +- src/Elastic.Markdown/Myst/MarkdownParser.cs | 31 +-- .../Myst/Renderers/HtmxLinkInlineRenderer.cs | 15 +- src/Elastic.Markdown/Slices/HtmlWriter.cs | 59 ++++- src/Elastic.Markdown/Slices/Index.cshtml | 2 + .../Slices/Layout/_Breadcrumbs.cshtml | 12 +- .../Slices/Layout/_Header.cshtml | 211 ++++++++++++++++-- .../Slices/Layout/_PagesNav.cshtml | 2 +- .../Slices/Layout/_PrevNextNav.cshtml | 8 +- .../Slices/Layout/_PrimaryNav.cshtml | 58 +++++ .../Layout/_PrimaryNavDropdownItem.cshtml | 14 ++ .../Slices/Layout/_TableOfContents.cshtml | 2 +- .../Slices/Layout/_TocTree.cshtml | 13 +- .../Slices/Layout/_TocTreeNav.cshtml | 54 ++--- src/Elastic.Markdown/Slices/_Layout.cshtml | 2 +- src/Elastic.Markdown/Slices/_ViewModels.cs | 37 +++ .../_static/elasticsearch-logo-color-64px.svg | 12 + .../_static/logo-elastic-horizontal-color.svg | 23 ++ .../_static/observability-logo-color-64px.svg | 12 + .../_static/security-logo-color-64px.svg | 12 + src/docs-builder/Http/DocumentationWebHost.cs | 53 +++++ .../Directives/DirectiveBaseTests.cs | 2 +- .../Inline/AnchorLinkTests.cs | 10 +- .../Inline/DirectiveBlockLinkTests.cs | 2 +- .../Inline/InlineAnchorTests.cs | 2 +- .../Inline/InlineLinkTests.cs | 12 +- .../Inline/InlneBaseTests.cs | 2 +- 36 files changed, 673 insertions(+), 129 deletions(-) create mode 100644 src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs create mode 100644 src/Elastic.Markdown/Slices/Layout/_PrimaryNav.cshtml create mode 100644 src/Elastic.Markdown/Slices/Layout/_PrimaryNavDropdownItem.cshtml create mode 100644 src/Elastic.Markdown/_static/elasticsearch-logo-color-64px.svg create mode 100644 src/Elastic.Markdown/_static/logo-elastic-horizontal-color.svg create mode 100644 src/Elastic.Markdown/_static/observability-logo-color-64px.svg create mode 100644 src/Elastic.Markdown/_static/security-logo-color-64px.svg diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 6a93b7030..6f0f24dac 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -7,6 +7,7 @@ jobs: build: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: include: - repository: elastic/docs-content diff --git a/docs/_docset.yml b/docs/_docset.yml index 9434bb0f6..5a6c83126 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -9,6 +9,10 @@ subs: serverless-short: Serverless ece: "Elastic Cloud Enterprise" eck: "Elastic Cloud on Kubernetes" + +features: + primary-nav: false + toc: - file: index.md - hidden: developer-notes.md diff --git a/src/Elastic.Markdown/Assets/pages-nav.ts b/src/Elastic.Markdown/Assets/pages-nav.ts index ba8b8ff21..a5864cde0 100644 --- a/src/Elastic.Markdown/Assets/pages-nav.ts +++ b/src/Elastic.Markdown/Assets/pages-nav.ts @@ -15,9 +15,9 @@ function scrollCurrentNaviItemIntoView(nav: HTMLElement, delay: number) { const currentNavItem = $('.current', nav); expandAllParents(currentNavItem); setTimeout(() => { - if (currentNavItem && !isElementInViewport(currentNavItem)) { currentNavItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); + window.scrollTo(0, 0); } }, delay); } @@ -36,7 +36,7 @@ export function initNav() { if (!pagesNav) { return; } - const navItems = $$('a[href="' + window.location.pathname + '"]', pagesNav); + const navItems = $$('a[href="' + window.location.pathname + '"], a[href="' + window.location.pathname + '/"]', pagesNav); navItems.forEach(el => { el.classList.add('current'); }); diff --git a/src/Elastic.Markdown/Assets/styles.css b/src/Elastic.Markdown/Assets/styles.css index 112eddee4..472d55bd4 100644 --- a/src/Elastic.Markdown/Assets/styles.css +++ b/src/Elastic.Markdown/Assets/styles.css @@ -72,8 +72,9 @@ .sidebar { .sidebar-nav { - @apply sticky top-22 z-30 overflow-y-auto; + @apply sticky top-21 z-30 overflow-y-auto; max-height: calc(100vh - var(--spacing) * 22); + scrollbar-gutter: stable; } .sidebar-link { @@ -81,7 +82,9 @@ text-ink-light hover:text-black text-sm - leading-[1.2em] + text-wrap + inline-block + leading-[1.3em] tracking-[-0.02em]; } } @@ -169,7 +172,7 @@ } #pages-nav .current { - @apply text-blue-elastic!; + @apply font-semibold text-blue-elastic!; } .markdown-content { diff --git a/src/Elastic.Markdown/Helpers/Htmx.cs b/src/Elastic.Markdown/Helpers/Htmx.cs index 050af241e..e4b8a6dd9 100644 --- a/src/Elastic.Markdown/Helpers/Htmx.cs +++ b/src/Elastic.Markdown/Helpers/Htmx.cs @@ -2,9 +2,70 @@ // 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.Text; +using Elastic.Markdown.IO.Configuration; + namespace Elastic.Markdown.Helpers; -public class Htmx +public static class Htmx +{ + public static string GetHxSelectOob(FeatureFlags features, string? pathPrefix, string currentUrl, string targetUrl) + { + HashSet selectTargets = + [ + "#primary-nav", "#secondary-nav", "#markdown-content", "#toc-nav", "#prev-next-nav", "#breadcrumbs" + ]; + if (!HasSameTopLevelGroup(pathPrefix, currentUrl, targetUrl) && features.IsPrimaryNavEnabled) + _ = selectTargets.Add("#pages-nav"); + return string.Join(',', selectTargets); + } + + public static bool HasSameTopLevelGroup(string? pathPrefix, string currentUrl, string targetUrl) + { + if (string.IsNullOrEmpty(targetUrl) || string.IsNullOrEmpty(currentUrl)) + return false; + var startIndex = pathPrefix?.Length ?? 0; + + if (currentUrl.Length < startIndex) + throw new InvalidUrlException("Current URL is not a valid URL", currentUrl, startIndex); + + if (targetUrl.Length < startIndex) + throw new InvalidUrlException("Target URL is not a valid URL", targetUrl, startIndex); + + var currentSegments = GetSegments(currentUrl[startIndex..].Trim('/')); + var targetSegments = GetSegments(targetUrl[startIndex..].Trim('/')); + return currentSegments.Length >= 1 && targetSegments.Length >= 1 && currentSegments[0] == targetSegments[0]; + } + + public static string GetPreload() => "true"; + + public static string GetHxSwap() => "none"; + public static string GetHxPushUrl() => "true"; + public static string GetHxIndicator() => "#htmx-indicator"; + + private static string[] GetSegments(string url) => url.Split('/'); + + public static string GetHxAttributes(FeatureFlags features, string? pathPrefix, string currentUrl, string targetUrl) + { + + var attributes = new StringBuilder(); + _ = attributes.Append($" hx-get={targetUrl}"); + _ = attributes.Append($" hx-select-oob={GetHxSelectOob(features, pathPrefix, currentUrl, targetUrl)}"); + _ = attributes.Append($" hx-swap={GetHxSwap()}"); + _ = attributes.Append($" hx-push-url={GetHxPushUrl()}"); + _ = attributes.Append($" hx-indicator={GetHxIndicator()}"); + _ = attributes.Append($" preload={GetPreload()}"); + return attributes.ToString(); + } +} + + +internal sealed class InvalidUrlException : ArgumentException { - public static string GetHxSelectOob() => "#markdown-content,#toc-nav,#prev-next-nav,#breadcrumbs"; + public InvalidUrlException(string message, string url, int startIndex) + : base($"{message} (Url: {url}, StartIndex: {startIndex})") + { + Data["Url"] = url; + Data["StartIndex"] = startIndex; + } } diff --git a/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs b/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs index 081252678..97e0afe36 100644 --- a/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs +++ b/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs @@ -31,6 +31,10 @@ public record ConfigurationFile : DocumentationFile private readonly Dictionary _substitutions = new(StringComparer.OrdinalIgnoreCase); public IReadOnlyDictionary Substitutions => _substitutions; + private readonly Dictionary _features = new(StringComparer.OrdinalIgnoreCase); + private FeatureFlags? _featureFlags; + public FeatureFlags Features => _featureFlags ??= new FeatureFlags(_features); + public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildContext context, int depth = 0, string parentPath = "") : base(sourceFile, rootPath) { @@ -79,6 +83,9 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon TableOfContents = entries; break; + case "features": + _features = reader.ReadDictionary(entry.Entry).ToDictionary(k => k.Key, v => bool.Parse(v.Value), StringComparer.OrdinalIgnoreCase); + break; case "external_hosts": reader.EmitWarning($"{entry.Key} has been deprecated and will be removed", entry.Key); break; @@ -97,6 +104,8 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon Globs = [.. ImplicitFolders.Select(f => Glob.Parse($"{f}/*.md"))]; } + public bool IsFeatureEnabled(string feature) => _features.TryGetValue(feature, out var enabled) && enabled; + private List ReadChildren(YamlStreamReader reader, KeyValuePair entry, string parentPath) { var entries = new List(); diff --git a/src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs b/src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs new file mode 100644 index 000000000..57055de93 --- /dev/null +++ b/src/Elastic.Markdown/IO/Configuration/FeatureFlags.cs @@ -0,0 +1,11 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// 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 + +namespace Elastic.Markdown.IO.Configuration; + +public class FeatureFlags(Dictionary featureFlags) +{ + public bool IsPrimaryNavEnabled => IsEnabled("primary-nav"); + private bool IsEnabled(string key) => featureFlags.TryGetValue(key, out var value) && value; +} diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index 94c57e716..e99acc238 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -316,7 +316,7 @@ private YamlFrontMatter ReadYamlFrontMatter(string raw) } - public static string CreateHtml(MarkdownDocument document) + public string CreateHtml(MarkdownDocument document) { //we manually render title and optionally append an applies block embedded in yaml front matter. var h1 = document.Descendants().FirstOrDefault(h => h.Level == 1); @@ -324,4 +324,13 @@ public static string CreateHtml(MarkdownDocument document) _ = document.Remove(h1); return document.ToHtml(MarkdownParser.Pipeline); } + + public static string CreateHtml(MarkdownDocument document, MarkdownParser parser) + { + //we manually render title and optionally append an applies block embedded in yaml front matter. + var h1 = document.Descendants().FirstOrDefault(h => h.Level == 1); + if (h1 is not null) + _ = document.Remove(h1); + return document.ToHtml(parser.Pipeline); + } } diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs index 89b18c96a..1453980d2 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs @@ -21,7 +21,7 @@ namespace Elastic.Markdown.Myst.Directives; /// An HTML renderer for a . /// /// -public class DirectiveHtmlRenderer : HtmlObjectRenderer +public class DirectiveHtmlRenderer(MarkdownParser markdownParser) : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlock) { @@ -62,10 +62,10 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo if (includeBlock.Literal) WriteLiteralIncludeBlock(renderer, includeBlock); else - WriteIncludeBlock(renderer, includeBlock); + WriteIncludeBlock(renderer, includeBlock, markdownParser); return; case SettingsBlock settingsBlock: - WriteSettingsBlock(renderer, settingsBlock); + WriteSettingsBlock(renderer, settingsBlock, markdownParser); return; default: // if (!string.IsNullOrEmpty(directiveBlock.Info) && !directiveBlock.Info.StartsWith('{')) @@ -219,28 +219,24 @@ private static void WriteLiteralIncludeBlock(HtmlRenderer renderer, IncludeBlock } } - private static void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block) + private static void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block, MarkdownParser parser) { if (!block.Found || block.IncludePath is null) return; - var parser = new MarkdownParser(block.Build, block.Context); var snippet = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath); var parentPath = block.Context.MarkdownSourcePath; var document = parser.ParseSnippetAsync(snippet, parentPath, block.Context.YamlFrontMatter, default).GetAwaiter().GetResult(); - var html = document.ToHtml(MarkdownParser.Pipeline); + var html = document.ToHtml(parser.Pipeline); _ = renderer.Write(html); } - private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block) + private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block, MarkdownParser parser) { if (!block.Found || block.IncludePath is null) return; - var parser = new MarkdownParser(block.Build, block.Context); - var file = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath); - YamlSettings? settings; try { @@ -264,7 +260,7 @@ private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock bloc RenderMarkdown = s => { var document = parser.ParseEmbeddedMarkdown(s, block.IncludeFrom, block.Context.YamlFrontMatter); - var html = document.ToHtml(MarkdownParser.Pipeline); + var html = document.ToHtml(parser.Pipeline); return html; } }); diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs index 4f990e3b8..c4246baa6 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs @@ -13,9 +13,9 @@ namespace Elastic.Markdown.Myst.Directives; public static class DirectiveMarkdownBuilderExtensions { - public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder pipeline) + public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder pipeline, MarkdownParser markdownParser) { - pipeline.Extensions.AddIfNotAlready(); + pipeline.Extensions.AddIfNotAlready(new DirectiveMarkdownExtension(markdownParser)); return pipeline; } } @@ -24,7 +24,7 @@ public static MarkdownPipelineBuilder UseDirectives(this MarkdownPipelineBuilder /// Extension to allow custom containers. /// /// -public class DirectiveMarkdownExtension : IMarkdownExtension +public class DirectiveMarkdownExtension(MarkdownParser markdownParser) : IMarkdownExtension { public void Setup(MarkdownPipelineBuilder pipeline) { @@ -53,7 +53,7 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) if (!renderer.ObjectRenderers.Contains()) { // Must be inserted before CodeBlockRenderer - _ = renderer.ObjectRenderers.InsertBefore(new DirectiveHtmlRenderer()); + _ = renderer.ObjectRenderers.InsertBefore(new DirectiveHtmlRenderer(markdownParser)); } _ = renderer.ObjectRenderers.Replace(new SectionedHeadingRenderer()); diff --git a/src/Elastic.Markdown/Myst/MarkdownParser.cs b/src/Elastic.Markdown/Myst/MarkdownParser.cs index 67a7e9093..befb60bba 100644 --- a/src/Elastic.Markdown/Myst/MarkdownParser.cs +++ b/src/Elastic.Markdown/Myst/MarkdownParser.cs @@ -4,6 +4,7 @@ using System.IO.Abstractions; using Cysharp.IO; +using Elastic.Markdown.IO.Configuration; using Elastic.Markdown.Myst.CodeBlocks; using Elastic.Markdown.Myst.Comments; using Elastic.Markdown.Myst.Directives; @@ -101,33 +102,33 @@ private static async Task ParseAsync( } // ReSharper disable once InconsistentNaming - private static MarkdownPipeline? MinimalPipelineCached; - private static MarkdownPipeline MinimalPipeline + private MarkdownPipeline? _minimalPipelineCached; + private MarkdownPipeline MinimalPipeline { get { - if (MinimalPipelineCached is not null) - return MinimalPipelineCached; + if (_minimalPipelineCached is not null) + return _minimalPipelineCached; var builder = new MarkdownPipelineBuilder() .UseYamlFrontMatter() .UseInlineAnchors() .UseHeadingsWithSlugs() - .UseDirectives(); + .UseDirectives(this); _ = builder.BlockParsers.TryRemove(); - MinimalPipelineCached = builder.Build(); - return MinimalPipelineCached; + _minimalPipelineCached = builder.Build(); + return _minimalPipelineCached; } } // ReSharper disable once InconsistentNaming - private static MarkdownPipeline? PipelineCached; - public static MarkdownPipeline Pipeline + private MarkdownPipeline? _pipelineCached; + public MarkdownPipeline Pipeline { get { - if (PipelineCached is not null) - return PipelineCached; + if (_pipelineCached is not null) + return _pipelineCached; var builder = new MarkdownPipelineBuilder() .UseInlineAnchors() @@ -141,15 +142,15 @@ public static MarkdownPipeline Pipeline .UseYamlFrontMatter() .UseGridTables() .UsePipeTables() - .UseDirectives() + .UseDirectives(this) .UseDefinitionLists() .UseEnhancedCodeBlocks() - .UseHtmxLinkInlineRenderer() + .UseHtmxLinkInlineRenderer(Build) .DisableHtml() .UseHardBreaks(); _ = builder.BlockParsers.TryRemove(); - PipelineCached = builder.Build(); - return PipelineCached; + _pipelineCached = builder.Build(); + return _pipelineCached; } } diff --git a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs index 094bd432b..8fae9da96 100644 --- a/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Renderers/HtmxLinkInlineRenderer.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Markdown.Helpers; +using Elastic.Markdown.IO.Configuration; using Markdig; using Markdig.Renderers; using Markdig.Renderers.Html.Inlines; @@ -10,7 +11,7 @@ namespace Elastic.Markdown.Myst.Renderers; -public class HtmxLinkInlineRenderer : LinkInlineRenderer +public class HtmxLinkInlineRenderer(BuildContext build) : LinkInlineRenderer { protected override void Write(HtmlRenderer renderer, LinkInline link) { @@ -30,11 +31,11 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) _ = renderer.Write(" hx-get=\""); _ = renderer.WriteEscapeUrl(link.GetDynamicUrl != null ? link.GetDynamicUrl() ?? link.Url : link.Url); _ = renderer.Write('"'); - _ = renderer.Write($" hx-select-oob=\"{Htmx.GetHxSelectOob()}\""); + _ = renderer.Write($" hx-select-oob=\"{Htmx.GetHxSelectOob(build.Configuration.Features, build.UrlPathPrefix, currentUrl, link.Url)}\""); _ = renderer.Write(" hx-swap=\"none\""); _ = renderer.Write(" hx-push-url=\"true\""); _ = renderer.Write(" hx-indicator=\"#htmx-indicator\""); - _ = renderer.Write(" preload=\"mouseover\""); + _ = renderer.Write($" preload=\"{Htmx.GetPreload()}\""); if (!string.IsNullOrEmpty(link.Title)) { @@ -62,14 +63,14 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) public static class CustomLinkInlineRendererExtensions { - public static MarkdownPipelineBuilder UseHtmxLinkInlineRenderer(this MarkdownPipelineBuilder pipeline) + public static MarkdownPipelineBuilder UseHtmxLinkInlineRenderer(this MarkdownPipelineBuilder pipeline, BuildContext build) { - pipeline.Extensions.AddIfNotAlready(); + pipeline.Extensions.AddIfNotAlready(new HtmxLinkInlineRendererExtension(build)); return pipeline; } } -public class HtmxLinkInlineRendererExtension : IMarkdownExtension +public class HtmxLinkInlineRendererExtension(BuildContext build) : IMarkdownExtension { public void Setup(MarkdownPipelineBuilder pipeline) { @@ -81,7 +82,7 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) if (renderer is HtmlRenderer htmlRenderer) { _ = htmlRenderer.ObjectRenderers.RemoveAll(x => x is LinkInlineRenderer); - htmlRenderer.ObjectRenderers.Add(new HtmxLinkInlineRenderer()); + htmlRenderer.ObjectRenderers.Add(new HtmxLinkInlineRenderer(build)); } } } diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index c4e5a1d1e..e6739e554 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; using Elastic.Markdown.IO; +using Elastic.Markdown.IO.Configuration; +using Elastic.Markdown.IO.Navigation; using Markdig.Syntax; using RazorSlices; @@ -12,17 +14,25 @@ public class HtmlWriter(DocumentationSet documentationSet, IFileSystem writeFile { private DocumentationSet DocumentationSet { get; } = documentationSet; - private async Task RenderNavigation(MarkdownFile markdown, Cancel ctx = default) + private async Task RenderNavigation(string topLevelGroupId, MarkdownFile markdown, Cancel ctx = default) { + var group = DocumentationSet.Tree.NavigationItems + .OfType() + .FirstOrDefault(i => i.Group.Id == topLevelGroupId)?.Group; + var slice = Layout._TocTree.Create(new NavigationViewModel { - Tree = DocumentationSet.Tree, - CurrentDocument = markdown + Title = group?.Index?.NavigationTitle ?? DocumentationSet.Tree.Index?.NavigationTitle ?? "Docs", + TitleUrl = group?.Index?.Url ?? DocumentationSet.Tree.Index?.Url ?? DocumentationSet.Build.UrlPathPrefix ?? "/", + Tree = group ?? DocumentationSet.Tree, + CurrentDocument = markdown, + IsRoot = topLevelGroupId == DocumentationSet.Tree.Id, + Features = DocumentationSet.Configuration.Features }); return await slice.RenderAsync(cancellationToken: ctx); } - private string? _renderedNavigation; + private readonly Dictionary _renderedNavigationCache = []; public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = default) { @@ -30,11 +40,41 @@ public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = defau return await RenderLayout(markdown, document, ctx); } + private static string GetTopLevelGroupId(MarkdownFile markdown) => + markdown.YieldParentGroups().Length > 1 + ? markdown.YieldParentGroups()[^2] + : markdown.YieldParentGroups()[0]; + public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument document, Cancel ctx = default) { - var html = MarkdownFile.CreateHtml(document); + var html = markdown.CreateHtml(document); await DocumentationSet.Tree.Resolve(ctx); - _renderedNavigation ??= await RenderNavigation(markdown, ctx); + + var topLevelNavigationItems = DocumentationSet.Tree.NavigationItems + .OfType() + .Select(i => i.Group); + + string? navigationHtml; + + if (DocumentationSet.Configuration.Features.IsPrimaryNavEnabled) + { + var topLevelGroupId = GetTopLevelGroupId(markdown); + if (!_renderedNavigationCache.TryGetValue(topLevelGroupId, out var value)) + { + value = await RenderNavigation(topLevelGroupId, markdown, ctx); + _renderedNavigationCache[topLevelGroupId] = value; + } + navigationHtml = value; + } + else + { + if (!_renderedNavigationCache.TryGetValue("root", out var value)) + { + value = await RenderNavigation(DocumentationSet.Tree.Id, markdown, ctx); + _renderedNavigationCache["root"] = value; + } + navigationHtml = value; + } var previous = DocumentationSet.GetPrevious(markdown); var next = DocumentationSet.GetNext(markdown); @@ -44,6 +84,7 @@ public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument d var path = Path.Combine(DocumentationSet.RelativeSourcePath, markdown.RelativePath); var editUrl = $"https://github.com/elastic/{remote}/edit/{branch}/{path}"; + var slice = Index.Create(new IndexViewModel { Title = markdown.Title ?? "[TITLE NOT SET]", @@ -54,11 +95,13 @@ public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument d CurrentDocument = markdown, PreviousDocument = previous, NextDocument = next, - NavigationHtml = _renderedNavigation, + TopLevelNavigationItems = [.. topLevelNavigationItems], + NavigationHtml = navigationHtml, UrlPathPrefix = markdown.UrlPathPrefix, Applies = markdown.YamlFrontMatter?.AppliesTo, GithubEditUrl = editUrl, - AllowIndexing = DocumentationSet.Build.AllowIndexing && !markdown.Hidden + AllowIndexing = DocumentationSet.Build.AllowIndexing && !markdown.Hidden, + Features = DocumentationSet.Configuration.Features }); return await slice.RenderAsync(cancellationToken: ctx); } diff --git a/src/Elastic.Markdown/Slices/Index.cshtml b/src/Elastic.Markdown/Slices/Index.cshtml index 85dfdb3e9..ec51e5529 100644 --- a/src/Elastic.Markdown/Slices/Index.cshtml +++ b/src/Elastic.Markdown/Slices/Index.cshtml @@ -11,9 +11,11 @@ Previous = Model.PreviousDocument, Next = Model.NextDocument, NavigationHtml = Model.NavigationHtml, + TopLevelNavigationItems = Model.TopLevelNavigationItems, UrlPathPrefix = Model.UrlPathPrefix, GithubEditUrl = Model.GithubEditUrl, AllowIndexing = Model.AllowIndexing, + Features = Model.Features }; }
    diff --git a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml index 43999e8d6..cc71473ce 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml @@ -4,13 +4,13 @@
  • Elastic @@ -24,11 +24,11 @@ itemprop="item" href="@item.Url" hx-get="@item.Url" - hx-select-oob="@Htmx.GetHxSelectOob()" + hx-select-oob="@Htmx.GetHxSelectOob(Model.Features, Model.UrlPathPrefix, item.Url, Model.CurrentDocument.Url)" hx-swap="none" hx-push-url="true" hx-indicator="#htmx-indicator" - preload="mouseover" + preload="@Htmx.GetPreload()" > @item.NavigationTitle diff --git a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml index 08c58a81a..408653e35 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml @@ -1,27 +1,196 @@ +@using Elastic.Markdown.Helpers @inherits RazorSlice -
    -
    -
    - - Elastic - -
    -
  • } else if (item is GroupNavigation folder) { var g = folder.Group; - const int initialExpandLevel = 1; - var shouldInitiallyExpand = g.Depth <= initialExpandLevel; -
  • -
  • + @if (g.NavigationItems.Count > 0) { -
  • "; var start = html.IndexOf(find, StringComparison.Ordinal); Html = start >= 0 diff --git a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs index 526cd281b..309545177 100644 --- a/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/AnchorLinkTests.cs @@ -75,7 +75,7 @@ [Sub Requirements](testing/req.md#sub-requirements) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] @@ -93,7 +93,7 @@ [Sub Requirements](testing/req.md#new-reqs) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] @@ -110,7 +110,7 @@ public class ExternalPageAnchorAutoTitleTests(ITestOutputHelper output) : Anchor public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Special Requirements > Sub Requirements

    """ + """

    Special Requirements > Sub Requirements

    """ ); [Fact] @@ -146,7 +146,7 @@ [Sub Requirements](testing/req.md#sub-requirements2) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] @@ -165,7 +165,7 @@ [Heading inside dropdown](testing/req.md#heading-inside-dropdown) public void GeneratesHtml() => // language=html Html.Should().Contain( - """Heading inside dropdown""" + """Heading inside dropdown""" ); [Fact] public void HasError() => Collector.Diagnostics.Should().HaveCount(0); diff --git a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs index d5549f6c2..6d570c2c0 100644 --- a/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/DirectiveBlockLinkTests.cs @@ -66,7 +66,7 @@ [Sub Requirements](testing/req.md#hint_ref) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs index df6669b9b..f5c95cae2 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs @@ -200,7 +200,7 @@ [Sub Requirements](testing/req.md#custom-anchor) public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Sub Requirements

    """ + """

    Sub Requirements

    """ ); [Fact] diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs index c7d08cb01..999ac3656 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs @@ -41,7 +41,7 @@ public class InlineLinkTests(ITestOutputHelper output) : LinkTestBase(output, public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Elasticsearch

    """ + """

    Elasticsearch

    """ ); [Fact] @@ -58,7 +58,7 @@ public class LinkToPageTests(ITestOutputHelper output) : LinkTestBase(output, public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Requirements

    """ + """

    Requirements

    """ ); [Fact] @@ -78,7 +78,7 @@ public class InsertPageTitleTests(ITestOutputHelper output) : LinkTestBase(outpu public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    Special Requirements

    """ + """

    Special Requirements

    """ ); [Fact] @@ -100,7 +100,7 @@ public class LinkReferenceTest(ITestOutputHelper output) : LinkTestBase(output, public void GeneratesHtml() => // language=html Html.Should().Contain( - """

    test

    """ + """

    test

    """ ); [Fact] @@ -231,10 +231,10 @@ public void GeneratesHtml() => Html.TrimEnd().Should().Be("""

    Links:

    """); diff --git a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs index 2ef781cd7..700c51004 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs @@ -129,7 +129,7 @@ public virtual async ValueTask InitializeAsync() await Set.LinkResolver.FetchLinks(); Document = await File.ParseFullAsync(default); - var html = MarkdownFile.CreateHtml(Document).AsSpan(); + var html = File.CreateHtml(Document).AsSpan(); var find = "\n"; var start = html.IndexOf(find, StringComparison.Ordinal); Html = start >= 0 && !TestingFullDocument From f1d725227625f7a5ca75c0fe5cf5902c519f4a19 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 28 Feb 2025 10:21:59 +0100 Subject: [PATCH 26/45] Inbound link checking, fully validate if found in links-index (#643) --- .../Diagnostics/Log.cs | 17 ++++++++++---- .../CrossLinks/CrossLinkResolver.cs | 23 ++++++++----------- .../Links/LinkIndexLinkChecker.cs | 3 ++- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/Elastic.Documentation.Tooling/Diagnostics/Log.cs b/src/Elastic.Documentation.Tooling/Diagnostics/Log.cs index d4ccd5589..7e6da01c4 100644 --- a/src/Elastic.Documentation.Tooling/Diagnostics/Log.cs +++ b/src/Elastic.Documentation.Tooling/Diagnostics/Log.cs @@ -12,10 +12,19 @@ public class Log(ILogger logger) : IDiagnosticsOutput { public void Write(Diagnostic diagnostic) { - if (diagnostic.Severity == Severity.Error) - logger.LogError("{Message} ({File}:{Line})", diagnostic.Message, diagnostic.File, diagnostic.Line); + if (diagnostic.File.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + if (diagnostic.Severity == Severity.Error) + logger.LogError("{Message}", diagnostic.Message); + else + logger.LogWarning("{Message}", diagnostic.Message); + } else - logger.LogWarning("{Message} ({File}:{Line})", diagnostic.Message, diagnostic.File, diagnostic.Line); + { + if (diagnostic.Severity == Severity.Error) + logger.LogError("{Message} ({File}:{Line})", diagnostic.Message, diagnostic.File, diagnostic.Line ?? 0); + else + logger.LogWarning("{Message} ({File}:{Line})", diagnostic.Message, diagnostic.File, diagnostic.Line ?? 0); + } } } - diff --git a/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs b/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs index d99f3a7bd..eeb5bde4f 100644 --- a/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs +++ b/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs @@ -57,7 +57,10 @@ public FetchedCrossLinks UpdateLinkReference(string repository, LinkReference li { var dictionary = _crossLinks.LinkReferences.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); dictionary[repository] = linkReference; - _crossLinks = _crossLinks with { LinkReferences = dictionary.ToFrozenDictionary() }; + _crossLinks = _crossLinks with + { + LinkReferences = dictionary.ToFrozenDictionary() + }; return _crossLinks; } @@ -68,21 +71,15 @@ public static bool TryResolve( [NotNullWhen(true)] out Uri? resolvedUri ) { - var lookup = fetchedCrossLinks.LinkReferences; - var declaredRepositories = fetchedCrossLinks.DeclaredRepositories; resolvedUri = null; - if (crossLinkUri.Scheme == "docs-content") - { - if (!lookup.TryGetValue(crossLinkUri.Scheme, out var linkReference)) - { - errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links"); - return false; - } - + var lookup = fetchedCrossLinks.LinkReferences; + if (crossLinkUri.Scheme != "asciidocalypse" && lookup.TryGetValue(crossLinkUri.Scheme, out var linkReference)) return TryFullyValidate(errorEmitter, linkReference, crossLinkUri, out resolvedUri); - } - // TODO this is temporary while we wait for all links.json files to be published + // TODO this is temporary while we wait for all links.json to be published + // Here we just silently rewrite the cross_link to the url + + var declaredRepositories = fetchedCrossLinks.DeclaredRepositories; if (!declaredRepositories.Contains(crossLinkUri.Scheme)) { errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links"); diff --git a/src/docs-assembler/Links/LinkIndexLinkChecker.cs b/src/docs-assembler/Links/LinkIndexLinkChecker.cs index 7964ce201..c1d66a29f 100644 --- a/src/docs-assembler/Links/LinkIndexLinkChecker.cs +++ b/src/docs-assembler/Links/LinkIndexLinkChecker.cs @@ -88,9 +88,10 @@ private async Task ValidateCrossLinks( { if (s.Contains("is not a valid link in the")) { + // var error = $"'elastic/{repository}' links to unknown file: " + s; error = error.Replace("is not a valid link in the", "in the"); - collector.EmitError(repository, error); + collector.EmitError($"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{uri.Scheme}/main/links.json", error); return; } From 5a9cd46c66ef56929486dfb72f5655538ded0fc0 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Fri, 28 Feb 2025 10:34:50 +0100 Subject: [PATCH 27/45] Add `landing-page-path` output and use it in preview workflow (#642) * Add first-page-path output and use it in preview build workflow * Refactor * Add to test * Fix smoke test * Refactor * Fix * Fix smoke test * Fix output name in usage and add output to action.yml * Refactor smoke-tests --- .github/workflows/preview-build.yml | 4 +++- .github/workflows/smoke-test.yml | 12 +++++++++++- action.yml | 2 ++ src/Elastic.Markdown/Slices/HtmlWriter.cs | 4 +++- src/docs-builder/Cli/Commands.cs | 4 +++- 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index f9793c0a9..66fbb9674 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -133,6 +133,7 @@ jobs: # we run our artifact directly please use the prebuild # elastic/docs-builder@main GitHub Action for all other repositories! - name: Build documentation + id: docs-build if: github.repository == 'elastic/docs-builder' && steps.deployment.outputs.result run: | dotnet run --project src/docs-builder -- --strict --path-prefix "${PATH_PREFIX}" @@ -176,6 +177,7 @@ jobs: if: always() && steps.deployment.outputs.result env: PR_NUMBER: ${{ github.event.pull_request.number }} + LANDING_PAGE_PATH: ${{ steps.docs-build.outputs.landing-page-path }} with: script: | await github.rest.repos.createDeploymentStatus({ @@ -183,6 +185,6 @@ jobs: repo: context.repo.repo, deployment_id: ${{ steps.deployment.outputs.result }}, state: "${{ steps.docs-build.outputs.skip == 'true' && 'inactive' || (steps.s3-upload.outcome == 'success' && 'success' || 'failure') }}", - environment_url: `https://docs-v3-preview.elastic.dev${process.env.PATH_PREFIX}`, + environment_url: `https://docs-v3-preview.elastic.dev${process.env.LANDING_PAGE_PATH ?? process.env.PATH_PREFIX}`, log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, }) diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 6f0f24dac..33f4d0a9b 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -6,13 +6,19 @@ on: jobs: build: runs-on: ubuntu-latest + name: build (${{ matrix.repository }}) strategy: fail-fast: false matrix: include: - repository: elastic/docs-content + landing-page-path-output: /docs/ - repository: elastic/apm-agent-android - - repository: elastic/cloud-on-k8s + landing-page-path-output: /docs/reference/ + + # This is a random repository that should not have a docset.yml + - repository: elastic/oblt-actions + landing-page-path-output: "" steps: - uses: actions/checkout@v4 @@ -25,5 +31,9 @@ jobs: path: test-repo - name: Build documentation + id: docs-build run: | dotnet run --project src/docs-builder -- --strict --path-prefix "/docs" -p test-repo + + - name: Verify landing-page-path output + run: test ${{ steps.docs-build.outputs.landing-page-path }} == ${{ matrix.landing-page-path-output }} diff --git a/action.yml b/action.yml index 9ce187976..9c1029e17 100644 --- a/action.yml +++ b/action.yml @@ -13,6 +13,8 @@ inputs: description: 'Treat warnings as errors' required: false outputs: + landing-page-path: + description: 'Path to the landing page of the documentation' skip: description: "hint from the documentation tool to skip the docs build for this PR" diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index e6739e554..e8509480e 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -1,6 +1,8 @@ // Licensed to Elasticsearch B.V under one or more agreements. // 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.Collections.Concurrent; using System.IO.Abstractions; using Elastic.Markdown.IO; using Elastic.Markdown.IO.Configuration; @@ -32,7 +34,7 @@ private async Task RenderNavigation(string topLevelGroupId, MarkdownFile return await slice.RenderAsync(cancellationToken: ctx); } - private readonly Dictionary _renderedNavigationCache = []; + private readonly ConcurrentDictionary _renderedNavigationCache = []; public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = default) { diff --git a/src/docs-builder/Cli/Commands.cs b/src/docs-builder/Cli/Commands.cs index aa2e3c624..396dbc874 100644 --- a/src/docs-builder/Cli/Commands.cs +++ b/src/docs-builder/Cli/Commands.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Tooling.Filters; using Elastic.Markdown; using Elastic.Markdown.IO; +using Elastic.Markdown.IO.Navigation; using Elastic.Markdown.Refactor; using Microsoft.Extensions.Logging; @@ -107,7 +108,8 @@ public async Task Generate( var set = new DocumentationSet(context, logger); var generator = new DocumentationGenerator(set, logger); await generator.GenerateAll(ctx); - + if (runningOnCi) + await githubActionsService.SetOutputAsync("landing-page-path", set.MarkdownFiles.First().Value.Url); if (bool.TryParse(githubActionsService.GetInput("strict"), out var strictValue) && strictValue) strict ??= strictValue; From e0b6e99d310cc7b96d4774e3a0a84d42768807ab Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Fri, 28 Feb 2025 10:43:36 +0100 Subject: [PATCH 28/45] Fix preview-build.yml (#644) --- .github/workflows/preview-build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 66fbb9674..7f01c81e9 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -133,7 +133,6 @@ jobs: # we run our artifact directly please use the prebuild # elastic/docs-builder@main GitHub Action for all other repositories! - name: Build documentation - id: docs-build if: github.repository == 'elastic/docs-builder' && steps.deployment.outputs.result run: | dotnet run --project src/docs-builder -- --strict --path-prefix "${PATH_PREFIX}" From d78729a64e51b0255fee57fb42d603c021d6a13d Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Fri, 28 Feb 2025 12:35:28 +0100 Subject: [PATCH 29/45] Remove unused workflow (#646) --- .github/workflows/preview-deploy.yml | 21 ------ .github/workflows/preview.yml | 97 ---------------------------- 2 files changed, 118 deletions(-) delete mode 100644 .github/workflows/preview-deploy.yml delete mode 100644 .github/workflows/preview.yml diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml deleted file mode 100644 index cc725c302..000000000 --- a/.github/workflows/preview-deploy.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: preview-deploy - -on: - workflow_call: ~ - workflow_run: - workflows: [preview-build] - types: - - completed - -permissions: - contents: none - id-token: write - deployments: write - actions: read - -jobs: - do-nothing: - runs-on: ubuntu-latest - steps: - - name: Do nothing - run: echo "This is here for backwards compatibility. After validating that the preview-build workflow is working as expected, you can remove this workflow file." diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml deleted file mode 100644 index 4e55338f9..000000000 --- a/.github/workflows/preview.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: preview - -on: - workflow_call: - inputs: - strict: - description: 'Treat warnings as errors' - type: boolean - default: true - continue-on-error: - description: 'Do not fail to publish if build fails' - type: boolean - required: false - default: true # default for will be false after migration - -permissions: - id-token: write - deployments: write - contents: read - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Create Deployment - uses: actions/github-script@v7 - id: deployment - with: - result-encoding: string - script: | - const { owner, repo } = context.repo; - const deployment = await github.rest.repos.createDeployment({ - owner, - repo, - ref: context.payload.pull_request.head.ref, - environment: `docs-preview-${context.issue.number}`, - auto_merge: false, - required_contexts: [], - }) - await github.rest.repos.createDeploymentStatus({ - deployment_id: deployment.data.id, - owner, - repo, - state: "in_progress", - log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}?pr=${context.issue.number}`, - }) - return deployment.data.id - - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - - - uses: actions/download-artifact@v4 - if: github.repository == 'elastic/docs-builder' - with: - name: docs-builder-binary - - # we run our artifact directly please use the prebuild - # elastic/docs-builder@main GitHub Action for all other repositories! - - name: Build documentation - if: github.repository == 'elastic/docs-builder' - env: - PR_NUMBER: - run: | - chmod +x ./docs-builder - ./docs-builder --strict --path-prefix "/${GITHUB_REPOSITORY}/pull/${{ github.event.pull_request.number }}" - - - name: Build documentation - if: github.repository != 'elastic/docs-builder' - uses: elastic/docs-builder@main - continue-on-error: ${{ inputs.continue-on-error == 'true' }} # Will be removed after the migration phase - with: - prefix: "/${{ github.repository }}/pull/${{ github.event.pull_request.number }}" - strict: ${{ inputs.strict }} - - - uses: elastic/docs-builder/.github/actions/aws-auth@main - - - name: Upload to S3 - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - aws s3 sync .artifacts/docs/html "s3://elastic-docs-v3-website-preview/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}" --delete - aws cloudfront create-invalidation --distribution-id EKT7LT5PM8RKS --paths "/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}/*" - - - name: Update deployment status - uses: actions/github-script@v7 - if: always() && steps.deployment.outputs.result - with: - script: | - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: ${{ steps.deployment.outputs.result }}, - state: "${{ job.status == 'success' && 'success' || 'failure' }}", - environment_url: `https://docs-v3-preview.elastic.dev/${context.repo.owner}/${context.repo.repo}/pull/${context.issue.number}`, - log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}?pr=${context.issue.number}`, - }) From 1c9b6df085210d04914e1eb9b9c1960fa6546264 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 28 Feb 2025 12:48:13 +0100 Subject: [PATCH 30/45] Add validate command for single published repository (#648) Update command to be rooted to `docs-assembler inbound-links ` --- actions/validate-inbound-local/action.yml | 2 +- build/Targets.fs | 4 -- ...LinkCommands.cs => InboundLinkCommands.cs} | 59 ++++++++++++++----- .../Links/LinkIndexLinkChecker.cs | 17 +++++- src/docs-assembler/Program.cs | 2 +- 5 files changed, 60 insertions(+), 24 deletions(-) rename src/docs-assembler/Cli/{LinkCommands.cs => InboundLinkCommands.cs} (70%) diff --git a/actions/validate-inbound-local/action.yml b/actions/validate-inbound-local/action.yml index 6ae079bc3..5bc31c760 100644 --- a/actions/validate-inbound-local/action.yml +++ b/actions/validate-inbound-local/action.yml @@ -7,4 +7,4 @@ runs: - name: Validate Inbound Links uses: elastic/docs-builder/actions/assembler@main with: - command: "link validate-inbound-local" \ No newline at end of file + command: "inbound-links validate-link-reference" \ No newline at end of file diff --git a/build/Targets.fs b/build/Targets.fs index f566ec9ac..5f0f9c74f 100644 --- a/build/Targets.fs +++ b/build/Targets.fs @@ -58,10 +58,6 @@ let private pristineCheck (arguments:ParseResults) = let private publishBinaries _ = exec { run "dotnet" "publish" "src/docs-builder/docs-builder.csproj" } exec { run "dotnet" "publish" "src/docs-assembler/docs-assembler.csproj" } - Zip.zip - ".artifacts/publish/docs-builder/release" - $"docs-builder-%s{OS.Name}-{OS.Arch}.zip" - [".artifacts/publish/docs-builder/release/docs-builder"] let private publishZip _ = exec { run "dotnet" "publish" "src/docs-builder/docs-builder.csproj" } diff --git a/src/docs-assembler/Cli/LinkCommands.cs b/src/docs-assembler/Cli/InboundLinkCommands.cs similarity index 70% rename from src/docs-assembler/Cli/LinkCommands.cs rename to src/docs-assembler/Cli/InboundLinkCommands.cs index 86b0bdf20..19bca6d52 100644 --- a/src/docs-assembler/Cli/LinkCommands.cs +++ b/src/docs-assembler/Cli/InboundLinkCommands.cs @@ -16,7 +16,7 @@ namespace Documentation.Assembler.Cli; -internal sealed class LinkCommands(ILoggerFactory logger, ICoreService githubActionsService) +internal sealed class InboundLinkCommands(ILoggerFactory logger, ICoreService githubActionsService) { private void AssignOutputLogger() { @@ -27,33 +27,44 @@ private void AssignOutputLogger() #pragma warning restore CA2254 } - /// - /// Validate all published cross_links in all published links.json files. - /// + /// Validate all published cross_links in all published links.json files. /// - [Command("validate-inbound-all")] + [Command("validate-all")] public async Task ValidateAllInboundLinks(Cancel ctx = default) { AssignOutputLogger(); return await new LinkIndexLinkChecker(logger).CheckAll(githubActionsService, ctx); } - /// - /// Create an index.json file from all discovered links.json files in our S3 bucket - /// + /// Validate all published cross_links in all published links.json files. /// - /// /// - [Command("validate-inbound-local")] - public async Task ValidateLocalInboundLinks(string? repository = null, string? file = null, Cancel ctx = default) + [Command("validate")] + public async Task ValidateRepoInboundLinks(string? repository = null, Cancel ctx = default) { AssignOutputLogger(); - file ??= ".artifacts/docs/html/links.json"; var fs = new FileSystem(); var root = fs.DirectoryInfo.New(Paths.Root.FullName); repository ??= GitCheckoutInformation.Create(root, new FileSystem()).RepositoryName; if (repository == null) throw new Exception("Unable to determine repository name"); + return await new LinkIndexLinkChecker(logger).CheckRepository(githubActionsService, repository, ctx); + } + + /// + /// Validate a locally published links.json file against all published links.json files in the registry + /// + /// + /// + [Command("validate-link-reference")] + public async Task ValidateLocalLinkReference(string? file = null, Cancel ctx = default) + { + AssignOutputLogger(); + file ??= ".artifacts/docs/html/links.json"; + var fs = new FileSystem(); + var root = fs.DirectoryInfo.New(Paths.Root.FullName); + var repository = GitCheckoutInformation.Create(root, new FileSystem()).RepositoryName + ?? throw new Exception("Unable to determine repository name"); return await new LinkIndexLinkChecker(logger).CheckWithLocalLinksJson(githubActionsService, repository, file, ctx); } @@ -69,14 +80,21 @@ public async Task CreateLinkIndex(Cancel ctx = default) IAmazonS3 client = new AmazonS3Client(); var bucketName = "elastic-docs-link-index"; - var request = new ListObjectsV2Request { BucketName = bucketName, MaxKeys = 5 }; + var request = new ListObjectsV2Request + { + BucketName = bucketName, + MaxKeys = 5 + }; Console.WriteLine("--------------------------------------"); Console.WriteLine($"Listing the contents of {bucketName}:"); Console.WriteLine("--------------------------------------"); - var linkIndex = new LinkIndex { Repositories = [] }; + var linkIndex = new LinkIndex + { + Repositories = [] + }; try { ListObjectsV2Response response; @@ -95,11 +113,20 @@ public async Task CreateLinkIndex(Cancel ctx = default) var repository = tokens[1]; var branch = tokens[2]; - var entry = new LinkIndexEntry { Repository = repository, Branch = branch, ETag = obj.ETag.Trim('"'), Path = obj.Key }; + var entry = new LinkIndexEntry + { + Repository = repository, + Branch = branch, + ETag = obj.ETag.Trim('"'), + Path = obj.Key + }; if (linkIndex.Repositories.TryGetValue(repository, out var existingEntry)) existingEntry[branch] = entry; else - linkIndex.Repositories.Add(repository, new Dictionary { { branch, entry } }); + linkIndex.Repositories.Add(repository, new Dictionary + { + { branch, entry } + }); Console.WriteLine(entry); } diff --git a/src/docs-assembler/Links/LinkIndexLinkChecker.cs b/src/docs-assembler/Links/LinkIndexLinkChecker.cs index c1d66a29f..a64c18f5e 100644 --- a/src/docs-assembler/Links/LinkIndexLinkChecker.cs +++ b/src/docs-assembler/Links/LinkIndexLinkChecker.cs @@ -25,6 +25,16 @@ public async Task CheckAll(ICoreService githubActionsService, Cancel ctx) return await ValidateCrossLinks(githubActionsService, crossLinks, resolver, null, ctx); } + public async Task CheckRepository(ICoreService githubActionsService, string repository, Cancel ctx) + { + var fetcher = new LinksIndexCrossLinkFetcher(logger); + var resolver = new CrossLinkResolver(fetcher); + //todo add ctx + var crossLinks = await resolver.FetchLinks(); + + return await ValidateCrossLinks(githubActionsService, crossLinks, resolver, repository, ctx); + } + public async Task CheckWithLocalLinksJson( ICoreService githubActionsService, string repository, @@ -75,7 +85,10 @@ private async Task ValidateCrossLinks( _ = collector.StartAsync(ctx); foreach (var (repository, linkReference) in crossLinks.LinkReferences) { - _logger.LogInformation("Validating {Repository}", repository); + if (!string.IsNullOrEmpty(currentRepository)) + _logger.LogInformation("Validating '{CurrentRepository}://' links in {TargetRepository}", currentRepository, repository); + else + _logger.LogInformation("Validating all cross_links in {Repository}", repository); foreach (var crossLink in linkReference.CrossLinks) { // if we are filtering we only want errors from inbound links to a certain @@ -96,10 +109,10 @@ private async Task ValidateCrossLinks( } collector.EmitError(repository, s); - }, uri, out _); } } + collector.Channel.TryComplete(); await collector.StopAsync(ctx); return collector.Errors + collector.Warnings; diff --git a/src/docs-assembler/Program.cs b/src/docs-assembler/Program.cs index afd775857..05928660d 100644 --- a/src/docs-assembler/Program.cs +++ b/src/docs-assembler/Program.cs @@ -21,7 +21,7 @@ app.UseFilter(); app.UseFilter(); -app.Add("link"); +app.Add("inbound-links"); app.Add("repo"); var githubActions = ConsoleApp.ServiceProvider.GetService(); From 9e0d9e29691e5149a64e1338d4aa86498b590e6c Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 28 Feb 2025 13:06:29 +0100 Subject: [PATCH 31/45] Refactor link index command into dedicated LinkRegistry module (#649) Moved the "create-index" command from InboundLinkCommands to a new LinkRegistryCommands class for better modularity and maintainability. Updated program registration to include the new LinkRegistryCommands module and renamed the command to "update" for clarity. --- src/docs-assembler/Cli/InboundLinkCommands.cs | 80 ------------- .../Cli/LinkRegistryCommands.cs | 112 ++++++++++++++++++ src/docs-assembler/Program.cs | 1 + 3 files changed, 113 insertions(+), 80 deletions(-) create mode 100644 src/docs-assembler/Cli/LinkRegistryCommands.cs diff --git a/src/docs-assembler/Cli/InboundLinkCommands.cs b/src/docs-assembler/Cli/InboundLinkCommands.cs index 19bca6d52..740a613dc 100644 --- a/src/docs-assembler/Cli/InboundLinkCommands.cs +++ b/src/docs-assembler/Cli/InboundLinkCommands.cs @@ -68,84 +68,4 @@ public async Task ValidateLocalLinkReference(string? file = null, Cancel ct return await new LinkIndexLinkChecker(logger).CheckWithLocalLinksJson(githubActionsService, repository, file, ctx); } - - /// - /// Create an index.json file from all discovered links.json files in our S3 bucket - /// - /// - [Command("create-index")] - public async Task CreateLinkIndex(Cancel ctx = default) - { - AssignOutputLogger(); - - IAmazonS3 client = new AmazonS3Client(); - var bucketName = "elastic-docs-link-index"; - var request = new ListObjectsV2Request - { - BucketName = bucketName, - MaxKeys = 5 - }; - - Console.WriteLine("--------------------------------------"); - Console.WriteLine($"Listing the contents of {bucketName}:"); - Console.WriteLine("--------------------------------------"); - - - var linkIndex = new LinkIndex - { - Repositories = [] - }; - try - { - ListObjectsV2Response response; - do - { - response = await client.ListObjectsV2Async(request, ctx); - foreach (var obj in response.S3Objects) - { - if (!obj.Key.StartsWith("elastic/")) - continue; - - var tokens = obj.Key.Split('/'); - if (tokens.Length < 3) - continue; - - var repository = tokens[1]; - var branch = tokens[2]; - - var entry = new LinkIndexEntry - { - Repository = repository, - Branch = branch, - ETag = obj.ETag.Trim('"'), - Path = obj.Key - }; - if (linkIndex.Repositories.TryGetValue(repository, out var existingEntry)) - existingEntry[branch] = entry; - else - linkIndex.Repositories.Add(repository, new Dictionary - { - { branch, entry } - }); - Console.WriteLine(entry); - } - - // If the response is truncated, set the request ContinuationToken - // from the NextContinuationToken property of the response. - request.ContinuationToken = response.NextContinuationToken; - } while (response.IsTruncated); - } - catch (AmazonS3Exception ex) - { - Console.WriteLine($"Error encountered on server. Message:'{ex.Message}' getting list of objects."); - } - - var json = LinkIndex.Serialize(linkIndex); - Console.WriteLine(json); - - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - await client.UploadObjectFromStreamAsync(bucketName, "link-index.json", stream, new Dictionary(), ctx); - - Console.WriteLine("Uploaded latest link-index.json"); - } } diff --git a/src/docs-assembler/Cli/LinkRegistryCommands.cs b/src/docs-assembler/Cli/LinkRegistryCommands.cs new file mode 100644 index 000000000..2e13de22b --- /dev/null +++ b/src/docs-assembler/Cli/LinkRegistryCommands.cs @@ -0,0 +1,112 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// 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; +using System.Text; +using Actions.Core.Services; +using Amazon.S3; +using Amazon.S3.Model; +using ConsoleAppFramework; +using Documentation.Assembler.Links; +using Elastic.Markdown.CrossLinks; +using Elastic.Markdown.IO; +using Elastic.Markdown.IO.Discovery; +using Microsoft.Extensions.Logging; + +namespace Documentation.Assembler.Cli; + +internal sealed class LinkRegistryCommands(ILoggerFactory logger) +{ + private void AssignOutputLogger() + { + var log = logger.CreateLogger(); +#pragma warning disable CA2254 + ConsoleApp.Log = msg => log.LogInformation(msg); + ConsoleApp.LogError = msg => log.LogError(msg); +#pragma warning restore CA2254 + } + + /// + /// Create an index.json file from all discovered links.json files in our S3 bucket + /// + /// + [Command("update")] + public async Task CreateLinkIndex(Cancel ctx = default) + { + AssignOutputLogger(); + + IAmazonS3 client = new AmazonS3Client(); + var bucketName = "elastic-docs-link-index"; + var request = new ListObjectsV2Request + { + BucketName = bucketName, + MaxKeys = 5 + }; + + Console.WriteLine("--------------------------------------"); + Console.WriteLine($"Listing the contents of {bucketName}:"); + Console.WriteLine("--------------------------------------"); + + + var linkIndex = new LinkIndex + { + Repositories = [] + }; + try + { + ListObjectsV2Response response; + do + { + response = await client.ListObjectsV2Async(request, ctx); + foreach (var obj in response.S3Objects) + { + if (!obj.Key.StartsWith("elastic/")) + continue; + + var tokens = obj.Key.Split('/'); + if (tokens.Length < 3) + continue; + + var repository = tokens[1]; + var branch = tokens[2]; + + var entry = new LinkIndexEntry + { + Repository = repository, + Branch = branch, + ETag = obj.ETag.Trim('"'), + Path = obj.Key + }; + if (linkIndex.Repositories.TryGetValue(repository, out var existingEntry)) + existingEntry[branch] = entry; + else + { + linkIndex.Repositories.Add(repository, new Dictionary + { + { branch, entry } + }); + } + + Console.WriteLine(entry); + } + + // If the response is truncated, set the request ContinuationToken + // from the NextContinuationToken property of the response. + request.ContinuationToken = response.NextContinuationToken; + } while (response.IsTruncated); + } + catch (AmazonS3Exception ex) + { + Console.WriteLine($"Error encountered on server. Message:'{ex.Message}' getting list of objects."); + } + + var json = LinkIndex.Serialize(linkIndex); + Console.WriteLine(json); + + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + await client.UploadObjectFromStreamAsync(bucketName, "link-index.json", stream, new Dictionary(), ctx); + + Console.WriteLine("Uploaded latest https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/link-index.json"); + } +} diff --git a/src/docs-assembler/Program.cs b/src/docs-assembler/Program.cs index 05928660d..cb3c84074 100644 --- a/src/docs-assembler/Program.cs +++ b/src/docs-assembler/Program.cs @@ -21,6 +21,7 @@ app.UseFilter(); app.UseFilter(); +app.Add("link-registry"); app.Add("inbound-links"); app.Add("repo"); From 6a40a7862827a6747cdbe849afa23d7c85f952aa Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 28 Feb 2025 14:47:56 +0100 Subject: [PATCH 32/45] Better error messages for cross_link errors (From `docs-builder` and `docs-assembler`) (#651) * Ensure inbound links validation skips docset.yml declared repositories validation * Add warning that link links to repository not in yet in the registry * Add better errors to docs-builder to (link to links.json file) --- .../ConfigurationCrossLinkFetcher.cs | 3 +- .../CrossLinks/CrossLinkFetcher.cs | 6 +- .../CrossLinks/CrossLinkResolver.cs | 15 +++-- .../DiagnosticLinkInlineParser.cs | 6 +- src/docs-assembler/Cli/InboundLinkCommands.cs | 22 +++++--- .../Links/LinkIndexCrossLinkFetcher.cs | 3 +- .../Links/LinkIndexLinkChecker.cs | 55 ++++++++++++++----- .../TestCrossLinkResolver.cs | 7 ++- .../Framework/TestCrossLinkResolver.fs | 10 ++-- 9 files changed, 88 insertions(+), 39 deletions(-) diff --git a/src/Elastic.Markdown/CrossLinks/ConfigurationCrossLinkFetcher.cs b/src/Elastic.Markdown/CrossLinks/ConfigurationCrossLinkFetcher.cs index 63323dd4d..9b2928cd3 100644 --- a/src/Elastic.Markdown/CrossLinks/ConfigurationCrossLinkFetcher.cs +++ b/src/Elastic.Markdown/CrossLinks/ConfigurationCrossLinkFetcher.cs @@ -36,7 +36,8 @@ public override async Task Fetch() return new FetchedCrossLinks { DeclaredRepositories = declaredRepositories, - LinkReferences = dictionary.ToFrozenDictionary() + LinkReferences = dictionary.ToFrozenDictionary(), + FromConfiguration = true }; } diff --git a/src/Elastic.Markdown/CrossLinks/CrossLinkFetcher.cs b/src/Elastic.Markdown/CrossLinks/CrossLinkFetcher.cs index 1606f9689..822493549 100644 --- a/src/Elastic.Markdown/CrossLinks/CrossLinkFetcher.cs +++ b/src/Elastic.Markdown/CrossLinks/CrossLinkFetcher.cs @@ -13,12 +13,16 @@ namespace Elastic.Markdown.CrossLinks; public record FetchedCrossLinks { public required FrozenDictionary LinkReferences { get; init; } + public required HashSet DeclaredRepositories { get; init; } + public required bool FromConfiguration { get; init; } + public static FetchedCrossLinks Empty { get; } = new() { DeclaredRepositories = [], - LinkReferences = new Dictionary().ToFrozenDictionary() + LinkReferences = new Dictionary().ToFrozenDictionary(), + FromConfiguration = false }; } diff --git a/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs b/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs index eeb5bde4f..72fd72768 100644 --- a/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs +++ b/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs @@ -35,7 +35,7 @@ public record LinkIndexEntry public interface ICrossLinkResolver { Task FetchLinks(); - bool TryResolve(Action errorEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri); + bool TryResolve(Action errorEmitter, Action warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri); } public class CrossLinkResolver(CrossLinkFetcher fetcher) : ICrossLinkResolver @@ -48,8 +48,8 @@ public async Task FetchLinks() return _crossLinks; } - public bool TryResolve(Action errorEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) => - TryResolve(errorEmitter, _crossLinks, crossLinkUri, out resolvedUri); + public bool TryResolve(Action errorEmitter, Action warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) => + TryResolve(errorEmitter, warningEmitter, _crossLinks, crossLinkUri, out resolvedUri); private static Uri BaseUri { get; } = new("https://docs-v3-preview.elastic.dev"); @@ -66,6 +66,7 @@ public FetchedCrossLinks UpdateLinkReference(string repository, LinkReference li public static bool TryResolve( Action errorEmitter, + Action warningEmitter, FetchedCrossLinks fetchedCrossLinks, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri @@ -82,7 +83,10 @@ public static bool TryResolve( var declaredRepositories = fetchedCrossLinks.DeclaredRepositories; if (!declaredRepositories.Contains(crossLinkUri.Scheme)) { - errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links"); + if (fetchedCrossLinks.FromConfiguration) + errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links: '{crossLinkUri}'"); + else + warningEmitter($"'{crossLinkUri.Scheme}' is not yet publishing to the links registry: '{crossLinkUri}'"); return false; } @@ -163,7 +167,8 @@ private static bool LookupLink( return true; } - errorEmitter($"'{lookupPath}' is not a valid link in the '{crossLinkUri.Scheme}' cross link repository."); + var linksJson = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{crossLinkUri.Scheme}/main/links.json"; + errorEmitter($"'{lookupPath}' is not a valid link in the '{crossLinkUri.Scheme}' cross link index: {linksJson}."); return false; } diff --git a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs index a451c2007..fa451fc7d 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs @@ -162,7 +162,11 @@ private static void ProcessCrossLink(LinkInline link, InlineProcessor processor, if (url != null) context.Build.Collector.EmitCrossLink(url); - if (context.CrossLinkResolver.TryResolve(s => processor.EmitError(link, s), uri, out var resolvedUri)) + if (context.CrossLinkResolver.TryResolve( + s => processor.EmitError(link, s), + s => processor.EmitWarning(link, s), + uri, out var resolvedUri) + ) link.Url = resolvedUri.ToString(); } diff --git a/src/docs-assembler/Cli/InboundLinkCommands.cs b/src/docs-assembler/Cli/InboundLinkCommands.cs index 740a613dc..a74228a77 100644 --- a/src/docs-assembler/Cli/InboundLinkCommands.cs +++ b/src/docs-assembler/Cli/InboundLinkCommands.cs @@ -18,6 +18,8 @@ namespace Documentation.Assembler.Cli; internal sealed class InboundLinkCommands(ILoggerFactory logger, ICoreService githubActionsService) { + private readonly LinkIndexLinkChecker _linkIndexLinkChecker = new(logger, githubActionsService); + private void AssignOutputLogger() { var log = logger.CreateLogger(); @@ -33,22 +35,26 @@ private void AssignOutputLogger() public async Task ValidateAllInboundLinks(Cancel ctx = default) { AssignOutputLogger(); - return await new LinkIndexLinkChecker(logger).CheckAll(githubActionsService, ctx); + return await _linkIndexLinkChecker.CheckAll(ctx); } /// Validate all published cross_links in all published links.json files. - /// + /// + /// /// [Command("validate")] - public async Task ValidateRepoInboundLinks(string? repository = null, Cancel ctx = default) + public async Task ValidateRepoInboundLinks(string? from = null, string? to = null, Cancel ctx = default) { AssignOutputLogger(); var fs = new FileSystem(); var root = fs.DirectoryInfo.New(Paths.Root.FullName); - repository ??= GitCheckoutInformation.Create(root, new FileSystem()).RepositoryName; - if (repository == null) - throw new Exception("Unable to determine repository name"); - return await new LinkIndexLinkChecker(logger).CheckRepository(githubActionsService, repository, ctx); + if (from == null && to == null) + { + from ??= GitCheckoutInformation.Create(root, new FileSystem()).RepositoryName; + if (from == null) + throw new Exception("Unable to determine repository name"); + } + return await _linkIndexLinkChecker.CheckRepository(from, to, ctx); } /// @@ -66,6 +72,6 @@ public async Task ValidateLocalLinkReference(string? file = null, Cancel ct var repository = GitCheckoutInformation.Create(root, new FileSystem()).RepositoryName ?? throw new Exception("Unable to determine repository name"); - return await new LinkIndexLinkChecker(logger).CheckWithLocalLinksJson(githubActionsService, repository, file, ctx); + return await _linkIndexLinkChecker.CheckWithLocalLinksJson(repository, file, ctx); } } diff --git a/src/docs-assembler/Links/LinkIndexCrossLinkFetcher.cs b/src/docs-assembler/Links/LinkIndexCrossLinkFetcher.cs index ae02140c3..0204f56ac 100644 --- a/src/docs-assembler/Links/LinkIndexCrossLinkFetcher.cs +++ b/src/docs-assembler/Links/LinkIndexCrossLinkFetcher.cs @@ -27,7 +27,8 @@ public override async Task Fetch() return new FetchedCrossLinks { DeclaredRepositories = declaredRepositories, - LinkReferences = dictionary.ToFrozenDictionary() + LinkReferences = dictionary.ToFrozenDictionary(), + FromConfiguration = false }; } } diff --git a/src/docs-assembler/Links/LinkIndexLinkChecker.cs b/src/docs-assembler/Links/LinkIndexLinkChecker.cs index a64c18f5e..c29a68701 100644 --- a/src/docs-assembler/Links/LinkIndexLinkChecker.cs +++ b/src/docs-assembler/Links/LinkIndexLinkChecker.cs @@ -11,32 +11,44 @@ namespace Documentation.Assembler.Links; -public class LinkIndexLinkChecker(ILoggerFactory logger) +public class LinkIndexLinkChecker(ILoggerFactory logger, ICoreService githubActionsService) { private readonly ILogger _logger = logger.CreateLogger(); - public async Task CheckAll(ICoreService githubActionsService, Cancel ctx) + private sealed record RepositoryFilter + { + public string? LinksTo { get; set; } + public string? LinksFrom { get; set; } + + public static RepositoryFilter None => new(); + } + + public async Task CheckAll(Cancel ctx) { var fetcher = new LinksIndexCrossLinkFetcher(logger); var resolver = new CrossLinkResolver(fetcher); //todo add ctx var crossLinks = await resolver.FetchLinks(); - return await ValidateCrossLinks(githubActionsService, crossLinks, resolver, null, ctx); + return await ValidateCrossLinks(crossLinks, resolver, RepositoryFilter.None, ctx); } - public async Task CheckRepository(ICoreService githubActionsService, string repository, Cancel ctx) + public async Task CheckRepository(string? toRepository, string? fromRepository, Cancel ctx) { var fetcher = new LinksIndexCrossLinkFetcher(logger); var resolver = new CrossLinkResolver(fetcher); //todo add ctx var crossLinks = await resolver.FetchLinks(); + var filter = new RepositoryFilter + { + LinksTo = toRepository, + LinksFrom = fromRepository + }; - return await ValidateCrossLinks(githubActionsService, crossLinks, resolver, repository, ctx); + return await ValidateCrossLinks(crossLinks, resolver, filter, ctx); } public async Task CheckWithLocalLinksJson( - ICoreService githubActionsService, string repository, string localLinksJson, Cancel ctx @@ -70,33 +82,44 @@ Cancel ctx } _logger.LogInformation("Validating all cross links to {Repository}:// from all repositories published to link-index.json", repository); + var filter = new RepositoryFilter + { + LinksTo = repository + }; - return await ValidateCrossLinks(githubActionsService, crossLinks, resolver, repository, ctx); + return await ValidateCrossLinks(crossLinks, resolver, filter, ctx); } private async Task ValidateCrossLinks( - ICoreService githubActionsService, FetchedCrossLinks crossLinks, CrossLinkResolver resolver, - string? currentRepository, + RepositoryFilter filter, Cancel ctx) { var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); _ = collector.StartAsync(ctx); foreach (var (repository, linkReference) in crossLinks.LinkReferences) { - if (!string.IsNullOrEmpty(currentRepository)) - _logger.LogInformation("Validating '{CurrentRepository}://' links in {TargetRepository}", currentRepository, repository); + if (!string.IsNullOrEmpty(filter.LinksTo)) + _logger.LogInformation("Validating '{CurrentRepository}://' links in {TargetRepository}", filter.LinksTo, repository); + else if (!string.IsNullOrEmpty(filter.LinksFrom)) + { + if (repository != filter.LinksFrom) + continue; + _logger.LogInformation("Validating cross_links from {TargetRepository}", filter.LinksFrom); + } else _logger.LogInformation("Validating all cross_links in {Repository}", repository); + foreach (var crossLink in linkReference.CrossLinks) { // if we are filtering we only want errors from inbound links to a certain // repository var uri = new Uri(crossLink); - if (currentRepository != null && uri.Scheme != currentRepository) + if (filter.LinksTo != null && uri.Scheme != filter.LinksTo) continue; + var linksJson = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{uri.Scheme}/main/links.json"; _ = resolver.TryResolve(s => { if (s.Contains("is not a valid link in the")) @@ -104,17 +127,19 @@ private async Task ValidateCrossLinks( // var error = $"'elastic/{repository}' links to unknown file: " + s; error = error.Replace("is not a valid link in the", "in the"); - collector.EmitError($"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{uri.Scheme}/main/links.json", error); + collector.EmitError(linksJson, error); return; } collector.EmitError(repository, s); - }, uri, out _); + }, s => collector.EmitWarning(linksJson, s), uri, out _); } } collector.Channel.TryComplete(); await collector.StopAsync(ctx); - return collector.Errors + collector.Warnings; + // non-strict for now + return collector.Errors; + // return collector.Errors + collector.Warnings; } } diff --git a/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs b/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs index f08226b6e..bda2b497c 100644 --- a/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs +++ b/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs @@ -49,11 +49,12 @@ public Task FetchLinks() _crossLinks = new FetchedCrossLinks { DeclaredRepositories = DeclaredRepositories, - LinkReferences = LinkReferences.ToFrozenDictionary() + LinkReferences = LinkReferences.ToFrozenDictionary(), + FromConfiguration = true }; return Task.FromResult(_crossLinks); } - public bool TryResolve(Action errorEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) => - CrossLinkResolver.TryResolve(errorEmitter, _crossLinks, crossLinkUri, out resolvedUri); + public bool TryResolve(Action errorEmitter, Action warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) => + CrossLinkResolver.TryResolve(errorEmitter, warningEmitter, _crossLinks, crossLinkUri, out resolvedUri); } diff --git a/tests/authoring/Framework/TestCrossLinkResolver.fs b/tests/authoring/Framework/TestCrossLinkResolver.fs index 42a08868c..9598de485 100644 --- a/tests/authoring/Framework/TestCrossLinkResolver.fs +++ b/tests/authoring/Framework/TestCrossLinkResolver.fs @@ -65,16 +65,18 @@ type TestCrossLinkResolver (config: ConfigurationFile) = let crossLinks = FetchedCrossLinks( DeclaredRepositories=this.DeclaredRepositories, - LinkReferences=this.LinkReferences.ToFrozenDictionary() + LinkReferences=this.LinkReferences.ToFrozenDictionary(), + FromConfiguration=true ) Task.FromResult crossLinks - member this.TryResolve(errorEmitter, crossLinkUri, []resolvedUri : byref) = + member this.TryResolve(errorEmitter, warningEmitter, crossLinkUri, []resolvedUri : byref) = let crossLinks = FetchedCrossLinks( DeclaredRepositories=this.DeclaredRepositories, - LinkReferences=this.LinkReferences.ToFrozenDictionary() + LinkReferences=this.LinkReferences.ToFrozenDictionary(), + FromConfiguration=true ) - CrossLinkResolver.TryResolve(errorEmitter, crossLinks, crossLinkUri, &resolvedUri); + CrossLinkResolver.TryResolve(errorEmitter, warningEmitter, crossLinks, crossLinkUri, &resolvedUri); From bb0c9cabf6456f143f109ef8423c44fb795c6646 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 28 Feb 2025 15:16:54 +0100 Subject: [PATCH 33/45] Add logging to git remote resolving (#652) --- .../IO/Discovery/GitCheckoutInformation.cs | 30 +++++++++++++++++-- src/docs-assembler/Cli/InboundLinkCommands.cs | 4 +-- .../DocSet/LinkReferenceTests.cs | 3 +- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs b/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs index c940b5bfa..0334e9316 100644 --- a/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs +++ b/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs @@ -4,6 +4,7 @@ using System.IO.Abstractions; using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; using SoftCircuits.IniFileParser; namespace Elastic.Markdown.IO.Discovery; @@ -37,7 +38,7 @@ public string? RepositoryName } // manual read because libgit2sharp is not yet AOT ready - public static GitCheckoutInformation Create(IDirectoryInfo source, IFileSystem fileSystem) + public static GitCheckoutInformation Create(IDirectoryInfo source, IFileSystem fileSystem, ILogger? logger = null) { if (fileSystem is not FileSystem) { @@ -53,7 +54,10 @@ public static GitCheckoutInformation Create(IDirectoryInfo source, IFileSystem f var fakeRef = Guid.NewGuid().ToString()[..16]; var gitConfig = Git(source, ".git/config"); if (!gitConfig.Exists) + { + logger?.LogInformation("Git checkout information not available."); return Unavailable; + } var head = Read(source, ".git/HEAD") ?? fakeRef; var gitRef = head; @@ -74,20 +78,38 @@ public static GitCheckoutInformation Create(IDirectoryInfo source, IFileSystem f ini.Load(streamReader); var remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY"); + logger?.LogInformation("Remote from environment: {GitRemote}", remote); if (string.IsNullOrEmpty(remote)) + { remote = BranchTrackingRemote(branch, ini); + logger?.LogInformation("Remote from branch: {GitRemote}", remote); + } + if (string.IsNullOrEmpty(remote)) + { remote = BranchTrackingRemote("main", ini); + logger?.LogInformation("Remote from main branch: {GitRemote}", remote); + } + if (string.IsNullOrEmpty(remote)) + { remote = BranchTrackingRemote("master", ini); + logger?.LogInformation("Remote from master branch: {GitRemote}", remote); + } + if (string.IsNullOrEmpty(remote)) + { remote = "elastic/docs-builder-unknown"; + logger?.LogInformation("Remote from fallback: {GitRemote}", remote); + } remote = remote.AsSpan().TrimEnd("git").TrimEnd('.').ToString(); + + logger?.LogInformation("Remote trimmed: {GitRemote}", remote); if (remote.EndsWith("docs-conten")) remote += "t"; - return new GitCheckoutInformation + var info = new GitCheckoutInformation { Ref = gitRef, Branch = branch, @@ -95,6 +117,10 @@ public static GitCheckoutInformation Create(IDirectoryInfo source, IFileSystem f RepositoryName = remote.Split('/').Last() }; + logger?.LogInformation("-> Remote Name: {GitRemote}", info.Remote); + logger?.LogInformation("-> Repository Name: {RepositoryName}", info.RepositoryName); + return info; + IFileInfo Git(IDirectoryInfo directoryInfo, string path) => fileSystem.FileInfo.New(Path.Combine(directoryInfo.FullName, path)); diff --git a/src/docs-assembler/Cli/InboundLinkCommands.cs b/src/docs-assembler/Cli/InboundLinkCommands.cs index a74228a77..f32dc3afb 100644 --- a/src/docs-assembler/Cli/InboundLinkCommands.cs +++ b/src/docs-assembler/Cli/InboundLinkCommands.cs @@ -50,7 +50,7 @@ public async Task ValidateRepoInboundLinks(string? from = null, string? to var root = fs.DirectoryInfo.New(Paths.Root.FullName); if (from == null && to == null) { - from ??= GitCheckoutInformation.Create(root, new FileSystem()).RepositoryName; + from ??= GitCheckoutInformation.Create(root, new FileSystem(), logger.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName; if (from == null) throw new Exception("Unable to determine repository name"); } @@ -69,7 +69,7 @@ public async Task ValidateLocalLinkReference(string? file = null, Cancel ct file ??= ".artifacts/docs/html/links.json"; var fs = new FileSystem(); var root = fs.DirectoryInfo.New(Paths.Root.FullName); - var repository = GitCheckoutInformation.Create(root, new FileSystem()).RepositoryName + var repository = GitCheckoutInformation.Create(root, new FileSystem(), logger.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName ?? throw new Exception("Unable to determine repository name"); return await _linkIndexLinkChecker.CheckWithLocalLinksJson(repository, file, ctx); diff --git a/tests/Elastic.Markdown.Tests/DocSet/LinkReferenceTests.cs b/tests/Elastic.Markdown.Tests/DocSet/LinkReferenceTests.cs index 4f17e636f..cdb6c1581 100644 --- a/tests/Elastic.Markdown.Tests/DocSet/LinkReferenceTests.cs +++ b/tests/Elastic.Markdown.Tests/DocSet/LinkReferenceTests.cs @@ -6,6 +6,7 @@ using Elastic.Markdown.IO.Discovery; using Elastic.Markdown.IO.State; using FluentAssertions; +using Microsoft.Extensions.Logging; namespace Elastic.Markdown.Tests.DocSet; @@ -34,7 +35,7 @@ public class GitCheckoutInformationTests(ITestOutputHelper output) : NavigationT public void Create() { var root = ReadFileSystem.DirectoryInfo.New(Paths.Root.FullName); - var git = GitCheckoutInformation.Create(root, ReadFileSystem); + var git = GitCheckoutInformation.Create(root, ReadFileSystem, LoggerFactory.CreateLogger(nameof(GitCheckoutInformation))); git.Should().NotBeNull(); git.Branch.Should().NotBeNullOrWhiteSpace(); From 483f0268f89b8bcadcb574ec27c470e8b3eb85a1 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Fri, 28 Feb 2025 15:58:01 +0100 Subject: [PATCH 34/45] Update cross-repo links syntax info (#654) --- docs/syntax/links.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/syntax/links.md b/docs/syntax/links.md index 7f11243be..676365cf0 100644 --- a/docs/syntax/links.md +++ b/docs/syntax/links.md @@ -84,6 +84,10 @@ The syntax follows the format `://`, where: - `scheme`: The target repository name (e.g., kibana, beats) - `path`: The file path within that repository +:::{important} +The `path` in cross-repo links must be relative to the `docset.yml` file and not the full path within the repo +::: + ### External links Link to websites and resources outside the Elastic docs: @@ -139,4 +143,4 @@ This syntax exists to aid with migration. It is scheduled for removal and **shou ```markdown Some text $$$custom-anchor$$$ more text -``` \ No newline at end of file +``` From 2763ef7c9cd18ed07b90e58fccabe80fe4e8fa02 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 28 Feb 2025 16:02:17 +0100 Subject: [PATCH 35/45] If GITHUB_REPOSITORY is set use it verbatim (#655) --- .../IO/Discovery/GitCheckoutInformation.cs | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs b/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs index 0334e9316..f32c2fd2e 100644 --- a/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs +++ b/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs @@ -78,37 +78,30 @@ public static GitCheckoutInformation Create(IDirectoryInfo source, IFileSystem f ini.Load(streamReader); var remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY"); - logger?.LogInformation("Remote from environment: {GitRemote}", remote); if (string.IsNullOrEmpty(remote)) { remote = BranchTrackingRemote(branch, ini); logger?.LogInformation("Remote from branch: {GitRemote}", remote); - } - - if (string.IsNullOrEmpty(remote)) - { - remote = BranchTrackingRemote("main", ini); - logger?.LogInformation("Remote from main branch: {GitRemote}", remote); - } + if (string.IsNullOrEmpty(remote)) + { + remote = BranchTrackingRemote("main", ini); + logger?.LogInformation("Remote from main branch: {GitRemote}", remote); + } - if (string.IsNullOrEmpty(remote)) - { - remote = BranchTrackingRemote("master", ini); - logger?.LogInformation("Remote from master branch: {GitRemote}", remote); - } + if (string.IsNullOrEmpty(remote)) + { + remote = BranchTrackingRemote("master", ini); + logger?.LogInformation("Remote from master branch: {GitRemote}", remote); + } - if (string.IsNullOrEmpty(remote)) - { - remote = "elastic/docs-builder-unknown"; - logger?.LogInformation("Remote from fallback: {GitRemote}", remote); + if (string.IsNullOrEmpty(remote)) + { + remote = "elastic/docs-builder-unknown"; + logger?.LogInformation("Remote from fallback: {GitRemote}", remote); + } + remote = remote.AsSpan().TrimEnd("git").TrimEnd('.').ToString(); } - remote = remote.AsSpan().TrimEnd("git").TrimEnd('.').ToString(); - - logger?.LogInformation("Remote trimmed: {GitRemote}", remote); - if (remote.EndsWith("docs-conten")) - remote += "t"; - var info = new GitCheckoutInformation { Ref = gitRef, From a8d4f79665bc385d4a16ce6a7b71fb725e9c3ccf Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 28 Feb 2025 18:58:56 +0100 Subject: [PATCH 36/45] Share inbound-links commands with `docs-builder` (#657) --- .../Console/ConsoleDiagnosticsCollector.cs | 4 + .../Console/ErrataFileSourceRepository.cs | 34 ++++---- .../Diagnostics/DiagnosticsChannel.cs | 9 +- .../LinkIndexCrossLinkFetcher.cs | 2 +- .../InboundLinks}/LinkIndexLinkChecker.cs | 25 +++--- src/docs-assembler/Cli/InboundLinkCommands.cs | 26 +++--- .../Cli/LinkRegistryCommands.cs | 9 +- src/docs-assembler/Cli/RepositoryCommands.cs | 4 +- src/docs-builder/Cli/Commands.cs | 11 +-- src/docs-builder/Cli/InboundLinkCommands.cs | 84 +++++++++++++++++++ src/docs-builder/Program.cs | 1 + 11 files changed, 148 insertions(+), 61 deletions(-) rename src/{docs-assembler/Links => Elastic.Markdown/InboundLinks}/LinkIndexCrossLinkFetcher.cs (96%) rename src/{docs-assembler/Links => Elastic.Markdown/InboundLinks}/LinkIndexLinkChecker.cs (84%) create mode 100644 src/docs-builder/Cli/InboundLinkCommands.cs diff --git a/src/Elastic.Documentation.Tooling/Diagnostics/Console/ConsoleDiagnosticsCollector.cs b/src/Elastic.Documentation.Tooling/Diagnostics/Console/ConsoleDiagnosticsCollector.cs index 3ca857500..f863a1163 100644 --- a/src/Elastic.Documentation.Tooling/Diagnostics/Console/ConsoleDiagnosticsCollector.cs +++ b/src/Elastic.Documentation.Tooling/Diagnostics/Console/ConsoleDiagnosticsCollector.cs @@ -25,8 +25,12 @@ protected override void HandleItem(Diagnostic diagnostic) _errors.Add(diagnostic); } + private bool _stopped; public override async Task StopAsync(Cancel cancellationToken) { + if (_stopped) + return; + _stopped = true; var repository = new ErrataFileSourceRepository(); repository.WriteDiagnosticsToConsole(_errors, _warnings); diff --git a/src/Elastic.Documentation.Tooling/Diagnostics/Console/ErrataFileSourceRepository.cs b/src/Elastic.Documentation.Tooling/Diagnostics/Console/ErrataFileSourceRepository.cs index 391a4259e..13c3f614b 100644 --- a/src/Elastic.Documentation.Tooling/Diagnostics/Console/ErrataFileSourceRepository.cs +++ b/src/Elastic.Documentation.Tooling/Diagnostics/Console/ErrataFileSourceRepository.cs @@ -26,11 +26,11 @@ public bool TryGet(string id, [NotNullWhen(true)] out Source? source) public void WriteDiagnosticsToConsole(IReadOnlyCollection errors, IReadOnlyCollection warnings) { var report = new Report(this); - var limttedErrors = errors.Take(100).ToArray(); - var limittedWarnings = warnings.Take(100 - limttedErrors.Length); - var limitted = limittedWarnings.Concat(limttedErrors).ToArray(); + var limitedErrors = errors.Take(100).ToArray(); + var limitedWarnings = warnings.Take(100 - limitedErrors.Length); + var limited = limitedWarnings.Concat(limitedErrors).ToArray(); - foreach (var item in limitted) + foreach (var item in limited) { var d = item.Severity switch { @@ -55,22 +55,20 @@ public void WriteDiagnosticsToConsole(IReadOnlyCollection errors, IR var totalErrorCount = errors.Count + warnings.Count; AnsiConsole.WriteLine(); - if (totalErrorCount > 0) - { - AnsiConsole.Write(new Markup($" [bold]The following errors and warnings were found in the documentation[/]")); - AnsiConsole.WriteLine(); - AnsiConsole.WriteLine(); - // Render the report - report.Render(AnsiConsole.Console); - - AnsiConsole.WriteLine(); - AnsiConsole.WriteLine(); + if (totalErrorCount <= 0) + return; + AnsiConsole.Write(new Markup($" [bold]The following errors and warnings were found in the documentation[/]")); + AnsiConsole.WriteLine(); + AnsiConsole.WriteLine(); + // Render the report + report.Render(AnsiConsole.Console); - if (limitted.Length <= totalErrorCount) - AnsiConsole.Write(new Markup($" [bold]Only shown the first [yellow]{limitted.Length}[/] diagnostics out of [yellow]{totalErrorCount}[/][/]")); + AnsiConsole.WriteLine(); + AnsiConsole.WriteLine(); - AnsiConsole.WriteLine(); + if (totalErrorCount > limited.Length) + AnsiConsole.Write(new Markup($" [bold]Only shown the first [yellow]{limited.Length}[/] diagnostics out of [yellow]{totalErrorCount}[/][/]")); - } + AnsiConsole.WriteLine(); } } diff --git a/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs b/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs index f30eb760c..96f86bf69 100644 --- a/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs +++ b/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs @@ -61,7 +61,7 @@ public interface IDiagnosticsOutput } public class DiagnosticsCollector(IReadOnlyCollection outputs) - : IHostedService + : IHostedService, IAsyncDisposable { public DiagnosticsChannel Channel { get; } = new(); @@ -156,4 +156,11 @@ public void EmitWarning(string file, string message) }; Channel.Write(d); } + + public async ValueTask DisposeAsync() + { + Channel.TryComplete(); + await StopAsync(CancellationToken.None); + GC.SuppressFinalize(this); + } } diff --git a/src/docs-assembler/Links/LinkIndexCrossLinkFetcher.cs b/src/Elastic.Markdown/InboundLinks/LinkIndexCrossLinkFetcher.cs similarity index 96% rename from src/docs-assembler/Links/LinkIndexCrossLinkFetcher.cs rename to src/Elastic.Markdown/InboundLinks/LinkIndexCrossLinkFetcher.cs index 0204f56ac..886e2561c 100644 --- a/src/docs-assembler/Links/LinkIndexCrossLinkFetcher.cs +++ b/src/Elastic.Markdown/InboundLinks/LinkIndexCrossLinkFetcher.cs @@ -7,7 +7,7 @@ using Elastic.Markdown.IO.State; using Microsoft.Extensions.Logging; -namespace Documentation.Assembler.Links; +namespace Elastic.Markdown.InboundLinks; public class LinksIndexCrossLinkFetcher(ILoggerFactory logger) : CrossLinkFetcher(logger) { diff --git a/src/docs-assembler/Links/LinkIndexLinkChecker.cs b/src/Elastic.Markdown/InboundLinks/LinkIndexLinkChecker.cs similarity index 84% rename from src/docs-assembler/Links/LinkIndexLinkChecker.cs rename to src/Elastic.Markdown/InboundLinks/LinkIndexLinkChecker.cs index c29a68701..151b36382 100644 --- a/src/docs-assembler/Links/LinkIndexLinkChecker.cs +++ b/src/Elastic.Markdown/InboundLinks/LinkIndexLinkChecker.cs @@ -2,16 +2,15 @@ // 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 Actions.Core.Services; -using Elastic.Documentation.Tooling.Diagnostics.Console; using Elastic.Markdown.CrossLinks; +using Elastic.Markdown.Diagnostics; using Elastic.Markdown.IO; using Elastic.Markdown.IO.State; using Microsoft.Extensions.Logging; -namespace Documentation.Assembler.Links; +namespace Elastic.Markdown.InboundLinks; -public class LinkIndexLinkChecker(ILoggerFactory logger, ICoreService githubActionsService) +public class LinkIndexLinkChecker(ILoggerFactory logger) { private readonly ILogger _logger = logger.CreateLogger(); @@ -23,17 +22,17 @@ private sealed record RepositoryFilter public static RepositoryFilter None => new(); } - public async Task CheckAll(Cancel ctx) + public async Task CheckAll(DiagnosticsCollector collector, Cancel ctx) { var fetcher = new LinksIndexCrossLinkFetcher(logger); var resolver = new CrossLinkResolver(fetcher); //todo add ctx var crossLinks = await resolver.FetchLinks(); - return await ValidateCrossLinks(crossLinks, resolver, RepositoryFilter.None, ctx); + return await ValidateCrossLinks(collector, crossLinks, resolver, RepositoryFilter.None, ctx); } - public async Task CheckRepository(string? toRepository, string? fromRepository, Cancel ctx) + public async Task CheckRepository(DiagnosticsCollector collector, string? toRepository, string? fromRepository, Cancel ctx) { var fetcher = new LinksIndexCrossLinkFetcher(logger); var resolver = new CrossLinkResolver(fetcher); @@ -45,14 +44,12 @@ public async Task CheckRepository(string? toRepository, string? fromReposit LinksFrom = fromRepository }; - return await ValidateCrossLinks(crossLinks, resolver, filter, ctx); + return await ValidateCrossLinks(collector, crossLinks, resolver, filter, ctx); } - public async Task CheckWithLocalLinksJson( - string repository, + public async Task CheckWithLocalLinksJson(DiagnosticsCollector collector, string repository, string localLinksJson, - Cancel ctx - ) + Cancel ctx) { var fetcher = new LinksIndexCrossLinkFetcher(logger); var resolver = new CrossLinkResolver(fetcher); @@ -87,16 +84,16 @@ Cancel ctx LinksTo = repository }; - return await ValidateCrossLinks(crossLinks, resolver, filter, ctx); + return await ValidateCrossLinks(collector, crossLinks, resolver, filter, ctx); } private async Task ValidateCrossLinks( + DiagnosticsCollector collector, FetchedCrossLinks crossLinks, CrossLinkResolver resolver, RepositoryFilter filter, Cancel ctx) { - var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); _ = collector.StartAsync(ctx); foreach (var (repository, linkReference) in crossLinks.LinkReferences) { diff --git a/src/docs-assembler/Cli/InboundLinkCommands.cs b/src/docs-assembler/Cli/InboundLinkCommands.cs index f32dc3afb..532726876 100644 --- a/src/docs-assembler/Cli/InboundLinkCommands.cs +++ b/src/docs-assembler/Cli/InboundLinkCommands.cs @@ -2,14 +2,12 @@ // 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.Diagnostics.CodeAnalysis; using System.IO.Abstractions; -using System.Text; using Actions.Core.Services; -using Amazon.S3; -using Amazon.S3.Model; using ConsoleAppFramework; -using Documentation.Assembler.Links; -using Elastic.Markdown.CrossLinks; +using Elastic.Documentation.Tooling.Diagnostics.Console; +using Elastic.Markdown.InboundLinks; using Elastic.Markdown.IO; using Elastic.Markdown.IO.Discovery; using Microsoft.Extensions.Logging; @@ -18,15 +16,14 @@ namespace Documentation.Assembler.Cli; internal sealed class InboundLinkCommands(ILoggerFactory logger, ICoreService githubActionsService) { - private readonly LinkIndexLinkChecker _linkIndexLinkChecker = new(logger, githubActionsService); + private readonly LinkIndexLinkChecker _linkIndexLinkChecker = new(logger); + [SuppressMessage("Usage", "CA2254:Template should be a static expression")] private void AssignOutputLogger() { var log = logger.CreateLogger(); -#pragma warning disable CA2254 ConsoleApp.Log = msg => log.LogInformation(msg); ConsoleApp.LogError = msg => log.LogError(msg); -#pragma warning restore CA2254 } /// Validate all published cross_links in all published links.json files. @@ -35,7 +32,8 @@ private void AssignOutputLogger() public async Task ValidateAllInboundLinks(Cancel ctx = default) { AssignOutputLogger(); - return await _linkIndexLinkChecker.CheckAll(ctx); + await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); + return await _linkIndexLinkChecker.CheckAll(collector, ctx); } /// Validate all published cross_links in all published links.json files. @@ -54,16 +52,17 @@ public async Task ValidateRepoInboundLinks(string? from = null, string? to if (from == null) throw new Exception("Unable to determine repository name"); } - return await _linkIndexLinkChecker.CheckRepository(from, to, ctx); + await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); + return await _linkIndexLinkChecker.CheckRepository(collector, to, from, ctx); } /// /// Validate a locally published links.json file against all published links.json files in the registry /// - /// + /// Path to `links.json` defaults to '.artifacts/docs/html/links.json' /// [Command("validate-link-reference")] - public async Task ValidateLocalLinkReference(string? file = null, Cancel ctx = default) + public async Task ValidateLocalLinkReference([Argument] string? file = null, Cancel ctx = default) { AssignOutputLogger(); file ??= ".artifacts/docs/html/links.json"; @@ -72,6 +71,7 @@ public async Task ValidateLocalLinkReference(string? file = null, Cancel ct var repository = GitCheckoutInformation.Create(root, new FileSystem(), logger.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName ?? throw new Exception("Unable to determine repository name"); - return await _linkIndexLinkChecker.CheckWithLocalLinksJson(repository, file, ctx); + await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); + return await _linkIndexLinkChecker.CheckWithLocalLinksJson(collector, repository, file, ctx); } } diff --git a/src/docs-assembler/Cli/LinkRegistryCommands.cs b/src/docs-assembler/Cli/LinkRegistryCommands.cs index 2e13de22b..7f16becec 100644 --- a/src/docs-assembler/Cli/LinkRegistryCommands.cs +++ b/src/docs-assembler/Cli/LinkRegistryCommands.cs @@ -2,29 +2,24 @@ // 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; +using System.Diagnostics.CodeAnalysis; using System.Text; -using Actions.Core.Services; using Amazon.S3; using Amazon.S3.Model; using ConsoleAppFramework; -using Documentation.Assembler.Links; using Elastic.Markdown.CrossLinks; -using Elastic.Markdown.IO; -using Elastic.Markdown.IO.Discovery; using Microsoft.Extensions.Logging; namespace Documentation.Assembler.Cli; internal sealed class LinkRegistryCommands(ILoggerFactory logger) { + [SuppressMessage("Usage", "CA2254:Template should be a static expression")] private void AssignOutputLogger() { var log = logger.CreateLogger(); -#pragma warning disable CA2254 ConsoleApp.Log = msg => log.LogInformation(msg); ConsoleApp.LogError = msg => log.LogError(msg); -#pragma warning restore CA2254 } /// diff --git a/src/docs-assembler/Cli/RepositoryCommands.cs b/src/docs-assembler/Cli/RepositoryCommands.cs index 72c147cb1..2c9b8c9f7 100644 --- a/src/docs-assembler/Cli/RepositoryCommands.cs +++ b/src/docs-assembler/Cli/RepositoryCommands.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using ConsoleAppFramework; using Elastic.Markdown.IO; using Microsoft.Extensions.Logging; @@ -23,13 +24,12 @@ public void Handle(Exception e) { } internal sealed class RepositoryCommands(ILoggerFactory logger) { + [SuppressMessage("Usage", "CA2254:Template should be a static expression")] private void AssignOutputLogger() { var log = logger.CreateLogger(); -#pragma warning disable CA2254 ConsoleApp.Log = msg => log.LogInformation(msg); ConsoleApp.LogError = msg => log.LogError(msg); -#pragma warning restore CA2254 } // would love to use libgit2 so there is no git dependency but diff --git a/src/docs-builder/Cli/Commands.cs b/src/docs-builder/Cli/Commands.cs index 396dbc874..f0fe63da8 100644 --- a/src/docs-builder/Cli/Commands.cs +++ b/src/docs-builder/Cli/Commands.cs @@ -1,6 +1,8 @@ // Licensed to Elasticsearch B.V under one or more agreements. // 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.Diagnostics.CodeAnalysis; using System.IO.Abstractions; using Actions.Core.Services; using ConsoleAppFramework; @@ -9,7 +11,6 @@ using Elastic.Documentation.Tooling.Filters; using Elastic.Markdown; using Elastic.Markdown.IO; -using Elastic.Markdown.IO.Navigation; using Elastic.Markdown.Refactor; using Microsoft.Extensions.Logging; @@ -17,13 +18,12 @@ namespace Documentation.Builder.Cli; internal sealed class Commands(ILoggerFactory logger, ICoreService githubActionsService) { + [SuppressMessage("Usage", "CA2254:Template should be a static expression")] private void AssignOutputLogger() { var log = logger.CreateLogger(); -#pragma warning disable CA2254 ConsoleApp.Log = msg => log.LogInformation(msg); ConsoleApp.LogError = msg => log.LogError(msg); -#pragma warning restore CA2254 } /// @@ -74,7 +74,7 @@ public async Task Generate( AssignOutputLogger(); pathPrefix ??= githubActionsService.GetInput("prefix"); var fileSystem = new FileSystem(); - var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); + await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); var runningOnCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")); BuildContext context; @@ -103,6 +103,7 @@ public async Task Generate( await githubActionsService.SetOutputAsync("skip", "true"); return 0; } + if (runningOnCi) await githubActionsService.SetOutputAsync("skip", "false"); var set = new DocumentationSet(context, logger); @@ -166,7 +167,7 @@ public async Task Move( { AssignOutputLogger(); var fileSystem = new FileSystem(); - var collector = new ConsoleDiagnosticsCollector(logger, null); + await using var collector = new ConsoleDiagnosticsCollector(logger, null); var context = new BuildContext(collector, fileSystem, fileSystem, path, null); var set = new DocumentationSet(context, logger); diff --git a/src/docs-builder/Cli/InboundLinkCommands.cs b/src/docs-builder/Cli/InboundLinkCommands.cs new file mode 100644 index 000000000..4f8c9f8b7 --- /dev/null +++ b/src/docs-builder/Cli/InboundLinkCommands.cs @@ -0,0 +1,84 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// 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.Diagnostics.CodeAnalysis; +using System.IO.Abstractions; +using Actions.Core.Services; +using ConsoleAppFramework; +using Elastic.Documentation.Tooling.Diagnostics.Console; +using Elastic.Documentation.Tooling.Filters; +using Elastic.Markdown.InboundLinks; +using Elastic.Markdown.IO; +using Elastic.Markdown.IO.Discovery; +using Microsoft.Extensions.Logging; + +namespace Documentation.Builder.Cli; + +internal sealed class InboundLinkCommands(ILoggerFactory logger, ICoreService githubActionsService) +{ + private readonly LinkIndexLinkChecker _linkIndexLinkChecker = new(logger); + + [SuppressMessage("Usage", "CA2254:Template should be a static expression")] + private void AssignOutputLogger() + { + var log = logger.CreateLogger(); + ConsoleApp.Log = msg => log.LogInformation(msg); + ConsoleApp.LogError = msg => log.LogError(msg); + } + + /// Validate all published cross_links in all published links.json files. + /// + [Command("validate-all")] + [ConsoleAppFilter] + [ConsoleAppFilter] + public async Task ValidateAllInboundLinks(Cancel ctx = default) + { + AssignOutputLogger(); + await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); + return await _linkIndexLinkChecker.CheckAll(collector, ctx); + } + + /// Validate a single repository against the published cross_links in all published links.json files. + /// Outbound links 'from' repository to others, defaults to {pwd}/.git + /// Outbound links 'to' repository form others + /// + [Command("validate")] + [ConsoleAppFilter] + [ConsoleAppFilter] + public async Task ValidateRepoInboundLinks(string? from = null, string? to = null, Cancel ctx = default) + { + AssignOutputLogger(); + var fs = new FileSystem(); + var root = fs.DirectoryInfo.New(Paths.Root.FullName); + if (from == null && to == null) + { + from ??= GitCheckoutInformation.Create(root, new FileSystem(), logger.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName; + if (from == null) + throw new Exception("Unable to determine repository name"); + } + await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); + return await _linkIndexLinkChecker.CheckRepository(collector, to, from, ctx); + } + + /// + /// Validate a locally published links.json file against all published links.json files in the registry + /// + /// Path to `links.json` defaults to '.artifacts/docs/html/links.json' + /// + [Command("validate-link-reference")] + [ConsoleAppFilter] + [ConsoleAppFilter] + public async Task ValidateLocalLinkReference([Argument] string? file = null, Cancel ctx = default) + { + AssignOutputLogger(); + file ??= ".artifacts/docs/html/links.json"; + var fs = new FileSystem(); + var root = fs.DirectoryInfo.New(Paths.Root.FullName); + var repository = GitCheckoutInformation.Create(root, new FileSystem(), logger.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName + ?? throw new Exception("Unable to determine repository name"); + + await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService); + return await _linkIndexLinkChecker.CheckWithLocalLinksJson(collector, repository, file, ctx); + } +} diff --git a/src/docs-builder/Program.cs b/src/docs-builder/Program.cs index ee39ab1c3..dea9f7007 100644 --- a/src/docs-builder/Program.cs +++ b/src/docs-builder/Program.cs @@ -16,5 +16,6 @@ var app = ConsoleApp.Create(); app.Add(); +app.Add("inbound-links"); await app.RunAsync(args).ConfigureAwait(false); From 3f9eb40948bb8f8cc70b87276cce591cd9f1be39 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Fri, 28 Feb 2025 19:00:14 +0100 Subject: [PATCH 37/45] Fix `environment_url` in preview deployment (#658) --- .github/workflows/preview-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 7f01c81e9..163319da4 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -176,7 +176,7 @@ jobs: if: always() && steps.deployment.outputs.result env: PR_NUMBER: ${{ github.event.pull_request.number }} - LANDING_PAGE_PATH: ${{ steps.docs-build.outputs.landing-page-path }} + LANDING_PAGE_PATH: ${{ steps.docs-build.outputs.landing-page-path || env.PATH_PREFIX }} with: script: | await github.rest.repos.createDeploymentStatus({ @@ -184,6 +184,6 @@ jobs: repo: context.repo.repo, deployment_id: ${{ steps.deployment.outputs.result }}, state: "${{ steps.docs-build.outputs.skip == 'true' && 'inactive' || (steps.s3-upload.outcome == 'success' && 'success' || 'failure') }}", - environment_url: `https://docs-v3-preview.elastic.dev${process.env.LANDING_PAGE_PATH ?? process.env.PATH_PREFIX}`, + environment_url: `https://docs-v3-preview.elastic.dev${process.env.LANDING_PAGE_PATH}`, log_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, }) From 4dcfb39901323e0b0c5f1fad3a7070c754c5f23d Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Fri, 28 Feb 2025 20:06:05 +0100 Subject: [PATCH 38/45] Adjust links and wording in primary and secondary navigation (#659) --- .../Slices/Layout/_Header.cshtml | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml index 408653e35..b722fbedd 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml @@ -23,12 +23,21 @@ Url = Model.Link("/solutions"), HtmxAttributes = GetHxAttributes(Model.Link("/solutions")), DropdownItems = [ + new PrimaryNavDropdownItemViewModel + { + IconPath = Model.Static("elasticsearch-logo-color-64px.svg"), + IconAlt = "Search logo", + Title = "Search", + Description = "Build search experiences to help users find what they need instantly.", + Url = Model.Link("/solutions/search"), + HtmxAttributes = GetHxAttributes(Model.Link("/solutions/search")) + }, new PrimaryNavDropdownItemViewModel { IconPath = Model.Static("observability-logo-color-64px.svg"), IconAlt = "Observability logo", Title = "Observability", - Description = "Unify app and infrastructure visibility to proactively resolve issues.", + Description = "Unify monitoring for apps and infrastructure.", Url = Model.Link("/solutions/observability"), HtmxAttributes = Htmx.GetHxAttributes( Model.Features, @@ -42,51 +51,42 @@ IconPath = Model.Static("security-logo-color-64px.svg"), IconAlt = "Security logo", Title = "Security", - Description = "Protect, investigate, and respond to cyber threats with AI-driven security analytics.", + Description = "Protect, investigate, and respond to cyber threats.", Url = Model.Link("/solutions/security"), HtmxAttributes = GetHxAttributes(Model.Link("/solutions/security")) - }, - new PrimaryNavDropdownItemViewModel - { - IconPath = Model.Static("elasticsearch-logo-color-64px.svg"), - IconAlt = "Search logo", - Title = "Search", - Description = "Discover a world of AI possibilities — built with the power of search.", - Url = Model.Link("/solutions/search"), - HtmxAttributes = GetHxAttributes(Model.Link("/solutions/search")) } ] }, new PrimaryNavItemViewModel { - Title = "Work with the stack", + Title = "Work with the Elastic Stack", DropdownItems = [ new PrimaryNavDropdownItemViewModel { - Title = "Manage data", - Description = "Discover a world of AI possibilities — built with the power of search.", + Title = "Manage your data", + Description = "Ingest, enrich, and manage your data.", Url = Model.Link("/manage-data"), HtmxAttributes = GetHxAttributes(Model.Link("/manage-data")) }, new PrimaryNavDropdownItemViewModel { - Title = "Explore and analyze", - Description = "Unify app and infrastructure visibility to proactively resolve issues.", + Title = "Explore and analyze your data", + Description = "Query, shape, visualize, alert, and more.", Url = Model.Link("/explore-analyze"), HtmxAttributes = GetHxAttributes(Model.Link("/explore-analyze")) }, new PrimaryNavDropdownItemViewModel { - Title = "Deploy and manage", - Description = "Unify app and infrastructure visibility to proactively resolve issues.", + Title = "Deploy and manage Elastic", + Description = "Deploy, configure, manage, and upgrade clusters and deployments.", Url = Model.Link("/deploy-manage"), HtmxAttributes = GetHxAttributes(Model.Link("/deploy-manage")) }, new PrimaryNavDropdownItemViewModel { - Title = "Manage your cloud", - Description = "Unify app and infrastructure visibility to proactively resolve issues.", + Title = "Manage your Cloud account", + Description = "Manage the settings for your Elastic Cloud account.", Url = Model.Link("/cloud-account"), HtmxAttributes = GetHxAttributes(Model.Link("/cloud-account")) }, @@ -94,9 +94,9 @@ }, new PrimaryNavItemViewModel { - Title = "Troubleshoot", - HtmxAttributes = GetHxAttributes(Model.Link("/troubleshoot")), - Url = Model.Link("/troubleshoot"), + Title = "Reference", + HtmxAttributes = GetHxAttributes(Model.Link("/reference")), + Url = Model.Link("/reference"), }, ] }; @@ -172,18 +172,18 @@
    • - What's New + Release notes
    • - - Reference + + Troubleshoot
    • - Visit Elastic.co + elastic.co From e3a5e36c207bd1bab7716778937afc4508d55a17 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Mon, 3 Mar 2025 17:55:26 +0100 Subject: [PATCH 39/45] Add mobile navigations (#662) * Add mobile navigations * Remove landing-page feature toggle --- docs/index.md | 4 + src/Elastic.Markdown/Assets/copybutton.css | 4 +- src/Elastic.Markdown/Assets/markdown/code.css | 8 +- .../Assets/markdown/dropdown.css | 4 +- src/Elastic.Markdown/Assets/markdown/list.css | 4 +- src/Elastic.Markdown/Assets/markdown/tabs.css | 6 +- .../Assets/markdown/typography.css | 2 +- src/Elastic.Markdown/Assets/styles.css | 54 +++---- src/Elastic.Markdown/Assets/theme.css | 7 +- .../Slices/Layout/_Breadcrumbs.cshtml | 57 ++++++-- .../Slices/Layout/_Header.cshtml | 132 ++++++++---------- .../Slices/Layout/_PagesNav.cshtml | 19 ++- .../Slices/Layout/_PrevNextNav.cshtml | 6 +- .../Slices/Layout/_PrimaryNav.cshtml | 114 ++++++++------- .../Layout/_PrimaryNavDropdownItem.cshtml | 6 +- .../Slices/Layout/_SecondaryNav.cshtml | 57 ++++++++ .../Slices/Layout/_TableOfContents.cshtml | 4 +- .../Slices/Layout/_TocTreeNav.cshtml | 10 +- src/Elastic.Markdown/Slices/_Layout.cshtml | 11 +- 19 files changed, 305 insertions(+), 204 deletions(-) create mode 100644 src/Elastic.Markdown/Slices/Layout/_SecondaryNav.cshtml diff --git a/docs/index.md b/docs/index.md index 9939a9b4b..7e999138f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,7 @@ +--- +navigation_title: Elastic Docs v3 +--- + # Welcome to Elastic Docs v3 Elastic Docs V3 is our next-generation documentation platform designed to improve the experience of learning, using, and contributing to Elastic products. Built on a foundation of modern authoring tools and scalable infrastructure, V3 offers faster builds, streamlined versioning, and enhanced navigation to guide users through Elastic’s complex ecosystem. diff --git a/src/Elastic.Markdown/Assets/copybutton.css b/src/Elastic.Markdown/Assets/copybutton.css index 316c769da..e55e0d0ef 100644 --- a/src/Elastic.Markdown/Assets/copybutton.css +++ b/src/Elastic.Markdown/Assets/copybutton.css @@ -17,7 +17,7 @@ button.copybtn { /* The colors that GitHub uses */ border: #1b1f2426 1px solid; /*background-color: #f6f8fa;*/ - color: var(--color-gray-400); + color: var(--color-grey-50); } button.copybtn.success { @@ -73,7 +73,7 @@ div.highlight { font-size: .8em; left: -.2em; /*background: grey;*/ - color: var(--color-gray-400); + color: var(--color-grey-50); white-space: nowrap; z-index: 2; border-radius: 2px; diff --git a/src/Elastic.Markdown/Assets/markdown/code.css b/src/Elastic.Markdown/Assets/markdown/code.css index 8256145b1..164753052 100644 --- a/src/Elastic.Markdown/Assets/markdown/code.css +++ b/src/Elastic.Markdown/Assets/markdown/code.css @@ -8,7 +8,7 @@ @apply grid; code { @apply text-sm - text-gray-300 + text-grey-30 rounded-none border-0 overflow-x-auto @@ -24,7 +24,7 @@ @apply rounded-b-sm; } code.language-apiheader { - @apply border-b-1 border-b-gray-700; + @apply border-b-1 border-b-grey-100; } } @@ -70,10 +70,10 @@ code { @apply font-mono - bg-gray-100 + bg-grey-10 rounded-xs border-1 - border-gray-300 + border-grey-20 ; font-size: 0.875em; line-height: 1.4em; diff --git a/src/Elastic.Markdown/Assets/markdown/dropdown.css b/src/Elastic.Markdown/Assets/markdown/dropdown.css index fa5592f77..9ae2fee28 100644 --- a/src/Elastic.Markdown/Assets/markdown/dropdown.css +++ b/src/Elastic.Markdown/Assets/markdown/dropdown.css @@ -1,7 +1,7 @@ @layer components { .markdown-content { .dropdown { - @apply mt-4 border-1 border-gray-300 rounded-sm shadow-xs; + @apply mt-4 border-1 border-grey-20 rounded-sm shadow-xs; .dropdown-title { @apply flex justify-between @@ -15,7 +15,7 @@ } &[open] .dropdown-title { - @apply border-b-1 border-b-gray-300; + @apply border-b-1 border-b-grey-20; svg { transform: rotate(90deg); } diff --git a/src/Elastic.Markdown/Assets/markdown/list.css b/src/Elastic.Markdown/Assets/markdown/list.css index 52e10897e..446d37c4c 100644 --- a/src/Elastic.Markdown/Assets/markdown/list.css +++ b/src/Elastic.Markdown/Assets/markdown/list.css @@ -6,12 +6,12 @@ margin-left: 1.5em; /*list-style-position: inside;*/ li::marker { - @apply font-mono text-grey-80; + @apply text-grey-80; } } ul li { - @apply pl-[.5ch]; + /*@apply pl-[.5ch];*/ } li>ul, li>ol { diff --git a/src/Elastic.Markdown/Assets/markdown/tabs.css b/src/Elastic.Markdown/Assets/markdown/tabs.css index 532a2581b..6bc7a473e 100644 --- a/src/Elastic.Markdown/Assets/markdown/tabs.css +++ b/src/Elastic.Markdown/Assets/markdown/tabs.css @@ -3,13 +3,13 @@ @apply flex flex-wrap relative overflow-hidden mt-4; .tabs-label { - @apply cursor-pointer px-6 py-2 z-20 text-ink-light flex items-center border-1 border-gray-200 -mb-[1px]; + @apply cursor-pointer px-6 py-2 z-20 text-ink-light flex items-center border-1 border-grey-20 -mb-[1px]; &:not(:nth-of-type(1)) { margin-left: -1px; } &:hover { - @apply border-b-1 border-b-black text-black bg-gray-100; + @apply border-b-1 border-b-black text-black bg-grey-10; } } @@ -18,7 +18,7 @@ } .tabs-content { - @apply w-full order-99 border-1 border-gray-200 px-6 z-0 hidden pb-6 pt-2; + @apply w-full order-99 border-1 border-grey-20 px-6 z-0 hidden pb-6 pt-2; } .tabs-input:checked+.tabs-label+.tabs-content { diff --git a/src/Elastic.Markdown/Assets/markdown/typography.css b/src/Elastic.Markdown/Assets/markdown/typography.css index 4d59c9392..1519200bc 100644 --- a/src/Elastic.Markdown/Assets/markdown/typography.css +++ b/src/Elastic.Markdown/Assets/markdown/typography.css @@ -54,6 +54,6 @@ } a { - @apply font-body text-blue-elastic hover:text-blue-800 underline; + @apply font-body text-blue-elastic hover:text-blue-elastic-100 underline; } } diff --git a/src/Elastic.Markdown/Assets/styles.css b/src/Elastic.Markdown/Assets/styles.css index 472d55bd4..fb30b216f 100644 --- a/src/Elastic.Markdown/Assets/styles.css +++ b/src/Elastic.Markdown/Assets/styles.css @@ -11,16 +11,27 @@ @import "./markdown/table.css"; @import "./markdown/definition-list.css"; -#default-search::-webkit-search-cancel-button { - padding-right: calc(var(--spacing) * 2); - -webkit-appearance: none; - height: 16px; - width: 16px; - margin-left: .4em; - background-image: url("data:image/svg+xml;utf8,"); - cursor: pointer; - background-repeat: no-repeat; -} + +:root { + --outline-size: max(2px, 0.08em); + --outline-style: auto; + --outline-color: var(--color-blue-elastic); + --outline-offset: 5; + --header-height: calc(var(--spacing) * 21); + --banner-height: calc(var(--spacing) * 9); + --offset-height: calc(var(--header-height) + var(--banner-height)); +} + +/*#default-search::-webkit-search-cancel-button {*/ +/* padding-right: calc(var(--spacing) * 2);*/ +/* -webkit-appearance: none;*/ +/* height: 16px;*/ +/* width: 16px;*/ +/* margin-left: .4em;*/ +/* background-image: url("data:image/svg+xml;utf8,");*/ +/* cursor: pointer;*/ +/* background-repeat: no-repeat;*/ +/*}*/ #pages-nav li.current { position: relative; @@ -31,7 +42,7 @@ left: -1px; width: calc(var(--spacing) * 6); height: 1px; - background-color: var(--color-gray-200); + background-color: var(--color-grey-10); } } @@ -49,7 +60,7 @@ text-blue-elastic text-nowrap font-semibold - hover:text-blue-800 + hover:text-blue-elastic-100 inline-flex justify-center items-center; @@ -72,8 +83,8 @@ .sidebar { .sidebar-nav { - @apply sticky top-21 z-30 overflow-y-auto; - max-height: calc(100vh - var(--spacing) * 22); + @apply sticky top-(--offset-height) z-30 overflow-y-auto; + max-height: calc(100vh - var(--offset-height)); scrollbar-gutter: stable; } @@ -81,11 +92,11 @@ @apply text-ink-light hover:text-black - text-sm + lg:text-sm text-wrap inline-block leading-[1.3em] - tracking-[-0.02em]; + tracking-[-0.02em] } } @@ -95,7 +106,7 @@ .applies { @apply font-sans; - border-bottom: 1px solid var(--color-gray-300); + border-bottom: 1px solid var(--color-grey-20); padding-bottom: calc(var(--spacing) * 3); .applies-to-label { @@ -113,7 +124,7 @@ font-size: 0.8em; border-radius: 0.4em; background-color: var(--color-white); - border: 1px solid var(--color-gray-300); + border: 1px solid var(--color-grey-20); } } } @@ -122,12 +133,7 @@ scroll-margin-top: calc(var(--spacing) * 26); } -:root { - --outline-size: max(2px, 0.08em); - --outline-style: auto; - --outline-color: var(--color-blue-elastic); - --outline-offset: 5; -} + :is(a, button, input, textarea, summary):focus { outline: var(--outline-size) var(--outline-style) var(--outline-color); diff --git a/src/Elastic.Markdown/Assets/theme.css b/src/Elastic.Markdown/Assets/theme.css index ac705780d..b9318cc9d 100644 --- a/src/Elastic.Markdown/Assets/theme.css +++ b/src/Elastic.Markdown/Assets/theme.css @@ -1,9 +1,10 @@ @theme { - /*--color-*: initial;*/ --font-sans: "Mier B", "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-body: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --color-*: initial; + --color-white: #FFFFFF; --color-black: #000000; @@ -13,10 +14,6 @@ --color-ink-light: #535966; --color-ink-dark: #1C1E23; - --color-gray: #E6EBF2; - --color-gray-light: #F5F7FA; - --color-gray-dark: #D4DAE5; - --color-blue-sky: #36B9FF; --color-blue-midnight: #20377D; --color-blue-developer: #101C3F; diff --git a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml index cc71473ce..bedacc541 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml @@ -1,8 +1,8 @@ @using Elastic.Markdown.Helpers @inherits RazorSlice - diff --git a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml index b722fbedd..86c6df859 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Header.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Header.cshtml @@ -101,18 +101,41 @@ ] }; } - -
      - -
      -
      -
      -
      -
      - - Elastic - +
      + +
      +
      + + Elastic + @if (Model.Features.IsPrimaryNavEnabled) { @await RenderPartialAsync(_PrimaryNav.Create(primaryNavViewModel)) @@ -121,76 +144,39 @@ {
      } - -
      +
      @if (Model.Features.IsPrimaryNavEnabled) { - + @await RenderPartialAsync(_SecondaryNav.Create(Model)) } diff --git a/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml b/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml index 71fbca700..1b5a0f457 100644 --- a/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml @@ -1,13 +1,20 @@ @inherits RazorSlice -