Sitecore – Image Captions in Rich-text Fields

The Problem: Sitecore does not provide an out of the box solution for adding captions to images in rich-text editor fields. This is a must have feature and something that users expect to find in Sitecore.

My Solution: I ended up adding a pipeline that parses through the HTML content held in rich-text fields and adds captions when it finds appropriate images. So the caption is added when a page is rendered at run time. This solution holds the caption text in a field that is actually on the media item itself. It’s debatable if this is the best place to hold the caption text but I like the idea of entering a caption on the Sitecore media item and having it be reused each time the image is displayed in a rich-text field.

caption_template

As you can see in the image above, I added a Caption Text field to the image templates. You can add this field directly to the JPEG and Image templates but you need to consider that the templates will be reset when upgrading to a new version of Sitecore. Your best bet is to create your own custom image templates that inherit from the default JPEG and Image templates. This topic is discussed in a supplemental blog post named Custom Image Templates in Sitecore.

Of course you could always choose a pre-existing field like the Description field to be the “caption” field. Just make sure that the field you choose exists on both the JPEG and Image templates. You can change the “caption” field by making a change to the custom config file outlined at the bottom of the post.

In a perfect world you would be able to right click on an image in a rich-text field and also be able to enter the caption as an image property but I don’t know how to do that at the moment (Any advice would be appreciated).

Let’s look at an example of the default markup that is output by Sitecore for an image in a rich-text field:

<img alt="Lorem ipsum dolor" src="/~/media/images/example_image.jpg" style="width: 175px; height: 225px; float: left;">

Now let’s look at the same example with a caption included:

<figure class="rte-image" style="float: left; width: 175px;">
    <img alt="Lorem ipsum dolor" src="/~/media/images/example_image.jpg" style="width: 175px; height: 225px; float: left;">
    <figcaption class="rte-caption">
        Example caption text
    </figcaption>
</figure>

caption_template

The solution feels a little hacky but there are lots of various scenarios to account for in the code. For instance the image can be floated left, floated right, can be linked, can have its height and width changed, etc. That’s probably why the code started to get a bit long.

Here is the pipeline code (and lots of it):

public class SetRteCaptions
{
    // The regular expression used to identify image media in the rich-text editor
    private readonly Regex _regex = new Regex("/([A-Fa-f0-9]{32,32})\\.ashx");
        
    // The field name for the caption text on the media item
    private string ParamValue { get; set; }

    // Gets the rte caption field name
    public void SetRteCaptionParams(XmlNode node)
    {
        ParamValue = XmlUtil.GetAttribute("value", node);
    }

    public void Process(RenderFieldArgs args)
    {
        if (args.FieldTypeKey != "rich text" || String.IsNullOrEmpty(ParamValue) ||
            (String.IsNullOrEmpty(args.Result.FirstPart) && String.IsNullOrEmpty(args.Result.LastPart)))
            return;

        args.Result.FirstPart = SetCaptions(args.Result.FirstPart, ParamValue);
        args.Result.LastPart = SetCaptions(args.Result.LastPart, ParamValue);
    }

    /// <summary>
    ///     Adds captions to images in the rich-text editor if the caption is entered on the media item. You can change the
    ///     field to pull the caption from in the setRteCaptions.config file.
    /// </summary>
    /// <param name="value">The data held in the RTE field</param>
    /// <param name="captionField">The name of the field to pull the caption text from. This field is on the media item itself</param>
    public string SetCaptions(string value, string captionField)
    {
        if (captionField == null) return value;
        var htmlDoc = new HtmlDocument();
        htmlDoc.LoadHtml(value);
        HtmlNodeCollection nodes = htmlDoc.DocumentNode.SelectNodes("//img[@src]");
        if (nodes == null || nodes.Count == 0) return value;

        foreach (HtmlNode node in nodes)
        {
            string src = node.GetAttributeValue("src", String.Empty);
            if (src == null) continue;
            Match match = _regex.Match(src);
            if (!match.Success) continue;
            ID guid = ID.Parse(match.Groups[1].Value);
            Item mediaItem = Context.Database.GetItem(guid);
            if (mediaItem == null) continue;
            string floatStyle = "none";
                
            string field = mediaItem[captionField];
            if (String.IsNullOrEmpty(field)) continue;

            string styleAttributes = node.GetAttributeValue("style", String.Empty);
            if (!String.IsNullOrEmpty(styleAttributes))
                floatStyle = ExtractFloat(styleAttributes);

            string styleWidth = GetImageWrapperWidth(node);  
            HtmlTextNode captionNode = htmlDoc.CreateTextNode(SetCaptionMarkup(mediaItem, captionField));
            if (node.ParentNode.OriginalName == "a")
            {
                node.ParentNode.ParentNode.InsertAfter(captionNode, node.ParentNode);
                HtmlTextNode prependNode = htmlDoc.CreateTextNode(SetImageWrapper(floatStyle, styleWidth));
                node.ParentNode.ParentNode.InsertBefore(prependNode, node.ParentNode);
            }
            else
            {
                node.ParentNode.InsertAfter(captionNode, node);
                HtmlTextNode prependNode = htmlDoc.CreateTextNode(SetImageWrapper(floatStyle, styleWidth));
                node.ParentNode.InsertBefore(prependNode, node);
            }
        }
        return htmlDoc.DocumentNode.OuterHtml;
    }

    /// <summary>
    /// Gets the width of the image wrapper.
    /// </summary>
    /// <param name="node">The node.</param>
    /// <returns></returns>
    private string GetImageWrapperWidth(HtmlNode node)
    {
        string styleAttributes = node.GetAttributeValue("style", String.Empty);
        if (!String.IsNullOrEmpty(styleAttributes))
        {
            string styleWidth  = ExtractWidth(styleAttributes);
            if (IsValidInteger(styleWidth))
                return styleWidth;
        }
        string widthAttribute = node.GetAttributeValue("width", String.Empty);
        if (!String.IsNullOrEmpty(widthAttribute))
        {
            if (IsValidInteger(widthAttribute))
                return widthAttribute;
        }
        return null;
    }

    /// <summary>
    /// Determines whether [is valid integer] [the specified width].
    /// </summary>
    /// <param name="width">The width.</param>
    /// <returns></returns>
    private static bool IsValidInteger(string width)
    {
        int n;
        return int.TryParse(width, out n);
    }

    /// <summary>
    ///     Gets the width of the image
    /// </summary>
    /// <param name="s">The markup for the origional image</param>
    private static string ExtractWidth(string s)
    {
        int startIndex = s.IndexOf("width:", StringComparison.Ordinal) + 6;
        if (startIndex == -1)
            return null;
        int endIndex = s.IndexOf("px;", startIndex, StringComparison.Ordinal);
        if (endIndex == -1)
            return null;
        if (endIndex < startIndex)
            return null;
        return s.Substring(startIndex, endIndex - startIndex);
    }

    /// <summary>
    ///     Gets the float of the image
    /// </summary>
    /// <param name="s">The markup for the origional image</param>
    private static string ExtractFloat(string s)
    {
        int startIndex = s.IndexOf("float:", StringComparison.Ordinal);
        if (startIndex == -1)
            return null;
        startIndex = startIndex + 6;
        int endIndex = s.IndexOf(";", startIndex, StringComparison.Ordinal);
        if (endIndex == -1)
            return null;
        if (endIndex < startIndex)
            return null;
        return s.Substring(startIndex, endIndex - startIndex);
    }

    /// <summary>
    ///     Sets the markup for the caption box
    /// </summary>
    /// <param name="item">The media item to apply the caption to</param>
    /// <param name="captionWidth">The width of the caption box (Optional)</param>
    /// <param name="captionField">The name of the field to pull the caption text from. This field is on the media item itself</param>
    /// <returns></returns>
    private static string SetCaptionMarkup(Item item, string captionField)
    {
        string captionText = item[captionField];
        if (string.IsNullOrEmpty(captionText)) return null;
        return "<figcaption class='rte-caption'>" + captionText + "</figcaption></figure>";
    }

    /// <summary>
    ///     Sets to markup for the wrapper that contains the image and the caption
    /// </summary>
    /// <param name="captionFloat">The float of the origional image (ex: left, none)</param>
    /// <param name="captionWidth">The width of the caption box (Optional)</param>
    private string SetImageWrapper(string captionFloat, string captionWidth = null)
    {
        int n;
        if (captionWidth == null || !(int.TryParse(captionWidth, out n)))
            return "<figure class='rte-image' style='float:" + captionFloat + ";'>";
        return "<figure class='rte-image' style='float:" + captionFloat + "; width: " + (Int32.Parse(captionWidth)) + "px;'>";
    }
}

And here is the associated config file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderField>
        <processor patch:before="processor[@type='Sitecore.Pipelines.RenderField.ExpandLinks, Sitecore.Kernel']" type="SetRteCaptions, __assembly__">
          <params hint="raw:setRteCaptionParams">
            <param name="captionFieldName" value="Caption Text" />
          </params>
        </processor>  
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Let me know what you think and what you’re currently using for image captions in rich-text fields.

Advertisement
Tagged with:
Posted in Sitecore
One comment on “Sitecore – Image Captions in Rich-text Fields
  1. Christopher Huemmer says:

    Hi there,
    great article on how to add image captions.

    We’re currently facing a similar requirement,
    unfortunately we can’t make use of “renderfield” because our project is based on MVC.

    Would you know of a workaround?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s