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 - @Model.Alt + @(Model.Alt == string.Empty ? HtmlString.Empty : new HtmlString(Model.Alt)) [CONTENT] @@ -14,7 +15,7 @@ - @Model.Alt + @(Model.Alt == string.Empty ? HtmlString.Empty : new HtmlString(Model.Alt)) 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 + +![{{hello-world}}](_static/img/observability.png) +""" +) +{ + + [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 + +![Observability](_static/img/observability.png "{{hello-world}}") +""" +) +{ + + [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 """ + + """