docs: function diffElements(vNode, elem) {
    var output = '';
    // Diff tagnames and save for closing
    var tagDiff = textDiff(vNode.tagName.toLowerCase(), elem.tagName.toLowerCase());
    tagDiff = '<span class="TemplateString_tag">' + tagDiff + '</span>';
    output += chars.open;
    output += tagDiff;
  
    // Check declared property values vs DOM values
    // This bypasses properties that are not managed by VirtualNode
    _.each(vNode.properties, function(value, key) {
      if (key === 'attributes' || key === 'namespace') {
        return;
      }
      if (key.toLowerCase() === 'contenteditable' && value.toLowerCase() === 'inherit') {
        return;
      }
      var propValue = value;
      if (isHook(propValue)) {
        if (propValue.value) {
          propValue = propValue.value;
        } else {
          // If this is a hook with no value, ignore as it's functional
          return;
        }
      }
      var propName = utils.propertiesToTransform[key] ? utils.propertiesToTransform[key] : key;
  
      var domValue = elem[key];
      if (_.isObject(domValue)) {
        domValue = elem.getAttribute(key);
      }
      if (propName === 'href' || propName === 'src') {
        // the href property displays the fully resolved URL when read, so fall back to attribute value
        domValue = elem.getAttribute(propName);
      } else if (propName === 'style') {
        // Style attributes are tricky because they can validly contain whitespace and be out of order
        propValue = _.filter(propValue.cssText.split(';'), _.identity).sort().join(';').replace(/\s/g, '');
        if (typeof domValue === 'string') {
          domValue = _.filter(domValue.split(';'), _.identity).sort().join(';').replace(/\s/g, '');
        }
      }
  
      var vAttr = ' <span class="TemplateString_attrName">' +
            propName + '</span>=<span class="TemplateString_attrValue">' +
            chars.quote + propValue + chars.quote + '</span>';
      var dAttr = ' <span class="TemplateString_attrName">' +
            propName + '</span>=<span class="TemplateString_attrValue">' +
            chars.quote + textDiff(String(propValue), String(domValue)) + chars.quote + '</span>';
      // If the property is a boolean, any non-"false" value of the template is fine
      output += elem[key] === true && propValue.toString() !== 'false' ? vAttr : dAttr;
    });
  
    // Check declared attribute values vs DOM values
    // This bypasses attributes that are not managed by VirtualNode
    _.each(vNode.properties.attributes, function(value, key) {
      if (key.toLowerCase() === 'contenteditable' && value.toLowerCase() === 'inherit') {
        return;
      }
      if (key === 'namespace') {
        return;
      }
      var propValue = value;
      if (isHook(propValue)) {
        if (propValue.value) {
          propValue = propValue.value;
        } else {
          // If this is a hook with no value, ignore as it's functional
          return;
        }
      }
      var vAttr = ' <span class="TemplateString_attrName">' + key + '</span>=<span class="TemplateString_attrValue">' + chars.quote + propValue + chars.quote + '</span>';
      var eAttr = ' <span class="TemplateString_attrName">' + key + '</span>=<span class="TemplateString_attrValue">' + chars.quote + elem.getAttribute(key) + chars.quote + '</span>';
      output += textDiff(vAttr, eAttr);
    });
    output += chars.close;
  
    // Iterate over the larger number of children
    var numChildren = Math.max(vNode.children.length, elem.childNodes.length);
  
    if (vNode.tagName.toLowerCase() !== 'noscript') {
      for (var i = 0; i < numChildren; i++) {
        output += recursiveDiff(vNode.children[i], elem.childNodes[i]);
      }
    }
  
    // For diffing purposes all tags are <tag></tag>, even self-closing tags
    output += chars.open + '/' + tagDiff + chars.close;
    return output;
  }