function numberLines(node, opt_startLineNum, isPreformatted, opt_numberLines, opt_calloutModifiedLines) {
    var nocode = /(?:^|\s)nocode(?:\s|$)/;
    var lineBreak = /\r\n?|\n/;
    var prefixes = [
      { prefix: "*", className: "linemodified", },
      { prefix: "-", className: "linedeleted", },
      { prefix: "+", className: "lineadded", },
    ];
    opt_numberLines = (opt_numberLines == undefined) ? true : opt_numberLines;

    var startsWith = function(str, prefix) {
      return str.length >= prefix.length && str.substring(0, prefix.length) == prefix;
    };
  
    var document = node.ownerDocument;
  
    var li = document.createElement('li');
    while (node.firstChild) {
      li.appendChild(node.firstChild);
    }
    // An array of lines.  We split below, so this is initialized to one
    // un-split line.
    var listItems = [li];
  
    function walk(node) {
      var type = node.nodeType;
      if (type == 1 && !nocode.test(node.className)) {  // Element
        if ('br' === node.nodeName) {
          breakAfter(node);
          // Discard the <BR> since it is now flush against a </LI>.
          if (node.parentNode) {
            node.parentNode.removeChild(node);
          }
        } else {
          for (var child = node.firstChild; child; child = child.nextSibling) {
            walk(child);
          }
        }
      } else if ((type == 3 || type == 4) && isPreformatted) {  // Text
        var text = node.nodeValue;
        var match = text.match(lineBreak);
        if (match) {
          var firstLine = text.substring(0, match.index);
          node.nodeValue = firstLine;
          var tail = text.substring(match.index + match[0].length);
          if (tail) {
            var parent = node.parentNode;
            parent.insertBefore(
              document.createTextNode(tail), node.nextSibling);
          }
          breakAfter(node);
          if (!firstLine) {
            // Don't leave blank text nodes in the DOM.
            node.parentNode.removeChild(node);
          }
        }
      }
    }
  
    // Split a line after the given node.
    function breakAfter(lineEndNode) {
      // If there's nothing to the right, then we can skip ending the line
      // here, and move root-wards since splitting just before an end-tag
      // would require us to create a bunch of empty copies.
      while (!lineEndNode.nextSibling) {
        lineEndNode = lineEndNode.parentNode;
        if (!lineEndNode) { return; }
      }
  
      function breakLeftOf(limit, copy) {
        // Clone shallowly if this node needs to be on both sides of the break.
        var rightSide = copy ? limit.cloneNode(false) : limit;
        var parent = limit.parentNode;
        if (parent) {
          // We clone the parent chain.
          // This helps us resurrect important styling elements that cross lines.
          // E.g. in <i>Foo<br>Bar</i>
          // should be rewritten to <li><i>Foo</i></li><li><i>Bar</i></li>.
          var parentClone = breakLeftOf(parent, 1);
          // Move the clone and everything to the right of the original
          // onto the cloned parent.
          var next = limit.nextSibling;
          parentClone.appendChild(rightSide);
          for (var sibling = next; sibling; sibling = next) {
            next = sibling.nextSibling;
            parentClone.appendChild(sibling);
          }
        }
        return rightSide;
      }
  
      var copiedListItem = breakLeftOf(lineEndNode.nextSibling, 0);
  
      // Walk the parent chain until we reach an unattached LI.
      for (var parent;
           // Check nodeType since IE invents document fragments.
           (parent = copiedListItem.parentNode) && parent.nodeType === 1;) {
        copiedListItem = parent;
      }
      // Put it on the list of lines for later processing.
      listItems.push(copiedListItem);
    }
  
    // Split lines while there are lines left to split.
    for (var i = 0;  // Number of lines that have been split so far.
         i < listItems.length;  // length updated by breakAfter calls.
         ++i) {
      walk(listItems[i]);
    }
  
    // Make sure numeric indices show correctly.
    if (opt_startLineNum === (opt_startLineNum|0)) {
      listItems[0].setAttribute('value', opt_startLineNum);
    }
  
    var ol = document.createElement(opt_numberLines ? 'ol' : 'ul');
    const classNames = [];
    if (opt_numberLines) {
      classNames.push('linenums');
    }
    if (opt_calloutModifiedLines) {
      classNames.push('modifiedlines');
    }
    ol.className = classNames.join(" ");
    var offset = Math.max(0, (opt_startLineNum - 1 /* zero index */) | 0) || 0;
    for (var i = 0, n = listItems.length; i < n; ++i) {
      li = listItems[i];
      // Stick a class on the LIs so that stylesheets can
      // color odd/even rows, or any other row pattern that
      // is co-prime with 10.
      const classNames = [];
      if (opt_numberLines) {
        classNames.push('L' + ((i + offset) % 10));
      }
      if (!li.firstChild) {
        li.appendChild(document.createTextNode('\xA0'));
      }
      if (opt_calloutModifiedLines) {
        // returns undefined to continue, true of found, false if not found
        function findTextWithPrefix(node) {
          if (!node) {
            return;
          }
          var type = node.nodeType;
          if (type == 1) { // Element
            for (var child = node.firstChild; child; child = child.nextSibling) {
              var result = findTextWithPrefix(child);
              if (result !== undefined) {
                  return result;
              }
            }
          } else if (type == 3 || type == 4) {
            var text = node.nodeValue;
            if (text.length > 0) {
              for (var pp = 0; pp < prefixes.length; ++pp) {
                var prefixInfo = prefixes[pp];
                if (startsWith(text, prefixInfo.prefix)) {
                  node.nodeValue = text.substring(prefixInfo.prefix.length) || ' ';
                  return prefixInfo.className;
                }
              }
              return false;
            }
          }
        }
        var foundPrefix = findTextWithPrefix(li);
        if (foundPrefix) {
          classNames.push(foundPrefix);
        }
      }
      li.className = classNames.join(" ");
      ol.appendChild(li);
    }
  
    node.appendChild(ol);
  }
Given a DOM subtree, wraps it in a list, and puts each line into its own
list item.

@param {Node} node modified in place.  Its content is pulled into an
    HTMLOListElement, and each line is moved into a separate list item.
    This requires cloning elements, so the input might not have unique
    IDs after numbering.
@param {boolean} isPreformatted true if white-space in text nodes should
    be treated as significant.