Skip to content
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

Q: Custom renderer for links? #706

Closed
sommmen opened this issue Mar 22, 2023 · 3 comments
Closed

Q: Custom renderer for links? #706

sommmen opened this issue Mar 22, 2023 · 3 comments
Labels

Comments

@sommmen
Copy link

sommmen commented Mar 22, 2023

Hiya,

I have a markdown string that contains links.
I'm pushing this string to Slack, but unfortunately slack has a slightly different way of formatting links:

<http://www.example.com|This message *is* a link>

https://api.slack.com/reference/surfaces/formatting#linking-urls

What i'd like to do is parse my string with markdig and then render a markdown string.
This seems very much possible with markdig - i think i need a custom MarkdownObjectRenderer?

I'm however a bit confused on where to even start, could someone give some guidance as how to write a custom render for some markdown objects?

What i'm thinking right now is to override the NormalizeRenderer, and replace the LinkInlineRenderer with my own.

@sommmen
Copy link
Author

sommmen commented Mar 22, 2023

I managed to figure it out.
Not a lot of docs, but there is a lot of code documentation and the structure is clear.

Well done with this library!

Let me know if you have some tips:

/// <summary>
/// A markdown renderer for Slack.
/// Slack supports basic markdown, but not a lot.
/// It also has some custom formats.
/// This renderer aims to output slack compatible(ish) markdown messages.
/// <see href="https://api.slack.com/reference/surfaces/formatting"/>
/// </summary>
public class SlackRenderer : RoundtripRenderer
{
    /// <inheritdoc />
    public SlackRenderer(TextWriter writer)
        : base(writer)
    {
        if (!ObjectRenderers.Replace<LinkInlineRenderer>(new SlackLinkInlineRenderer()))
            throw new InvalidOperationException("Replacement failed!");
    }
}

/// <summary>
/// <see cref="SlackRenderer"/>
/// </summary>
public class SlackLinkInlineRenderer : LinkInlineRenderer
{
    protected override void Write(RoundtripRenderer renderer, LinkInline link)
    {
        // See: https://api.slack.com/reference/surfaces/formatting#links-in-retrieved-messages
        // Sample slack link: <http://www.example.com|This message *is* a link>

        // TODO Images are not supported by slack
        //if (link.IsImage)
        //{
        //    renderer.Write('!');
        //}

        // TODO Spec: https://spec.commonmark.org/0.30/#full-reference-link
        //      Reference links are not yet supported. We could support them by storing links 
        //      We can probably see the full link in the link property, and inject that instead (so all ref links will be just inline links).


        // link text
        renderer.Write('<');
        
        if (link.Url != null)
        {
            renderer.Write(link.TriviaBeforeUrl);
            renderer.Write(link.UnescapedUrl);
            renderer.Write(link.TriviaBeforeUrl);
            renderer.Write('|');
            renderer.WriteChildren(link);
        }
        else
        {
            renderer.WriteChildren(link);
        }
        
        renderer.Write('>');
    }
}

@Atulin
Copy link

Atulin commented Mar 21, 2024

@sommmen I also found myself in need of customizing how links are rendered, and I stumbled upon this issue, but while I have no problem modifying this code to my needs, I... don't actually know how to use it, how to turn it into an extension, or even just modify the pipeline with it directly.

@sommmen
Copy link
Author

sommmen commented Mar 21, 2024

@sommmen I also found myself in need of customizing how links are rendered, and I stumbled upon this issue, but while I have no problem modifying this code to my needs, I... don't actually know how to use it, how to turn it into an extension, or even just modify the pipeline with it directly.

public static class SlackHelpers
{
    public static string GetMarkDown(string input)
    {
        var pipeline = new MarkdownPipelineBuilder()
            .EnableTrackTrivia()
            .UsePipeTables()
            .Build();

        var document = Markdown.Parse(input, pipeline);
        var writer = new StringWriter();
        var renderer = new SlackRenderer(writer);
        _ = renderer.Render(document);
        writer.Flush();
        return writer.ToString();
    }
}
/// <summary>
/// A markdown renderer for Slack.
/// Slack supports basic markdown, but not a lot.
/// It also has some custom formats.
/// This renderer aims to output slack compatible(ish) markdown messages.
/// <see href="https://api.slack.com/reference/surfaces/formatting"/>
/// </summary>
public class SlackRenderer : RoundtripRenderer
{
    /// <inheritdoc />
    public SlackRenderer(TextWriter writer)
        : base(writer)
    {
        if (!ObjectRenderers.Replace<LinkInlineRenderer>(new SlackLinkInlineRenderer()))
            throw new InvalidOperationException("Replacement failed!");
        ObjectRenderers.Add(new SlackPipeTableRenderer());
        if (!ObjectRenderers.Replace<HeadingRenderer>(new SlackHeadingRenderer()))
            throw new InvalidOperationException("Replacement failed!");
        if (!ObjectRenderers.Replace<EmphasisInlineRenderer>(new SlackEmphasisInlineRenderer()))
            throw new InvalidOperationException("Replacement failed!");
    }

    /// <summary>
    /// <see cref="SlackRenderer"/>
    /// </summary>
    public class SlackLinkInlineRenderer : LinkInlineRenderer
    {
        protected override void Write(RoundtripRenderer renderer, LinkInline link)
        {
            // See: https://api.slack.com/reference/surfaces/formatting#links-in-retrieved-messages
            // Sample slack link: <http://www.example.com|This message *is* a link>

            // TODO Images are not supported by slack
            //if (link.IsImage)
            //{
            //    renderer.Write('!');
            //}

            // NOTE Spec: https://spec.commonmark.org/0.30/#full-reference-link
            //      Reference links are not yet supported. We could support them by storing links 
            //      We can probably see the full link in the link property, and inject that instead (so all ref links will be just inline links).

            // link text
            renderer.Write('<');

            if (link.Url != null)
            {
                renderer.Write(link.TriviaBeforeUrl);
                renderer.Write(link.UnescapedUrl);
                renderer.Write(link.TriviaBeforeUrl);
                renderer.Write('|');
                renderer.WriteChildren(link);
            }
            else
            {
                renderer.WriteChildren(link);
            }

            renderer.Write('>');
        }
    }

    private class SlackPipeTableRenderer : MarkdownObjectRenderer<SlackRenderer, Table>
    {
        #region Overrides of MarkdownObjectRenderer<SlackRenderer,Table>

        /// <inheritdoc />
        protected override void Write(SlackRenderer renderer, Table table)
        {
            renderer.Write(table.TriviaBefore);

            // Table is wrapped in code block for it to render nice(ish) in slack
            renderer.WriteLine("```");

            var p = new int[table.ColumnDefinitions.Count];
            foreach (var row in table.Cast<TableRow>())
            {
                for (var i = 0; i < table.ColumnDefinitions.Count; i++)
                {
                    if (i < row.Count)
                    {
                        var cell = (TableCell)row[i];
                        if (p[i] < cell.Span.Length)
                            p[i] = cell.Span.Length;
                    }
                }
            }

            foreach (var row in table.Cast<TableRow>())
            {
                foreach (var (def, cell, l) in table.ColumnDefinitions.Zip(row.Cast<TableCell>(), p))
                {
                    var padding = Math.Max(0, l - cell.Span.Length);
                    Debug.Assert(def.Alignment != TableColumnAlign.Center, "NOT YET SUPPORTED!");

                    if (def.Alignment == TableColumnAlign.Right && padding > 0)
                        renderer.Write(new string(' ', padding));
                    renderer.Write(cell.TriviaBefore);
                    renderer.Write(cell);
                    renderer.Write(cell.TriviaAfter);
                    if (def.Alignment == TableColumnAlign.Left && padding > 0)
                        renderer.Write(new string(' ', padding));

                    if (cell != row.LastChild)
                        renderer.Write('\t');
                }

                renderer.WriteLine();
            }

            renderer.WriteLine("```");

            renderer.Write(table.TriviaAfter);
        }

        #endregion
    }

    private class SlackHeadingRenderer : MarkdownObjectRenderer<SlackRenderer, HeadingBlock>
    {
        protected override void Write(SlackRenderer renderer, HeadingBlock obj)
        {
            // Slack does not support headings (as markdown text)
            // So we simply bold any headings...

            if (obj.IsSetext)
            {
                renderer.RenderLinesBefore(obj);
                var headingChar = obj.Level == 1 ? '=' : '-';
                var line = new string(headingChar, obj.HeaderCharCount);

                renderer.WriteLeafInline(obj);
                renderer.WriteLine(obj.SetextNewline);
                renderer.Write(obj.TriviaBefore);
                renderer.Write('*');
                renderer.Write(line);
                renderer.Write('*');
                renderer.WriteLine(obj.NewLine);
                renderer.Write(obj.TriviaAfter);

                renderer.RenderLinesAfter(obj);
            }
            else
            {
                renderer.RenderLinesBefore(obj);

                renderer.Write(obj.TriviaBefore);
                renderer.Write(obj.TriviaAfterAtxHeaderChar);
                renderer.Write('*');
                renderer.WriteLeafInline(obj);
                renderer.Write('*');
                renderer.Write(obj.TriviaAfter);
                renderer.WriteLine(obj.NewLine);

                renderer.RenderLinesAfter(obj);
            }
        }
    }

    private class SlackEmphasisInlineRenderer : MarkdownObjectRenderer<SlackRenderer, EmphasisInline>
    {
        protected override void Write(SlackRenderer renderer, EmphasisInline obj)
        {
            // See: https://commonmark.org/help/tutorial/02-emphasis.html
            // See: https://api.slack.com/reference/surfaces/formatting#visual-styles

            var emphasisText = obj.DelimiterCount == 1 ? '_' : '*';

            renderer.Write(emphasisText);
            renderer.WriteChildren(obj);
            renderer.Write(emphasisText);
        }
    }

}

Good luck :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants