How Would You Parse This Object Representing an Annotated String To HTML? Stuck

Viewed 57

This is the desired string rendered

Suppose you have an Array of Objects representing different attributes of the string:

[{
    "isBold": false,
    "isCode": true,
    "isItalic": false,
    "isStrikethrough": false,
    "range": [
        42,
        8
    ]
},
{
    "isBold": false,
    "isCode": false,
    "isItalic": true,
    "isStrikethrough": false,
    "range": [
        19,
        10
    ]
},
{
    "isBold": false,
    "isCode": false,
    "isItalic": false,
    "isStrikethrough": true,
    "range": [
        46,
        11
    ]
},
{
    "isBold": true,
    "isCode": false,
    "isItalic": false,
    "isStrikethrough": false,
    "range": [
        27,
        12
    ],
    "nested": [
        {
            "isBold": false,
            "isCode": true,
            "isItalic": false,
            "isStrikethrough": false,
            "range": [
                28,
                7
            ]
        }
    ]
},
{
    "highlightColor": "#fff100 #fff432",
    "isBold": false,
    "isCode": false,
    "isItalic": false,
    "isStrikethrough": false,
    "range": [
        1,
        15
    ]
},
{
    "isBold": false,
    "isCode": false,
    "isItalic": false,
    "isStrikethrough": false,
    "linkURL": "www.google.com",
    "range": [
        5,
        29
    ],
    "nested": [
        {
            "isBold": false,
            "isCode": false,
            "isItalic": true,
            "isStrikethrough": false,
            "range": [
                19,
                10
            ]
        }
    ]
}]

Any suggestions on how you would go about rendering something like this?

The nested ranges and different attributes has me completely stuck... I tried going through the string char by char and collecting which attributes belong to each char – I'm close, but I'm not rendering links correctly and the code attributes get split up instead of wrapping their entire text:

Render of my attempt to parse

function renderTextAttributes(content, textAttributes)
  let attributes = []
  let currentAttributes, start, end
  for (let i = 0; i < content.length; i++) {
    if (i === content.length - 1) {
      attributes.push({
        attributes: currentAttributes,
        content:
          currentAttributes.length > 0
            ? `<span class="${currentAttributes.join(' ')}">${content.slice(
                Math.max(0, start)
              )}</span>`
            : content.slice(Math.max(0, start)),
        end,
        start
      })
    } else if (currentAttributes) {
      let charAttributes = []
      for (const attribute of textAttributes) {
        if (
          i >= attribute.range[0] &&
          i <= attribute.range[0] + attribute.range[1]
        ) {
          charAttributes = [
            ...new Set([...charAttributes, ...findAttributes(attribute)])
          ]
        }
      }
      if (
        JSON.stringify(charAttributes) === JSON.stringify(currentAttributes)
      ) {
        end = i
      } else {
        attributes.push({
          attributes: currentAttributes,
          content:
            currentAttributes.length > 0
              ? `<span class="${currentAttributes.join(
                  ' '
                )}">${content.substring(start, end)}</span>`
              : content.substring(start, end),
          end,
          start
        })
        currentAttributes = null
        i--
      }
    } else {
      currentAttributes = []
      start = i
      end = i + 1
      for (const attribute of textAttributes) {
        if (
          i >= attribute.range[0] &&
          i <= attribute.range[0] + attribute.range[1]
        ) {
          currentAttributes = [
            ...new Set([...currentAttributes, ...findAttributes(attribute)])
          ]
        }
      }
    }
  }

  let newContent = ''
  for (const attribute of attributes) {
    newContent += attribute.content
  }

  return newContent
}
1 Answers

I did something that may be what you are looking for.

**(update) Sometimes when inserting the tags with function replaceWithTags we get some closing tags (e.g.: </code> between <del> tag) in the middle of the string and that mess up the final HTML, the string result may be ok, but when printing it on the DOM because HTML doesn't throw errors, instead, it tries to add the missing/closing tags, so, to solve that, I'm adding a validation where it checks if we have a single closing tag and we add the current opening tag right after the found single closing tag, that way if we have for example:

for Strikethrough case:

'<del> be </code>rendere</del>'

we will add a <del> tag after </code> like this:

<del> be </code><del>rendere</del>

as you can see it is still not a valid HTML code, but the browser will take care of it, adding the missing code:

Final HTML screenshot

let attributes = [
    {
        "isBold": false,
        "isCode": true,
        "isItalic": false,
        "isStrikethrough": false,
        "range": [
            42,
            8
        ]
    },
    {
        "isBold": false,
        "isCode": false,
        "isItalic": true,
        "isStrikethrough": false,
        "range": [
            19,
            10
        ]
    },
    {
        "isBold": false,
        "isCode": false,
        "isItalic": false,
        "isStrikethrough": true,
        "range": [
            46,
            11
        ]
    },
    {
        "isBold": true,
        "isCode": false,
        "isItalic": false,
        "isStrikethrough": false,
        "range": [
            27,
            12
        ],
        "nested": [
            {
                "isBold": false,
                "isCode": true,
                "isItalic": false,
                "isStrikethrough": false,
                "range": [
                    28,
                    7
                ]
            }
        ]
    },
    {
        "highlightColor": "#fff100 #fff432",
        "isBold": false,
        "isCode": false,
        "isItalic": false,
        "isStrikethrough": false,
        "range": [
            1,
            15
        ]
    },
    {
        "isBold": false,
        "isCode": false,
        "isItalic": false,
        "isStrikethrough": false,
        "linkURL": "www.google.com",
        "range": [
            5,
            29
        ],
        "nested": [
            {
                "isBold": false,
                "isCode": false,
                "isItalic": true,
                "isStrikethrough": false,
                "range": [
                    19,
                    10
                ]
            }
        ]
    }
];


const replaceWithTags = (content, ar, start, end, tag, attr) => {

    const startTag = `<${tag}${attr ? ' ' + attr : ''}>`;
    
    let str = content.join('');
    
    let match = str.match(/<\/.*?>/g);
    
    if (match && match.length === 1) {
        let idx = content.findIndex(x => x.match(/<\/.*?>/g))
        content[idx] = content[idx] + startTag;
    }

    ar[start] = startTag +  content.slice(0, 1);

    ar[end] = content.slice(content.length - 1, content.length) + `</${tag}>`;

    ar = [...ar.slice(0, start + 1), 
          ...content.slice(1, content.length -  1), 
          ...ar.slice(end, ar.length)];

    return ar;
}

const tags = {
    isCode: (content, ar, start, end) => {
        return replaceWithTags(content, ar, start, end, 'code');
    },
    isBold: (content, ar, start, end)=> {
        return replaceWithTags(content, ar, start, end, 'b');
    },
    isStrikethrough: (content, ar, start, end) => {
        return replaceWithTags(content, ar, start, end, 'del');
    },
    isItalic: (content, ar, start, end) => {
        return replaceWithTags(content, ar, start, end, 'span', 'style="font-style: italic;"');
    },
    linkURL: (content, ar, start, end, link) => {
        return replaceWithTags(content, ar, start, end, 'a', `href="${link}"`);
    },
    highlightColor: (content, ar, start, end, color) => {
        return replaceWithTags(content, ar, start, end, 'span', `style="background: ${color};"`);
    },
};

const renderTextAttributes = (content, textAttributes) => {
    let arr = content.split('');
    let checlAttrs = (attrs) => {
        for (let itm of attrs) {
            let start = itm.range[0];
            let end = itm.range[1];
            const slice = arr.slice(start, (start + (end)));
    
           for (let prop in itm) {
    
                if (typeof itm[prop] === 'boolean' && itm[prop]) {

                    arr = tags[prop](slice, arr, start, (start + (end - 1)));

                } else if (prop === 'linkURL') {

                    arr = tags[prop](slice, arr, start, (start + (end - 1)), itm[prop]);

                } else if (prop === 'highlightColor') {

                    arr = tags[prop](slice, arr, start, (start + (end - 1)), itm[prop].split(' ')[1]);

                } else if (prop === 'nested') {

                    arr = checlAttrs(itm[prop]);

                }
            }
     
        }
        return arr;
    }
    arr = checlAttrs(textAttributes);
    return arr.join('');
};

const text = 'String with different attributes and a link to be rendered.';
document.write(renderTextAttributes(text, attributes));
console.log(renderTextAttributes(text, attributes));

Related