DEV Community

Nozomu Ikuta
Nozomu Ikuta

Posted on

Exploring How lit-html Works: TemplateResult and SVGTemplateResult constructors (3)

In this series, How lit-html works, I will explore (not explain) internal implementation of lit-html.

In the previous post, we saw how how lit-html uses Trusted Types API to convert template strings to trusted HTML string.

In this post, I will dive into getHTML() method of TemplateResult more deeply.

Handling Comment-Like Expressions in the Template Literals

In the past post, I said that if-statements and related lines in getHTML() method of TemplateResult class are to handle comment-like strings.

Let's look into this point.

getHTML(): string {
  const l = this.strings.length - 1;
  let html = '';
  let isCommentBinding = false;

  for (let i = 0; i < l; i++) {
    const s = this.strings[i];

    const commentOpen = s.lastIndexOf('<!--');
    isCommentBinding = (commentOpen > -1 || isCommentBinding) &&
        s.indexOf('-->', commentOpen + 1) === -1;

    const attributeMatch = lastAttributeNameRegex.exec(s);
    if (attributeMatch === null) {
      html += s + (isCommentBinding ? commentMarker : nodeMarker);
    } else {
      html += s.substr(0, attributeMatch.index) + attributeMatch[1] +
          attributeMatch[2] + boundAttributeSuffix + attributeMatch[3] +
          marker;
    }
  }
  html += this.strings[l];
  return html;
}
Enter fullscreen mode Exit fullscreen mode

getHTML() method does conditional string concatenation based on the result of regular expression matching.

The definition of the regular expression (lastAttributeNameRegex) is like below.

export const lastAttributeNameRegex = /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
Enter fullscreen mode Exit fullscreen mode

Since comment in the code explains this regular expression well, you can check it to understand what the regular expression represents.

In short, the following patterns are all valid attribute expressions.

 attr=value

    attr    =    value

    attr    =    
value

 attr="value"

 attr    =   'value'

 attr="value >>>

 attr='value `<>=>>>'

 attr='<!--
Enter fullscreen mode Exit fullscreen mode

It will be difficult for most people to distinguish which part of the regular expression corresponds to which part of the actual string. To make it easier, I will show the matched parts with Named capture groups, which is a new feature of ES2018.

var string = `<div attr="<!--`
var regex = /(?<spaceBeforeName>[ \x09\x0a\x0c\x0d])(?<name>[^\0-\x1F\x7F-\x9F "'>=/]+)(?<equalSignAndValue>[ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/g
var result = regex.exec(string)

console.dir(result)
// =>
// [ 
//    ' attr="<!--',
//    ' ',
//    'attr',
//    '="<!--',
//    index: 4,
//    input: '<div attr="<!--',
//    groups: { 
//      spaceBeforeName: ' ',
//      name: 'attr', 
//      equalSignAndValue: '="<!--'
//    }
// ]

Enter fullscreen mode Exit fullscreen mode

Now they became easier to distinguish, I think.

If the string matches the pattern, then lit-html combines the expressions with all the space characters before equal sign removed and .

html += s.substr(0, attributeMatch.index) + attributeMatch[1] +
          attributeMatch[2] + boundAttributeSuffix + attributeMatch[3] +
          marker;

// is actually...

html += '<div' + ' ' + 'attr' + '="<!--' + '{{lit-3958644673182541}}'

// and is finally...

html += '<div attr="<!--{{lit-3958644673182541}}'
Enter fullscreen mode Exit fullscreen mode

What's Wrong

Now It is the problem that all the part after attr="<!--{{lit-3958644673182541}} could be parsed as comment.

When this is getTemplateElement() is called, the content of the HTML template element becomes empty because there seems no elements which have both of opening and closing tags.

This kind of string will finally throw an error when render function of the library is called (Of course I will explore this function later).

const value = 'value'
const templateResult = html`<div attr="<!--${value}>Error!</div>`

render(templateResult, document.body)
// => Uncaught TypeError: Failed to set the 'currentNode' property on 'TreeWalker': The provided value is not of type 'Node'.
Enter fullscreen mode Exit fullscreen mode

Summary

So far, I learned the following points:

  • lit-html sometimes can't create a HTML string 100% accurately.
  • It is always good to wrap attribute values with quotations to avoid the cases.

In the next post, I will explore the reparentNodes function which is used by getTemplateElement() of SVGTemplateResult class.

Top comments (0)