Q: Custom renderer for links? #598

Closed
opened 2026-01-29 14:40:43 +00:00 by claunia · 3 comments
Owner

Originally created by @sommmen on GitHub (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.

Originally created by @sommmen on GitHub (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.
claunia added the question label 2026-01-29 14:40:43 +00:00
Author
Owner

@sommmen commented on GitHub (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('>');
    }
}
@sommmen commented on GitHub (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: ``` csharp /// <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('>'); } } ```
Author
Owner

@Atulin commented on GitHub (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.

@Atulin commented on GitHub (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.
Author
Owner

@sommmen commented on GitHub (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 :)

@sommmen commented on GitHub (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. ``` csharp 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(); } } ``` ``` csharp /// <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 in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/markdig#598