Tallan Blog

Tallan’s Experts Share Their Knowledge on Technology, Trends and Solutions to Business Challenges

Getting Started Processing and Converting the Quill.js Delta Format

There are many WYSIWYG (What You See Is What You Get) editors out there just waiting for you to drop them into your website, but one reason to choose Quill.js is for its ability to represent its contents as a JSON (JavaScript Object Notation) object.  Doing so allows you, the developer, to process those contents easily and convert them to another format. Also, it’s free, which is always a good thing.

For this, we will be focusing on some of the basics of how Quill.js expresses its contents in the Delta format and how you might go about processing those contents to fit your needs.  The examples below begin after the JSON string is converted to a C# object. For the features that are being covered here (bold, underline, italic, font color, and numbered/bulleted lists) the class structure might look something like the following:

public class Delta {
     public DeltaOp[] Ops { get; set; }
}

public class DeltaOp
{
     public string Insert { get; set; }
     public DeltaAttributes Attributes { get; set; }
}

public class DeltaAttributes
{
     public bool Bold { get; set; }
     public bool Underline { get; set; }
     public bool Italic { get; set; }
     public int Indent { get; set; }
     public ListEnum? List { get; set; }
     public string Color { get; set; }
}

public enum ListEnum
{
     Ordered,
     Bullet
}
Bold, Italic, Underline, Colors

Probably the most straight-forward thing to process are attributes such as Bold, Italic, Underline, or Font Color. Here is an example of how this might look using the Quill.js editor:

“I am just a single sentence with bolded, italicized, and underlined text which could be any color you like, or it could even be all three.”

Here is what the JSON looks like when we get the contents of the editor.  As you can see, each change in format creates a new op, which specifies the text being inserted and the attributes of that text.

{"ops":[
{"insert":"I am just a single sentence with "},
{"attributes":{"bold":true},"insert":"bolded, "},
{"attributes":{"italic":true},"insert":"italicized"},
{"insert":", and "},
{"attributes":{"underline":true},"insert":"underlined"},
{"insert":" text which could be "},
{"attributes":{"color":"red"},"insert":"a"},
{"attributes":{"color":"#ed7d31"},"insert":"n"},
{"attributes":{"color":"#70ad47"},"insert":"y"},
{"insert":" "},
{"attributes":{"color":"#2f5597"},"insert":"c"},
{"attributes":{"color":"red"},"insert":"o"},
{"attributes":{"color":"#ffc000"},"insert":"l"},
{"attributes":{"color":"#00b0f0"},"insert":"o"},
{"attributes":{"color":"#00b050"},"insert":"r "},
{"insert":"you like, or it could even be "},
{"attributes":{"underline":true,"italic":true,"bold":true},"insert":"all three"},
{"insert":".\n"}]}

So now all that you have to do to process this text is iterate through all the ops and generate your content.  If you wanted to convert this to XML, your code could be written like this:

        public string ConvertDeltaToXml(DeltaOp[] ops)
        {
            var xmlDocument = new XmlDocument();
            var root = xmlDocument.CreateElement("root");
            var paragraph = xmlDocument.CreateElement("paragraph");

            xmlDocument.AppendChild(root);

            foreach (var op in ops)
            {
                var texts = xmlDocument.CreateElement("texts");
                paragraph.AppendChild(texts);

                var textProperties = xmlDocument.CreateElement("textProperties");
                texts.AppendChild(textProperties);

                if (!string.IsNullOrWhiteSpace(op.Attributes?.Color))
                {
                    var textColor = xmlDocument.CreateElement("textColor");
                    var opColor = ColorTranslator.FromHtml(op.Attributes.Color);
                    textColor.SetAttribute("value", ToHex(opColor));
                    textProperties.AppendChild(textColor);
                }

                if (op.Attributes?.Bold ?? false)
                {
                    var bold = xmlDocument.CreateElement("bold");
                    textProperties.AppendChild(bold);
                }

                if (op.Attributes?.Italic ?? false)
                {
                    var italic = xmlDocument.CreateElement("italic");
                    textProperties.AppendChild(italic);
                }

                if (op.Attributes?.Underline ?? false)
                {
                    var underline = xmlDocument.CreateElement("underline");
                    textProperties.AppendChild(underline);
                }

                var text = xmlDocument.CreateElement("text");
                text.InnerText = op.Insert;
                texts.AppendChild(text);
            }

            root.AppendChild(paragraph);
            return xmlDocument.InnerXml;
        }

For the next example, suppose we now only care about creating multiple paragraphs. In this case, we need to pay attention to the ‘\n’ character when it shows up, but we need to be careful because depending on how the ops are split, this ‘\n’ could show up in different places.

“Here is the first paragraph and it’s pretty good.
Here is the second paragraph and it is even better.
Here is the third paragraph which is not as good as the first two.”

{"ops":[
{"insert":"Here is the first paragraph and it’s pretty good.\nHere is the "},
{"attributes":{"bold":true},"insert":"second "},
{"insert":"paragraph and it is even "},
{"attributes":{"bold":true},"insert":"better."},
{"insert":"\nHere is the third paragraph which is not as good as the first two.\n"}]}
        public string ConvertDeltaToXml(DeltaOp[] ops)
        {
            var xmlDocument = new XmlDocument();
            var root = xmlDocument.CreateElement("root");
            var paragraph = xmlDocument.CreateElement("paragraph");

            xmlDocument.AppendChild(root);

            foreach (var op in ops)
            {
                bool isNewParagraph = false;

                foreach (var splitOp in op.Insert.Split('\n'))
                {
                    if (isNewParagraph)
                    {
                        root.AppendChild(paragraph);
                        paragraph = xmlDocument.CreateElement("paragraph");
                    }

                    if (!string.IsNullOrEmpty(splitOp))
                    {
                        var texts = xmlDocument.CreateElement("texts");
                        paragraph.AppendChild(texts);

                        var text = xmlDocument.CreateElement("text");
                        text.InnerText = splitOp;
                        texts.AppendChild(text);
                    }

                    isNewParagraph = true;
                }
            }

            root.AppendChild(paragraph);
            return xmlDocument.InnerXml;
        }

If you want to handle lists the JSON looks a little bit different. Essentially when processing these ops, you will not know if your current op is part of a list until looking at the next op. So, assuming you want to process from top to bottom, this means you will have to defer writing to your new format until you reach the next op. You may also notice that the text above the first bullet and the first bullet are both a part of the same op despite one being in the list and the other being outside the list. This is why it is still very important to pay attention to the ‘\n’ character.

Here is a bullet list,

  • Bullet one
  • Bullet two
  • Bullet three

and here is a numbered list

1. The first number

a. The letter a

i. The roman numeral i

b. The letter b

2. The second number

3. The third number

{"ops":[
{"insert":"Here is a bullet list,\nBullet one"},
{"attributes":{"list":"bullet"},"insert":"\n"},
{"insert":"Bullet two"},
{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"},
{"insert":"Bullet three"},
{"attributes":{"list":"bullet"},"insert":"\n"},
{"insert":"and here is an "},
{"attributes":{"underline":true},"insert":"numbered list"},
{"insert":"\nThe first number"},
{"attributes":{"list":"ordered"},"insert":"\n"},
{"insert":"The letter a"},
{"attributes":{"indent":1,"list":"ordered"},"insert":"\n"},
{"insert":"The roman numeral i"},
{"attributes":{"indent":2,"list":"ordered"},"insert":"\n"},
{"insert":"The letter b"},
{"attributes":{"indent":1,"list":"ordered"},"insert":"\n"},
{"insert":"The second number"},
{"attributes":{"list":"ordered"},"insert":"\n"},
{"insert":"The third number"},
{"attributes":{"list":"ordered"},"insert":"\n"}]}
public string ConvertDeltaToXml(DeltaOp[] ops)
{
var xmlDocument = new XmlDocument();
var root = xmlDocument.CreateElement("root");
var paragraph = xmlDocument.CreateElement("paragraph");

xmlDocument.AppendChild(root);

foreach (var op in ops)
{
bool isNewParagraph = false;
bool isList = op.Attributes?.List == ListEnum.Bullet ||
op.Attributes?.List == ListEnum.Ordered;
bool isBreak = !isList && op.Insert == "\n";

foreach (var splitOp in op.Insert.Split('\n'))
{
if (isNewParagraph)
{
                        if (isList)
                        {
                            var properties = xmlDocument.CreateElement("properties");
                            paragraph.PrependChild(properties);

                            var paragraphType = xmlDocument.CreateElement("style");
                            paragraphType.SetAttribute("value", "List");
                            properties.AppendChild(paragraphType);

                            var style = xmlDocument.CreateElement("style");
                            properties.AppendChild(style);

                            var indent = xmlDocument.CreateElement("indent");
                            indent.SetAttribute("value", op.Attributes.Indent.ToString());
                            style.AppendChild(indent);

                            var listStyle = xmlDocument.CreateElement("listStyle");
                            if (op.Attributes.List == ListEnum.Bullet)
                            {
                                listStyle.SetAttribute("value", "bullet");
                            }

                            if (op.Attributes.List == ListEnum.Ordered)
                            {
                                listStyle.SetAttribute("value", "ordered");
                            }
                            style.AppendChild(listStyle);
                        }

                        root.AppendChild(paragraph);
                        paragraph = xmlDocument.CreateElement("paragraph");
                    }

                    if (!isList && !string.IsNullOrEmpty(splitOp))
                    {
                        var texts = xmlDocument.CreateElement("texts");
                        paragraph.AppendChild(texts);

                        var textProperties = xmlDocument.CreateElement("textProperties");
                        texts.AppendChild(textProperties);

                        var text = xmlDocument.CreateElement("text");
                        text.InnerText = splitOp;
                        texts.AppendChild(text);
                    }

                    isNewParagraph = true;
                }
            }

            root.AppendChild(paragraph);
            return xmlDocument.InnerXml;
        }

This could be taken even further to cover more of the features offered in the Quill.js editor, but hopefully this demonstrates that converting from the Delta format to some other format is possible as long as you have a good understanding of what the JSON represents.


Learn more about Tallan or see us in person at one of our many Events!

Share this post:

No comments

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

\\\