Skip to content
9 changes: 9 additions & 0 deletions src/Elastic.Markdown/Exporters/LlmMarkdownExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Products;
using Elastic.Markdown.Helpers;
using Elastic.Markdown.Myst.Renderers.LlmMarkdown;
using Markdig.Syntax;

namespace Elastic.Markdown.Exporters;
Expand Down Expand Up @@ -155,6 +156,14 @@ private string CreateLlmContentWithMetadata(MarkdownExportFileContext context, s
_ = metadata.AppendLine($" - {item}");
}

// Add applies_to information from frontmatter
if (sourceFile.YamlFrontMatter?.AppliesTo is not null)
{
var appliesToText = LlmAppliesToHelper.RenderApplicableTo(sourceFile.YamlFrontMatter.AppliesTo, context.BuildContext);
if (!string.IsNullOrEmpty(appliesToText))
_ = metadata.AppendLine($"applies_to: {appliesToText}");
}

_ = metadata.AppendLine("---");
_ = metadata.AppendLine();
_ = metadata.AppendLine($"# {sourceFile.Title}");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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.Documentation;
using Elastic.Documentation.AppliesTo;
using Elastic.Documentation.Configuration;
using Elastic.Markdown.Myst.Components;

namespace Elastic.Markdown.Myst.Renderers.LlmMarkdown;

/// <summary>
/// Helper class to render ApplicableTo information in LLM-friendly text format
/// </summary>
public static class LlmAppliesToHelper
{
/// <summary>
/// Converts ApplicableTo to a readable text format for LLM consumption
/// </summary>
public static string RenderApplicableTo(ApplicableTo? appliesTo, IDocumentationConfigurationContext buildContext)
{
if (appliesTo is null || appliesTo == ApplicableTo.All)
return string.Empty;

var viewModel = new ApplicableToViewModel
{
AppliesTo = appliesTo,
Inline = false,
ShowTooltip = false,
VersionsConfig = buildContext.VersionsConfiguration
};

var items = viewModel.GetApplicabilityItems();
if (items.Count == 0)
return string.Empty;

var itemList = new List<string>();

foreach (var item in items)
{
var text = BuildApplicabilityText(item);
if (!string.IsNullOrEmpty(text))
itemList.Add(text);
}

if (itemList.Count == 0)
return string.Empty;

return string.Join(", ", itemList);
}

private static string BuildApplicabilityText(ApplicabilityItem item)
{
// For LLM output, use the shorter Key name for better readability
var parts = new List<string> { item.Key };

// For LLM output, show the actual applicability information directly
var applicability = item.Applicability;

// Add lifecycle if it's not GA
if (applicability.Lifecycle != ProductLifecycle.GenerallyAvailable)
parts.Add(applicability.GetLifeCycleName());

// Add version information if present
if (applicability.Version is not null and not AllVersionsSpec)
{
var versionText = FormatVersion(applicability.Version);
if (!string.IsNullOrEmpty(versionText))
parts.Add(versionText);
}

return string.Join(" ", parts);
}

private static string FormatVersion(VersionSpec versionSpec)
{
var min = versionSpec.Min;
var max = versionSpec.Max;
var showMinPatch = versionSpec.ShowMinPatch;
var showMaxPatch = versionSpec.ShowMaxPatch;

static string FormatSemVersion(SemVersion v, bool showPatch) =>
showPatch ? $"{v.Major}.{v.Minor}.{v.Patch}" : $"{v.Major}.{v.Minor}";

return versionSpec.Kind switch
{
VersionSpecKind.GreaterThanOrEqual => $"{FormatSemVersion(min, showMinPatch)}+",
VersionSpecKind.Range when max is not null => $"{FormatSemVersion(min, showMinPatch)}-{FormatSemVersion(max, showMaxPatch)}",
VersionSpecKind.Exact => FormatSemVersion(min, showMinPatch),
_ => string.Empty
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,18 @@ protected override void Write(LlmMarkdownRenderer renderer, DirectiveBlock obj)
switch (obj)
{
case IBlockAppliesTo appliesBlock when !string.IsNullOrEmpty(appliesBlock.AppliesToDefinition):
renderer.Writer.Write($" applies-to=\"{appliesBlock.AppliesToDefinition}\"");
// Check if the block has a parsed AppliesTo object (e.g., AdmonitionBlock)
var appliesToText = obj switch
{
AdmonitionBlock admonition when admonition.AppliesTo is not null =>
LlmAppliesToHelper.RenderApplicableTo(admonition.AppliesTo, renderer.BuildContext),
_ => null
};
// Fallback to raw definition if parsing didn't work or returned empty
appliesToText ??= appliesBlock.AppliesToDefinition;

if (!string.IsNullOrEmpty(appliesToText))
renderer.Writer.Write($" applies-to=\"{appliesToText}\"");
break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using Elastic.Markdown.Myst.InlineParsers.Substitution;
using Elastic.Markdown.Myst.Roles;
using Elastic.Markdown.Myst.Roles.AppliesTo;
using Elastic.Markdown.Myst.Roles.Kbd;
using Markdig.Renderers;
using Markdig.Syntax.Inlines;
Expand Down Expand Up @@ -101,7 +102,13 @@ protected override void Write(LlmMarkdownRenderer renderer, RoleLeaf obj)
renderer.Writer.Write(output);
break;
}
// TODO: Add support for applies_to role
case AppliesToRole appliesTo:
{
var text = LlmAppliesToHelper.RenderApplicableTo(appliesTo.AppliesTo, renderer.BuildContext);
if (!string.IsNullOrEmpty(text))
renderer.Writer.Write($"[{text}]");
break;
}
default:
{
new LlmCodeInlineRenderer().Write(renderer, obj);
Expand Down
4 changes: 2 additions & 2 deletions tests/authoring/LlmMarkdown/LlmMarkdownOutput.fs
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ This is an inline {applies_to}`stack: preview 9.1` element.
"""

[<Fact>]
let ``converts to plain text with optional comment`` () =
let ``converts to readable text`` () =
markdown |> convertsToNewLLM """
This is an inline `stack: preview 9.1` element.
This is an inline [Stack Preview 9.1+] element.
"""

type ``admonition directive`` () =
Expand Down