Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 9 additions & 4 deletions src/Elastic.Markdown/Myst/Directives/ImageBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -21,6 +22,11 @@ public class ImageBlock(DirectiveBlockParser parser, ParserContext context)
/// </summary>
public string? Alt { get; set; }

/// <summary>
/// Title text: a short description of the image
/// </summary>
public string? Title { get; set; }

/// <summary>
/// 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
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -112,5 +119,3 @@ private void ExtractImageUrl(ParserContext context)
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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();
}

Expand Down
5 changes: 3 additions & 2 deletions src/Elastic.Markdown/Slices/Directives/Image.cshtml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@using Microsoft.AspNetCore.Mvc.ModelBinding.Validation
@inherits RazorSlice<ImageViewModel>
<a class="reference internal image-reference" href="javascript:void(0)" onclick="document.getElementById('modal-@Model.UniqueImageId').style.display='flex'">
<img loading="lazy" alt="@Model.Alt" src="@Model.ImageUrl" style="@Model.Style" class="@Model.Screenshot" />
<img loading="lazy" title="@Model.Title" alt="@(Model.Alt == string.Empty ? HtmlString.Empty : new HtmlString(Model.Alt))" src="@Model.ImageUrl" style="@Model.Style" class="@Model.Screenshot" />
[CONTENT]
</a>

Expand All @@ -14,7 +15,7 @@
</a>
</span>
<a class="reference internal image-reference" href="@Model.ImageUrl" target="_blank">
<img loading="lazy" alt="@Model.Alt" src="@Model.ImageUrl" />
<img loading="lazy" title="@Model.Title" alt="@(Model.Alt == string.Empty ? HtmlString.Empty : new HtmlString(Model.Alt))" src="@Model.ImageUrl" />
</a>
</div>
</div>
3 changes: 2 additions & 1 deletion src/Elastic.Markdown/Slices/Directives/_ViewModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
40 changes: 40 additions & 0 deletions tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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\"");
}
14 changes: 14 additions & 0 deletions tests/authoring/Blocks/ImageBlocks.fs
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,17 @@ type ``image ref out of scope`` () =
[<Fact>]
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
:::
"""

[<Fact>]
let ``validate empty alt attribute`` () =
markdown |> convertsToContainingHtml """
<img loading="lazy" alt src="/img/some-image.png" style="width: 250px;">
"""
Loading