diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs
index 7f06fd39b..66f01d151 100644
--- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs
+++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs
@@ -91,7 +91,8 @@ private static void WriteImage(HtmlRenderer renderer, ImageBlock block)
{
Label = block.Label,
Align = block.Align,
- Alt = block.Alt,
+ Alt = block.Alt ?? string.Empty,
+ Title = block.Title,
Height = block.Height,
Scale = block.Scale,
Target = block.Target,
@@ -128,7 +129,8 @@ private static void WriteFigure(HtmlRenderer renderer, ImageBlock block)
{
Label = block.Label,
Align = block.Align,
- Alt = block.Alt,
+ Alt = block.Alt ?? string.Empty,
+ Title = block.Title,
Height = block.Height,
Scale = block.Scale,
Target = block.Target,
diff --git a/src/Elastic.Markdown/Myst/Directives/ImageBlock.cs b/src/Elastic.Markdown/Myst/Directives/ImageBlock.cs
index 4c454ab0f..352960aeb 100644
--- a/src/Elastic.Markdown/Myst/Directives/ImageBlock.cs
+++ b/src/Elastic.Markdown/Myst/Directives/ImageBlock.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information
using Elastic.Markdown.Diagnostics;
+using Elastic.Markdown.Helpers;
using Elastic.Markdown.IO;
using Elastic.Markdown.Myst.InlineParsers;
@@ -21,6 +22,11 @@ public class ImageBlock(DirectiveBlockParser parser, ParserContext context)
///
public string? Alt { get; set; }
+ ///
+ /// Title text: a short description of the image
+ ///
+ public string? Title { get; set; }
+
///
/// The desired height of the image. Used to reserve space or scale the image vertically. When the “scale” option
/// is also specified, they are combined. For example, a height of 200px and a scale of 50 is equivalent to
@@ -65,9 +71,10 @@ public class ImageBlock(DirectiveBlockParser parser, ParserContext context)
public override void FinalizeAndValidate(ParserContext context)
{
Label = Prop("label", "name");
- Alt = Prop("alt");
- Align = Prop("align");
+ Alt = Prop("alt")?.ReplaceSubstitutions(context) ?? string.Empty;
+ Title = Prop("title")?.ReplaceSubstitutions(context);
+ Align = Prop("align");
Height = Prop("height", "h");
Width = Prop("width", "w");
@@ -112,5 +119,3 @@ private void ExtractImageUrl(ParserContext context)
}
}
}
-
-
diff --git a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs
index 4cc09e85a..59231fc9d 100644
--- a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs
+++ b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs
@@ -63,38 +63,41 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
ValidateAndProcessLink(link, processor, context);
- ParseStylingInstructions(link);
+ ParseStylingInstructions(link, context);
return match;
}
- private static void ParseStylingInstructions(LinkInline link)
+ private static void ParseStylingInstructions(LinkInline link, ParserContext context)
{
if (!link.IsImage)
return;
- if (string.IsNullOrWhiteSpace(link.Title) || link.Title.IndexOf('=') < 0)
- return;
+ var attributes = link.GetAttributes();
+ var title = link.Title;
- var matches = LinkRegexExtensions.MatchTitleStylingInstructions().Match(link.Title);
- if (!matches.Success)
+ if (string.IsNullOrEmpty(title))
return;
- var width = matches.Groups["width"].Value;
- if (!width.EndsWith('%'))
- width += "px";
- var height = matches.Groups["height"].Value;
- if (string.IsNullOrEmpty(height))
- height = width;
- else if (!height.EndsWith('%'))
- height += "px";
- var title = link.Title[..matches.Index];
-
- link.Title = title;
- var attributes = link.GetAttributes();
- attributes.AddProperty("width", width);
- attributes.AddProperty("height", height);
+ var matches = LinkRegexExtensions.MatchTitleStylingInstructions().Match(title);
+ if (matches.Success)
+ {
+ var width = matches.Groups["width"].Value;
+ if (!width.EndsWith('%'))
+ width += "px";
+ var height = matches.Groups["height"].Value;
+ if (string.IsNullOrEmpty(height))
+ height = width;
+ else if (!height.EndsWith('%'))
+ height += "px";
+
+ attributes.AddProperty("width", width);
+ attributes.AddProperty("height", height);
+
+ title = title[..matches.Index];
+ }
+ link.Title = title?.ReplaceSubstitutions(context);
}
private static bool IsInCommentBlock(LinkInline link) =>
@@ -183,7 +186,7 @@ private static void ProcessCrossLink(LinkInline link, InlineProcessor processor,
s => processor.EmitError(link, s),
s => processor.EmitWarning(link, s),
uri, out var resolvedUri)
- )
+ )
link.Url = resolvedUri.ToString();
}
diff --git a/src/Elastic.Markdown/Slices/Directives/Image.cshtml b/src/Elastic.Markdown/Slices/Directives/Image.cshtml
index 88aa1212c..3ffc31854 100644
--- a/src/Elastic.Markdown/Slices/Directives/Image.cshtml
+++ b/src/Elastic.Markdown/Slices/Directives/Image.cshtml
@@ -1,6 +1,7 @@
+@using Microsoft.AspNetCore.Mvc.ModelBinding.Validation
@inherits RazorSlice
-
+
[CONTENT]
@@ -14,7 +15,7 @@
-
+
diff --git a/src/Elastic.Markdown/Slices/Directives/_ViewModels.cs b/src/Elastic.Markdown/Slices/Directives/_ViewModels.cs
index bf33ad95c..f9b8ed0b6 100644
--- a/src/Elastic.Markdown/Slices/Directives/_ViewModels.cs
+++ b/src/Elastic.Markdown/Slices/Directives/_ViewModels.cs
@@ -49,7 +49,8 @@ public class ImageViewModel
{
public required string? Label { get; init; }
public required string? Align { get; init; }
- public required string? Alt { get; init; }
+ public required string Alt { get; init; }
+ public required string? Title { get; init; }
public required string? Height { get; init; }
public required string? Scale { get; init; }
public required string? Target { get; init; }
diff --git a/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs b/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs
index 947a58ae0..fdc43ac77 100644
--- a/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs
+++ b/tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs
@@ -165,3 +165,43 @@ public void OnlySeesGlobalVariable() =>
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
}
+
+public class ReplaceInImageAlt(ITestOutputHelper output) : InlineTest(output,
+"""
+---
+sub:
+ hello-world: Hello World
+---
+
+# Testing ReplaceInImageAlt
+
+
+"""
+)
+{
+
+ [Fact]
+ public void OnlySeesGlobalVariable() =>
+ Html.Should().NotContain("alt=\"{{hello-world}}\"")
+ .And.Contain("alt=\"Hello World\"");
+}
+
+public class ReplaceInImageTitle(ITestOutputHelper output) : InlineTest(output,
+"""
+---
+sub:
+ hello-world: Hello World
+---
+
+# Testing ReplaceInImageTitle
+
+
+"""
+)
+{
+
+ [Fact]
+ public void OnlySeesGlobalVariable() =>
+ Html.Should().NotContain("title=\"{{hello-world}}\"")
+ .And.Contain("title=\"Hello World\"");
+}
diff --git a/tests/authoring/Blocks/ImageBlocks.fs b/tests/authoring/Blocks/ImageBlocks.fs
index ad492f68a..7231f8d5a 100644
--- a/tests/authoring/Blocks/ImageBlocks.fs
+++ b/tests/authoring/Blocks/ImageBlocks.fs
@@ -79,3 +79,17 @@ type ``image ref out of scope`` () =
[]
let ``emits an error image reference is outside of documentation scope`` () =
docs |> hasError "./img/observability.png` does not exist. resolved to"
+
+type ``empty alt attribute`` () =
+ static let markdown = Setup.Markdown """
+:::{image} img/some-image.png
+:alt:
+:width: 250px
+:::
+"""
+
+ []
+ let ``validate empty alt attribute`` () =
+ markdown |> convertsToContainingHtml """
+
+ """