From 0cf5d9c01e570a11c0318eef12471827892a65ab Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 19 Mar 2025 17:03:03 +0100 Subject: [PATCH 1/6] Add ability to set description meta tag --- src/Elastic.Markdown/Assets/main.ts | 2 + .../DocumentationGenerator.cs | 2 +- .../Myst/FrontMatter/FrontMatterParser.cs | 3 + .../Slices/DescriptionGenerator.cs | 119 ++++++++++++++++++ src/Elastic.Markdown/Slices/HtmlWriter.cs | 3 +- src/Elastic.Markdown/Slices/Index.cshtml | 1 + .../Slices/Layout/_Head.cshtml | 4 +- src/Elastic.Markdown/Slices/_ViewModels.cs | 2 + 8 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 src/Elastic.Markdown/Slices/DescriptionGenerator.cs diff --git a/src/Elastic.Markdown/Assets/main.ts b/src/Elastic.Markdown/Assets/main.ts index 71b5ad68d..1a9229463 100644 --- a/src/Elastic.Markdown/Assets/main.ts +++ b/src/Elastic.Markdown/Assets/main.ts @@ -1,6 +1,8 @@ // @ts-nocheck import "htmx.org" import "htmx-ext-preload" +import "htmx-ext-head-support" + import {initTocNav} from "./toc-nav"; import {initHighlight} from "./hljs"; import {initTabs} from "./tabs"; diff --git a/src/Elastic.Markdown/DocumentationGenerator.cs b/src/Elastic.Markdown/DocumentationGenerator.cs index eb1756ebc..1adac0602 100644 --- a/src/Elastic.Markdown/DocumentationGenerator.cs +++ b/src/Elastic.Markdown/DocumentationGenerator.cs @@ -52,7 +52,7 @@ public DocumentationGenerator( DocumentationSet = docSet; Context = docSet.Build; Resolver = docSet.LinkResolver; - HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem); + HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem, new DescriptionGenerator()); _documentationFileExporter = documentationExporter ?? new DocumentationFileExporter(docSet.Build.ReadFileSystem, _writeFileSystem, HtmlWriter, conversionCollector); diff --git a/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs index bc05217bb..65457a013 100644 --- a/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs +++ b/src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs @@ -20,6 +20,9 @@ public class YamlFrontMatter [YamlMember(Alias = "title")] public string? Title { get; set; } + [YamlMember(Alias = "description")] + public string? Description { get; set; } + [YamlMember(Alias = "navigation_title")] public string? NavigationTitle { get; set; } diff --git a/src/Elastic.Markdown/Slices/DescriptionGenerator.cs b/src/Elastic.Markdown/Slices/DescriptionGenerator.cs new file mode 100644 index 000000000..f581aa305 --- /dev/null +++ b/src/Elastic.Markdown/Slices/DescriptionGenerator.cs @@ -0,0 +1,119 @@ +// 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.Text; +using Elastic.Markdown.Myst.Substitution; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; + +namespace Elastic.Markdown.Slices; + +public interface IDescriptionGenerator +{ + string GenerateDescription(MarkdownDocument document); +} + + +public class DescriptionGenerator : IDescriptionGenerator +{ + private const int MaxLength = 150; + + public string GenerateDescription(MarkdownDocument? document) + { + if (document == null) + return string.Empty; + + var description = new StringBuilder(); + foreach (var block in document.TakeWhile(_ => description.Length < MaxLength)) + { + switch (block) + { + case ParagraphBlock paragraph: + { + ProcessParagraph(paragraph, description); + break; + } + case ListBlock listBlock: + { + ProcessListBlock(listBlock, description); + break; + } + } + } + var result = description.ToString().TrimEnd('.').Trim(); + // It can happen that the last parsed block is longer, hence the result is longer than maxLength + // Hence we need to trim it to maxLength + if (result.Length > MaxLength) + result = string.Concat(result.AsSpan(0, MaxLength), "..."); + + return result; + } + + private static void ProcessParagraph(ParagraphBlock paragraph, StringBuilder description) + { + if (paragraph.Inline == null) + return; + + var paragraphText = GetInlineText(paragraph.Inline); + if (string.IsNullOrEmpty(paragraphText)) + return; + + _ = description.Append(paragraphText); + _ = description.Append(' '); + } + + private static void ProcessListBlock(ListBlock listBlock, StringBuilder description) + { + foreach (var item in listBlock) + { + if (item is not ListItemBlock listItem) + continue; + + foreach (var listItemBlock in listItem) + { + if (listItemBlock is not ParagraphBlock listItemParagraph || listItemParagraph.Inline == null) + continue; + + var paragraphText = GetInlineText(listItemParagraph.Inline); + _ = description.Append(paragraphText); + var lastChar = paragraphText[^1]; + if (lastChar is not '.' and not ',' and not '!' and not '?') + _ = description.Append(listItem == listBlock.LastChild ? ". " : ", "); + } + } + } + + private static string GetInlineText(ContainerInline inline) + { + var builder = new StringBuilder(); + foreach (var item in inline) + { + switch (item) + { + case SubstitutionLeaf subs: + _ = builder.Append(subs.Replacement); + break; + case LiteralInline literal: + _ = builder.Append(literal.Content.ToString()); + break; + case EmphasisInline emphasis: + _ = builder.Append(GetInlineText(emphasis)); + break; + case LinkInline link: + _ = builder.Append(GetInlineText(link)); + break; + case CodeInline code: + _ = builder.Append(code.Content); + break; + case LineBreakInline: + _ = builder.Append(' '); + break; + case ContainerInline container: + _ = builder.Append(GetInlineText(container)); + break; + } + } + return builder.ToString(); + } +} diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index 7fde86872..6a894d8e1 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -12,7 +12,7 @@ namespace Elastic.Markdown.Slices; -public class HtmlWriter(DocumentationSet documentationSet, IFileSystem writeFileSystem) +public class HtmlWriter(DocumentationSet documentationSet, IFileSystem writeFileSystem, IDescriptionGenerator descriptionGenerator) { private DocumentationSet DocumentationSet { get; } = documentationSet; private StaticFileContentHashProvider StaticFileContentHashProvider { get; } = new(new EmbeddedOrPhysicalFileProvider(documentationSet.Build)); @@ -90,6 +90,7 @@ public async Task RenderLayout(MarkdownFile markdown, MarkdownDocument d var slice = Index.Create(new IndexViewModel { Title = markdown.Title ?? "[TITLE NOT SET]", + Description = markdown.YamlFrontMatter?.Description ?? descriptionGenerator.GenerateDescription(document), TitleRaw = markdown.TitleRaw ?? "[TITLE NOT SET]", MarkdownHtml = html, PageTocItems = [.. markdown.PageTableOfContent.Values], diff --git a/src/Elastic.Markdown/Slices/Index.cshtml b/src/Elastic.Markdown/Slices/Index.cshtml index 6a181cc02..552b3b482 100644 --- a/src/Elastic.Markdown/Slices/Index.cshtml +++ b/src/Elastic.Markdown/Slices/Index.cshtml @@ -5,6 +5,7 @@ public LayoutViewModel LayoutModel => new() { Title = $"Elastic Documentation: {Model.Title}", + Description = Model.Description, PageTocItems = Model.PageTocItems.Where(i => i is { Level: 2 or 3 }).ToList(), Tree = Model.Tree, CurrentDocument = Model.CurrentDocument, diff --git a/src/Elastic.Markdown/Slices/Layout/_Head.cshtml b/src/Elastic.Markdown/Slices/Layout/_Head.cshtml index 92f2585d9..dba4aa483 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Head.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Head.cshtml @@ -2,12 +2,14 @@ @using Elastic.Markdown.Helpers @Model.Title + @foreach (var fontFile in await FontPreloader.GetFontUrisAsync(@Model.UrlPathPrefix)) { } - + + @await RenderPartialAsync(_Favicon.Create()) diff --git a/src/Elastic.Markdown/Slices/_ViewModels.cs b/src/Elastic.Markdown/Slices/_ViewModels.cs index 68c40a0c9..1d25fe3fa 100644 --- a/src/Elastic.Markdown/Slices/_ViewModels.cs +++ b/src/Elastic.Markdown/Slices/_ViewModels.cs @@ -11,6 +11,7 @@ namespace Elastic.Markdown.Slices; public class IndexViewModel { public required string Title { get; init; } + public required string Description { get; init; } public required string TitleRaw { get; init; } public required string MarkdownHtml { get; init; } public required DocumentationGroup Tree { get; init; } @@ -38,6 +39,7 @@ public class LayoutViewModel public static string CurrentNavigationId { get; } = Guid.NewGuid().ToString("N")[..8]; public string Title { get; set; } = "Elastic Documentation"; public string RawTitle { get; set; } = "Elastic Documentation"; + public required string Description { get; init; } public required IReadOnlyCollection PageTocItems { get; init; } public required DocumentationGroup Tree { get; init; } public string[] ParentIds => [.. CurrentDocument.YieldParentGroups()]; From 99a285df7eac7cdae3145472baaeb6bcaf210e9c Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 19 Mar 2025 17:27:48 +0100 Subject: [PATCH 2/6] WIP --- .../Slices/DescriptionGenerator.cs | 27 ++++++++++++++----- .../Slices/Layout/_Head.cshtml | 1 - 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Elastic.Markdown/Slices/DescriptionGenerator.cs b/src/Elastic.Markdown/Slices/DescriptionGenerator.cs index f581aa305..35d27bba0 100644 --- a/src/Elastic.Markdown/Slices/DescriptionGenerator.cs +++ b/src/Elastic.Markdown/Slices/DescriptionGenerator.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.Text; +using Elastic.Markdown.Myst.Directives; using Elastic.Markdown.Myst.Substitution; using Markdig.Syntax; using Markdig.Syntax.Inlines; @@ -19,16 +20,18 @@ public class DescriptionGenerator : IDescriptionGenerator { private const int MaxLength = 150; - public string GenerateDescription(MarkdownDocument? document) + public string GenerateDescription(MarkdownDocument document) { - if (document == null) - return string.Empty; - var description = new StringBuilder(); foreach (var block in document.TakeWhile(_ => description.Length < MaxLength)) { switch (block) { + case IncludeBlock include: + { + ProcessIncludeBlock(include, description); + break; + } case ParagraphBlock paragraph: { ProcessParagraph(paragraph, description); @@ -41,15 +44,25 @@ public string GenerateDescription(MarkdownDocument? document) } } } - var result = description.ToString().TrimEnd('.').Trim(); + + var result = description.ToString().TrimEnd('.'); // It can happen that the last parsed block is longer, hence the result is longer than maxLength - // Hence we need to trim it to maxLength + // Hence we need to shorten it. In this case it will be shorted to until the next space after `MaxLength` if (result.Length > MaxLength) - result = string.Concat(result.AsSpan(0, MaxLength), "..."); + { + var endIndex = result.IndexOf(' ', MaxLength - 1); + if (endIndex == -1) + endIndex = MaxLength; + result = string.Concat(result.AsSpan(0, endIndex + 1).Trim(), "..."); + } return result; } +#pragma warning disable IDE0060 + private static void ProcessIncludeBlock(IncludeBlock include, StringBuilder description) => Console.WriteLine("Not implemented: ProcessIncludeBlock"); +#pragma warning restore IDE0060 + private static void ProcessParagraph(ParagraphBlock paragraph, StringBuilder description) { if (paragraph.Inline == null) diff --git a/src/Elastic.Markdown/Slices/Layout/_Head.cshtml b/src/Elastic.Markdown/Slices/Layout/_Head.cshtml index dba4aa483..dacf637a8 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Head.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Head.cshtml @@ -9,7 +9,6 @@ } - @await RenderPartialAsync(_Favicon.Create()) From 1f619098daff40ac053c937bfc6d5e6fc0d78bae Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 19 Mar 2025 21:59:16 +0100 Subject: [PATCH 3/6] Cleanup --- src/Elastic.Markdown/Slices/DescriptionGenerator.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Elastic.Markdown/Slices/DescriptionGenerator.cs b/src/Elastic.Markdown/Slices/DescriptionGenerator.cs index 35d27bba0..4170d1073 100644 --- a/src/Elastic.Markdown/Slices/DescriptionGenerator.cs +++ b/src/Elastic.Markdown/Slices/DescriptionGenerator.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information using System.Text; -using Elastic.Markdown.Myst.Directives; using Elastic.Markdown.Myst.Substitution; using Markdig.Syntax; using Markdig.Syntax.Inlines; @@ -25,13 +24,10 @@ public string GenerateDescription(MarkdownDocument document) var description = new StringBuilder(); foreach (var block in document.TakeWhile(_ => description.Length < MaxLength)) { + // TODO: Add support for IncludeBlock + // This is needed when the first block is an IncludeBlock. switch (block) { - case IncludeBlock include: - { - ProcessIncludeBlock(include, description); - break; - } case ParagraphBlock paragraph: { ProcessParagraph(paragraph, description); @@ -59,10 +55,6 @@ public string GenerateDescription(MarkdownDocument document) return result; } -#pragma warning disable IDE0060 - private static void ProcessIncludeBlock(IncludeBlock include, StringBuilder description) => Console.WriteLine("Not implemented: ProcessIncludeBlock"); -#pragma warning restore IDE0060 - private static void ProcessParagraph(ParagraphBlock paragraph, StringBuilder description) { if (paragraph.Inline == null) From d7e6ffd5072e9a37397925e5f18e9ab04830cefe Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 19 Mar 2025 22:01:27 +0100 Subject: [PATCH 4/6] Revert change to stylesheet --- src/Elastic.Markdown/Slices/Layout/_Head.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Elastic.Markdown/Slices/Layout/_Head.cshtml b/src/Elastic.Markdown/Slices/Layout/_Head.cshtml index dacf637a8..18f4a4c44 100644 --- a/src/Elastic.Markdown/Slices/Layout/_Head.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_Head.cshtml @@ -7,7 +7,7 @@ { } - + @await RenderPartialAsync(_Favicon.Create()) From 06396055ac935df3f72cd823227516177035d11f Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 19 Mar 2025 22:21:48 +0100 Subject: [PATCH 5/6] Fix --- src/Elastic.Markdown/Slices/DescriptionGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Elastic.Markdown/Slices/DescriptionGenerator.cs b/src/Elastic.Markdown/Slices/DescriptionGenerator.cs index 4170d1073..35b740381 100644 --- a/src/Elastic.Markdown/Slices/DescriptionGenerator.cs +++ b/src/Elastic.Markdown/Slices/DescriptionGenerator.cs @@ -41,7 +41,7 @@ public string GenerateDescription(MarkdownDocument document) } } - var result = description.ToString().TrimEnd('.'); + var result = description.ToString(); // It can happen that the last parsed block is longer, hence the result is longer than maxLength // Hence we need to shorten it. In this case it will be shorted to until the next space after `MaxLength` if (result.Length > MaxLength) @@ -49,7 +49,7 @@ public string GenerateDescription(MarkdownDocument document) var endIndex = result.IndexOf(' ', MaxLength - 1); if (endIndex == -1) endIndex = MaxLength; - result = string.Concat(result.AsSpan(0, endIndex + 1).Trim(), "..."); + result = string.Concat(result.AsSpan(0, endIndex + 1).Trim().TrimEnd('.'), "..."); } return result; From 4b0a2ef14fabda2b348e169caab4f9cbb2a42eff Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 19 Mar 2025 22:57:42 +0100 Subject: [PATCH 6/6] Fix merge conflicts --- src/Elastic.Markdown/Slices/HtmlWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index b5ca3b3d1..416899f9e 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -63,7 +63,7 @@ private NavigationViewModel CreateNavigationModel(INavigation navigation) } } -public class HtmlWriter(DocumentationSet documentationSet, IFileSystem writeFileSystem, IDescriptionGenerator descriptionGenerator, INavigationHtmlWriter? navigationHtmlWriter = null) +public class HtmlWriter(DocumentationSet documentationSet, IFileSystem writeFileSystem, IDescriptionGenerator descriptionGenerator, INavigationHtmlWriter? navigationHtmlWriter = null) { private DocumentationSet DocumentationSet { get; } = documentationSet; public INavigationHtmlWriter NavigationHtmlWriter { get; } = navigationHtmlWriter ?? new IsolatedBuildNavigationHtmlWriter(documentationSet);