Skip to content

Add validation to image/figure directives #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Elastic.Markdown/Myst/Directives/DirectiveBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,8 @@ protected bool PropBool(params string[] keys)
protected void EmitError(ParserContext context, string message) =>
context.EmitError(Line + 1, 1, Directive.Length + 4 , message);

protected void EmitWarning(ParserContext context, string message) =>
context.EmitWarning(Line + 1, 1, Directive.Length + 4 , message);


}
12 changes: 6 additions & 6 deletions src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,14 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo

private void WriteImage(HtmlRenderer renderer, ImageBlock block)
{
var imageUrl = block.ImageUrl.StartsWith("/_static") || block.ImageUrl.StartsWith("_static")
var imageUrl =
block.ImageUrl != null &&
(block.ImageUrl.StartsWith("/_static") || block.ImageUrl.StartsWith("_static"))
? $"{block.Build.UrlPathPrefix}/{block.ImageUrl.TrimStart('/')}"
: block.ImageUrl;
var slice = Image.Create(new ImageViewModel
{
Classes = block.Classes,
CrossReferenceName = block.CrossReferenceName,
Label = block.Label,
Align = block.Align,
Alt = block.Alt,
Height = block.Height,
Expand All @@ -99,13 +100,12 @@ private void WriteImage(HtmlRenderer renderer, ImageBlock block)

private void WriteFigure(HtmlRenderer renderer, ImageBlock block)
{
var imageUrl = block.ImageUrl.StartsWith("/_static") || block.ImageUrl.StartsWith("_static")
var imageUrl = block.ImageUrl != null && (block.ImageUrl.StartsWith("/_static") || block.ImageUrl.StartsWith("_static"))
? $"{block.Build.UrlPathPrefix}/{block.ImageUrl.TrimStart('/')}"
: block.ImageUrl;
var slice = Slices.Directives.Figure.Create(new ImageViewModel
{
Classes = block.Classes,
CrossReferenceName = block.CrossReferenceName,
Label = block.Label,
Align = block.Align,
Alt = block.Alt,
Height = block.Height,
Expand Down
62 changes: 46 additions & 16 deletions src/Elastic.Markdown/Myst/Directives/ImageBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,29 +46,59 @@ public class ImageBlock(DirectiveBlockParser parser, Dictionary<string, string>
/// </summary>
public string? Target { get; set; }

/// <summary>
/// A space-separated list of CSS classes to add to the image.
/// </summary>
public string? Classes { get; protected set; }

/// <summary>
/// A reference target for the admonition (see cross-referencing).
/// </summary>
public string? CrossReferenceName { get; private set; }
public string? Label { get; private set; }

public string? ImageUrl { get; private set; }

public bool Found { get; private set; }

public string ImageUrl { get; private set; } = default!;

public override void FinalizeAndValidate(ParserContext context)
{
ImageUrl = Arguments ?? string.Empty; //todo validate
Classes = Properties.GetValueOrDefault("class");
CrossReferenceName = Properties.GetValueOrDefault("name");
Alt = Properties.GetValueOrDefault("alt");
Height = Properties.GetValueOrDefault("height");
Width = Properties.GetValueOrDefault("width");
Scale = Properties.GetValueOrDefault("scale");
Align = Properties.GetValueOrDefault("align");
Target = Properties.GetValueOrDefault("target");
Label = Prop("label", "name");
Alt = Prop("alt");
Align = Prop("align");

Height = Prop("height", "h");
Width = Prop("width", "w");

Scale = Prop("scale");
Target = Prop("target");

ExtractImageUrl(context);

}

private void ExtractImageUrl(ParserContext context)
{
var imageUrl = Arguments;
if (string.IsNullOrWhiteSpace(imageUrl))
{
EmitError(context , $"{Directive} requires an argument.");
return;
}

if (Uri.TryCreate(imageUrl, UriKind.Absolute, out var uri) && uri.Scheme.StartsWith("http"))
{
EmitWarning(context, $"{Directive} is using an external URI: {uri} ");
Found = true;
ImageUrl = imageUrl;
return;
}

var includeFrom = context.Path.Directory!.FullName;
if (imageUrl.StartsWith('/'))
includeFrom = context.Parser.SourcePath.FullName;

ImageUrl = imageUrl;
var imagePath = Path.Combine(includeFrom, imageUrl.TrimStart('/'));
if (context.Build.ReadFileSystem.File.Exists(imageUrl))
Found = true;
else
EmitError(context, $"`{imageUrl}` does not exist. resolved to `{imagePath}");
}
}

Expand Down
10 changes: 6 additions & 4 deletions src/Elastic.Markdown/Myst/Directives/IncludeBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@ public class IncludeBlock(DirectiveBlockParser parser, Dictionary<string, string
//https://mystmd.org/guide/directives#directive-include
public override void FinalizeAndValidate(ParserContext context)
{
var includePath = Arguments; //todo validate

Literal |= PropBool("literal");
Language = Prop("lang", "language", "code");
Caption = Prop("caption");
Label = Prop("label");

ExtractInclusionPath(context);
}

private void ExtractInclusionPath(ParserContext context)
{
var includePath = Arguments;
if (string.IsNullOrWhiteSpace(includePath))
{
context.EmitError(Line, Column, $"```{{{Directive}}}".Length , "include requires an argument.");
Expand All @@ -55,8 +59,6 @@ public override void FinalizeAndValidate(ParserContext context)
Found = true;
else
EmitError(context, $"`{IncludePath}` does not exist.");


}
}

Expand Down
4 changes: 2 additions & 2 deletions src/Elastic.Markdown/Slices/Directives/Figure.cshtml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
@inherits RazorSlice<ImageViewModel>
<figure class="align-default" id="@Model.CrossReferenceName">
<figure class="align-default" id="@Model.Label">
<a class="reference internal image-reference" href="@Model.ImageUrl"><img alt="@Model.Alt" class="align-center" src="@Model.ImageUrl" style="@Model.Style">
</a>
<figcaption>
<p>[CONTENT]<a class="headerlink" href="@Model.CrossReferenceName" title="Link to this image">¶</a></p>
<p>[CONTENT]<a class="headerlink" href="@Model.Label" title="Link to this image">¶</a></p>
</figcaption>
</figure>
4 changes: 1 addition & 3 deletions src/Elastic.Markdown/Slices/Directives/_ViewModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// 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.Directives;

namespace Elastic.Markdown.Slices.Directives;

Expand Down Expand Up @@ -45,8 +44,7 @@ public class IncludeViewModel

public class ImageViewModel
{
public required string? CrossReferenceName { get; init; }
public required string? Classes { get; init; }
public required string? Label { get; init; }
public required string? Align { get; init; }
public required string? Alt { get; init; }
public required string? Height { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ protected DirectiveTest(ITestOutputHelper output, [LanguageInjection("markdown")
});

var file = FileSystem.FileInfo.New("docs/source/index.md");
var root = FileSystem.DirectoryInfo.New(Paths.Root.FullName);
var root = file.Directory!;
Collector = new TestDiagnosticsCollector(logger);
var context = new BuildContext
{
Expand Down
26 changes: 22 additions & 4 deletions tests/Elastic.Markdown.Tests/Directives/ImageTests.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
// 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 Elastic.Markdown.Diagnostics;
using Elastic.Markdown.Myst.Directives;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;

namespace Elastic.Markdown.Tests.Directives;

public class ImageBlockTests(ITestOutputHelper output) : DirectiveTest<ImageBlock>(output,
"""
```{image} /_static/img/observability.png
```{image} img/observability.png
:alt: Elasticsearch
:width: 250px
```
"""
)
{
public override Task InitializeAsync()
{
FileSystem.AddFile(@"img/observability.png", "");
return base.InitializeAsync();
}

[Fact]
public void ParsesBlock() => Block.Should().NotBeNull();

Expand All @@ -25,7 +32,14 @@ public void ParsesBreakPoint()
{
Block!.Alt.Should().Be("Elasticsearch");
Block!.Width.Should().Be("250px");
Block!.ImageUrl.Should().Be("/_static/img/observability.png");
Block!.ImageUrl.Should().Be("img/observability.png");
}

[Fact]
public void ImageIsFoundSoNoErrorIsEmitted()
{
Block!.Found.Should().BeTrue();
Collector.Diagnostics.Count.Should().Be(0);
}
}

Expand All @@ -45,7 +59,11 @@ Relaxing at the beach 🏝 🌊 😎
public void ParsesBlock() => Block.Should().NotBeNull();

[Fact]
public void ParsesBreakPoint()
public void WarnsOnExternalUri()
{
Block!.Found.Should().BeTrue();

Collector.Diagnostics.Should().HaveCount(1)
.And.OnlyContain(d => d.Severity == Severity.Warning);
}
}