RickStrahl / Westwind.AspNetCore.Markdown

An ASP.NET Core Markdown support library that provides Markdown parsing, a Markdown TagHelper and Markdown Page Handler Middleware
MIT License
247 stars 30 forks source link

Images relatives to host #16

Closed diegosasw closed 4 years ago

diegosasw commented 4 years ago

You mention in the documentation that we should be aware that any markdown image with relative path (e.g: ![Image Alt here](imageCloseToMarkdown.png)) will be relative to host, not to the markdown file (e.g: the generated html would be <img src="imageCloseToMarkdown.png">) so it's likely not to render the image because the physical image will be in some inner folder such as posts/2020/01/03/.

My question is. Do you have support to allow using images relative to markdown that later on are not relative to the host?

I cannot find in Markdig whether there is a way to "override" the img rendering. If there was, we could always append a path on each href when rendering (e.g: append /posts/2020/01/03) because this path would be known as it's opening the .md stream.

Using absolute paths is very restrictive because an app could be hosted in different places, so I'm looking at how to get this working with relative paths and at the same time be able to see images rendered in the markdown (in case I edit it with github or similar)

Thanks!

PS: the link https://github.com/RickStrahl/Westwind.AspNetCore/blob/master/SampleWeb/Pages/Markdown.cshtml on the doc is broken

davidyack commented 4 years ago

I think I ran into the same thing, we are parsing from Url, and we want the images to be from the remote host not the host surfacing the content. Looks like you support that with tag helper - is there a way with parse from url to set that option?

RickStrahl commented 4 years ago

I had to look at this to refresh my memory on how this actually works :smile:

The path doesn't have to be server relative, but it's 'link relative'. Basically it behaves the way an HTML page would behave (because that's what it ends up with) with the base path in the rendered location (ie. what the address bar shows). The primary use case is .md file in a folder and you run that MD file like and HTML document with dependencies relative to that location.

If you need some other base URL because you're pulling the files from some other location like Github that's not that same folder, you can change the <base href="<otherUrl>" /> in the header of your template and effectively change where your dependencies are coming from. Or you can use the TagHelper to explicitly reference the dependencies.

I get that if you're loading the Markdown and dependencies from somewhere else you'll probably want the dependencies from somewhere else as well, but there's no way good way to generically resolve that that I can see short of using a base url. Whatever we could do with the document won't be any better than what a base url would do because at best it could be set only once.

If you're using WebClient based loading via the tag helper especially (say from a Github repo) there's no easy way to even specify a relative Url because Github won't actually serve files like images without using raw syntax.

RickStrahl commented 4 years ago

So I've added some additional logic that allows you to specify a base URL:

This allow you to control the base URL, but it has to be added into the template:

@section Headers {
    @if (string.IsNullOrEmpty(Model.BasePath))
    {
        <base href="@Model.BasePath"/>
    }
    ...
}
diegosasw commented 4 years ago

That's a nice approach!

My workaround was the following relying on Markdig. But I like more the simplicity of the base href.

public class MarkdownParser
    : IMarkdownParser
{
    public HtmlData Parse(string markdown, string assetPathPrefix = null)
    {
        if (markdown == null)
        {
            throw new ArgumentNullException(nameof(markdown));
        }

        var pipeline =
            new MarkdownPipelineBuilder()
                .UseAdvancedExtensions()
                .UseYamlFrontMatter()
                .Build();

        using var writer = new StringWriter();
        var renderer = new HtmlRenderer(writer);
        if (assetPathPrefix != null)
        {
            string LinkRewriter(string originalLink)
            {
                var isAbsolutePath = Uri.IsWellFormedUriString(originalLink, UriKind.Absolute);
                if (isAbsolutePath)
                {
                    return originalLink;
                }

                var originalLinkTrimmed = originalLink.TrimStart('/');
                var linkPathTrimmed = assetPathPrefix.TrimEnd('/');
                var pathRewritten = $"{linkPathTrimmed}/{originalLinkTrimmed}";
                return pathRewritten;
            }

            renderer.LinkRewriter = LinkRewriter;
        }
        pipeline.Setup(renderer);

        var markdownDocument = Markdown.Parse(markdown, pipeline);
        renderer.Render(markdownDocument);
        writer.Flush();
        var html = writer.ToString().Trim();
        var result = new HtmlData(html);
        return result;
    }
}

where the "base path" is passed to the parser, and tests to demonstrate this for images (or any relative link, actually):

public class Given_Markdown_With_Relative_Image_And_Link_Path_Prefix_When_Parsing
    : Given_When_Then_Test
{
    private MarkdownParser _sut;
    private string _markdown;
    private HtmlData _result;
    private HtmlData _expectedResult;
    private string _pathPrefix;

    protected override void Given()
    {
        const string imageData = "sample.png";

        _markdown = $"![ImagePath Alt]({imageData})";
        _pathPrefix = "/relativePath/foo";
        _sut = new MarkdownParser();

        var html = $"<p><img src=\"{_pathPrefix}/{imageData}\" alt=\"ImagePath Alt\" /></p>";
        _expectedResult = new HtmlData(html);
    }

    protected override void When()
    {
        _result = _sut.Parse(_markdown, _pathPrefix);
    }

    [Fact]
    public void Then_It_Should_Return_A_Paragraph_With_The_Image_Path_Prefixed()
    {
        _result.Should().BeEquivalentTo(_expectedResult);
    }
}

public class Given_Markdown_With_Relative_Image_In_Current_Folder_And_Link_Path_Prefix_With_Trailing_Slash_When_Parsing
    : Given_When_Then_Test
{
    private MarkdownParser _sut;
    private string _markdown;
    private HtmlData _result;
    private HtmlData _expectedResult;
    private string _pathPrefix;

    protected override void Given()
    {
        const string imageData = "sample.png";

        _markdown = $"![ImagePath Alt]({imageData})";
        _pathPrefix = "/relativePath/foo/";
        _sut = new MarkdownParser();

        var html = $"<p><img src=\"/relativePath/foo/sample.png\" alt=\"ImagePath Alt\" /></p>";
        _expectedResult = new HtmlData(html);
    }

    protected override void When()
    {
        _result = _sut.Parse(_markdown, _pathPrefix);
    }

    [Fact]
    public void Then_It_Should_Return_A_Paragraph_With_The_Image_Path_Prefixed()
    {
        _result.Should().BeEquivalentTo(_expectedResult);
    }
}

public class Given_Markdown_With_Relative_Image_In_Different_Folder_And_Link_Path_Prefix_With_Trailing_Slash_When_Parsing
    : Given_When_Then_Test
{
    private MarkdownParser _sut;
    private string _markdown;
    private HtmlData _result;
    private HtmlData _expectedResult;
    private string _pathPrefix;

    protected override void Given()
    {
        const string imageData = "/different/folder/sample.png";

        _markdown = $"![ImagePath Alt]({imageData})";
        _pathPrefix = "/relativePath/foo/";
        _sut = new MarkdownParser();

        var html = $"<p><img src=\"/relativePath/foo/different/folder/sample.png\" alt=\"ImagePath Alt\" /></p>";
        _expectedResult = new HtmlData(html);
    }

    protected override void When()
    {
        _result = _sut.Parse(_markdown, _pathPrefix);
    }

    [Fact]
    public void Then_It_Should_Return_A_Paragraph_With_The_Image_Path_Prefixed()
    {
        _result.Should().BeEquivalentTo(_expectedResult);
    }
}

public class Given_Markdown_With_Absolute_Image_And_Link_Path_Prefix_When_Parsing
    : Given_When_Then_Test
{
    private MarkdownParser _sut;
    private string _markdown;
    private HtmlData _result;
    private HtmlData _expectedResult;
    private string _pathPrefix;

    protected override void Given()
    {
        const string imageData = "https://foo/sample.png";

        _markdown = $"![ImagePath Alt]({imageData})";
        _pathPrefix = "/relativePath/foo";
        _sut = new MarkdownParser();

        var html = $"<p><img src=\"{imageData}\" alt=\"ImagePath Alt\" /></p>";
        _expectedResult = new HtmlData(html);
    }

    protected override void When()
    {
        _result = _sut.Parse(_markdown, _pathPrefix);
    }

    [Fact]
    public void Then_It_Should_Return_A_Paragraph_With_The_Original_Image_Path()
    {
        _result.Should().BeEquivalentTo(_expectedResult);
    }
}
RickStrahl commented 4 years ago

Yes that looks like it would work too but that's pretty heavy and requires reparsing the document. Also it's not just images that might be affected but could be other resources like relative links to other documents or script/css resources.

I'm not 100% sure, but I think the basePath may work for you especially since your code is trying to fix up every image link anyway.

diegosasw commented 4 years ago

I agree. basePath it is! Thanks a lot