Make gr-builder a Polymer component

Updated tests, fixed draft comments, context expanding.

Change-Id: Ic4bd9682c63edd8e80fbc2abcb4fa5e406a202ab
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js
deleted file mode 100644
index b897708..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-image.js
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the 'License');
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an 'AS IS' BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-(function(window, GrDiffBuilderSideBySide) {
-  'use strict';
-
-  function GrDiffBuilderImage(diff, comments, prefs, outputEl, baseImage,
-      revisionImage) {
-    GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl);
-    this._baseImage = baseImage;
-    this._revisionImage = revisionImage;
-  }
-
-  GrDiffBuilderImage.prototype = Object.create(
-      GrDiffBuilderSideBySide.prototype);
-  GrDiffBuilderImage.prototype.constructor = GrDiffBuilderImage;
-
-  GrDiffBuilderImage.prototype.emitDiff = function() {
-    this.emitGroup(this._groups[0]);
-
-    var section = this._createElement('tbody', 'image-diff');
-
-    this._emitImagePair(section);
-    this._emitImageLabels(section);
-
-    this._outputEl.appendChild(section);
-  };
-
-  GrDiffBuilderImage.prototype._emitImagePair = function(section) {
-    var tr = this._createElement('tr');
-
-    tr.appendChild(this._createElement('td'));
-    tr.appendChild(this._createImageCell(this._baseImage, 'left'));
-
-    tr.appendChild(this._createElement('td'));
-    tr.appendChild(this._createImageCell(this._revisionImage, 'right'));
-
-    section.appendChild(tr);
-  };
-
-  GrDiffBuilderImage.prototype._createImageCell = function(image, className) {
-    var td = this._createElement('td', className);
-    if (image) {
-      var imageEl = this._createElement('img');
-      imageEl.src = 'data:' + image.type + ';base64, ' + image.body;
-      image._height = imageEl.naturalHeight;
-      image._width = imageEl.naturalWidth;
-      imageEl.addEventListener('error', function(e) {
-        imageEl.remove();
-        td.textContent = '[Image failed to load]';
-      });
-      td.appendChild(imageEl);
-    }
-    return td;
-  };
-
-  GrDiffBuilderImage.prototype._emitImageLabels = function(section) {
-    var tr = this._createElement('tr');
-
-    tr.appendChild(this._createElement('td'));
-    var td = this._createElement('td', 'left');
-    var label = this._createElement('label');
-    label.textContent = this._getImageLabel(this._baseImage);
-    td.appendChild(label);
-    tr.appendChild(td);
-
-    tr.appendChild(this._createElement('td'));
-    td = this._createElement('td', 'right');
-    label = this._createElement('label');
-    label.textContent = this._getImageLabel(this._revisionImage);
-    td.appendChild(label);
-    tr.appendChild(td);
-
-    section.appendChild(tr);
-  };
-
-  GrDiffBuilderImage.prototype._getImageLabel = function(image) {
-    if (image) {
-      var type = image.type || image._expectedType;
-      if (image._width && image._height) {
-        return image._width + '⨉' + image._height + ' ' + type;
-      } else {
-        return type;
-      }
-    }
-    return 'No image';
-  };
-
-  window.GrDiffBuilderImage = GrDiffBuilderImage;
-})(window, GrDiffBuilderSideBySide);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
deleted file mode 100644
index 7e8779f..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the 'License');
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an 'AS IS' BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-(function(window, GrDiffBuilder) {
-  'use strict';
-
-  function GrDiffBuilderSideBySide(diff, comments, prefs, outputEl) {
-    GrDiffBuilder.call(this, diff, comments, prefs, outputEl);
-  }
-  GrDiffBuilderSideBySide.prototype = Object.create(GrDiffBuilder.prototype);
-  GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide;
-
-  GrDiffBuilderSideBySide.prototype.buildSectionElement = function(group,
-      opt_beforeSection) {
-    var sectionEl = this._createElement('tbody', 'section');
-    sectionEl.classList.add(group.type);
-    var pairs = group.getSideBySidePairs();
-    for (var i = 0; i < pairs.length; i++) {
-      sectionEl.appendChild(this._createRow(sectionEl, pairs[i].left,
-          pairs[i].right));
-    }
-    return sectionEl;
-  };
-
-  GrDiffBuilderSideBySide.prototype._createRow = function(section, leftLine,
-      rightLine) {
-    var row = this._createElement('tr');
-    row.classList.add('diff-row', 'side-by-side');
-    row.setAttribute('left-type', leftLine.type);
-    row.setAttribute('right-type', rightLine.type);
-
-    this._appendPair(section, row, leftLine, leftLine.beforeNumber,
-        GrDiffBuilder.Side.LEFT);
-    this._appendPair(section, row, rightLine, rightLine.afterNumber,
-        GrDiffBuilder.Side.RIGHT);
-    return row;
-  };
-
-  GrDiffBuilderSideBySide.prototype._appendPair = function(section, row, line,
-      lineNumber, side) {
-    var lineEl = this._createLineEl(line, lineNumber, line.type, side);
-    lineEl.classList.add(side);
-    row.appendChild(lineEl);
-    var action = this._createContextControl(section, line);
-    if (action) {
-      row.appendChild(action);
-    } else {
-      var textEl = this._createTextEl(line);
-      var threadEl = this._commentThreadForLine(line, side);
-      if (threadEl) {
-        textEl.appendChild(threadEl);
-      }
-      row.appendChild(textEl);
-    }
-  };
-
-  window.GrDiffBuilderSideBySide = GrDiffBuilderSideBySide;
-})(window, GrDiffBuilder);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
deleted file mode 100644
index 2f1aac6..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the 'License');
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an 'AS IS' BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-(function(window, GrDiffBuilder) {
-  'use strict';
-
-  function GrDiffBuilderUnified(diff, comments, prefs, outputEl) {
-    GrDiffBuilder.call(this, diff, comments, prefs, outputEl);
-  }
-  GrDiffBuilderUnified.prototype = Object.create(GrDiffBuilder.prototype);
-  GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified;
-
-  GrDiffBuilderUnified.prototype.buildSectionElement = function(group) {
-    var sectionEl = this._createElement('tbody', 'section');
-    sectionEl.classList.add(group.type);
-
-    for (var i = 0; i < group.lines.length; ++i) {
-      sectionEl.appendChild(this._createRow(sectionEl, group.lines[i]));
-    }
-    return sectionEl;
-  };
-
-  GrDiffBuilderUnified.prototype._createRow = function(section, line) {
-    var row = this._createElement('tr', line.type);
-    var lineEl = this._createLineEl(line, line.beforeNumber,
-        GrDiffLine.Type.REMOVE);
-    lineEl.classList.add('left');
-    row.appendChild(lineEl);
-    lineEl = this._createLineEl(line, line.afterNumber,
-        GrDiffLine.Type.ADD);
-    lineEl.classList.add('right');
-    row.appendChild(lineEl);
-    row.classList.add('diff-row', 'unified');
-
-    var action = this._createContextControl(section, line);
-    if (action) {
-      row.appendChild(action);
-    } else {
-      var textEl = this._createTextEl(line);
-      var threadEl = this._commentThreadForLine(line);
-      if (threadEl) {
-        textEl.appendChild(threadEl);
-      }
-      row.appendChild(textEl);
-    }
-    return row;
-  };
-
-  window.GrDiffBuilderUnified = GrDiffBuilderUnified;
-})(window, GrDiffBuilder);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
deleted file mode 100644
index f03687c..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
+++ /dev/null
@@ -1,691 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the 'License');
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an 'AS IS' BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-(function(window, GrDiffGroup, GrDiffLine) {
-  'use strict';
-
-  function GrDiffBuilder(diff, comments, prefs, outputEl) {
-    this._diff = diff;
-    this._comments = comments;
-    this._prefs = prefs;
-    this._outputEl = outputEl;
-    this._groups = [];
-
-    this._commentLocations = this._getCommentLocations(comments);
-    this._processContent(diff.content, this._groups, prefs.context);
-  }
-
-  GrDiffBuilder.LESS_THAN_CODE = '<'.charCodeAt(0);
-  GrDiffBuilder.GREATER_THAN_CODE = '>'.charCodeAt(0);
-  GrDiffBuilder.AMPERSAND_CODE = '&'.charCodeAt(0);
-  GrDiffBuilder.SEMICOLON_CODE = ';'.charCodeAt(0);
-
-  GrDiffBuilder.TAB_REGEX = /\t/g;
-
-  GrDiffBuilder.LINE_FEED_HTML =
-      '<span class="style-scope gr-diff br"></span>';
-
-  GrDiffBuilder.GroupType = {
-    ADDED: 'b',
-    BOTH: 'ab',
-    REMOVED: 'a',
-  };
-
-  GrDiffBuilder.Highlights = {
-    ADDED: 'edit_b',
-    REMOVED: 'edit_a',
-  };
-
-  GrDiffBuilder.Side = {
-    LEFT: 'left',
-    RIGHT: 'right',
-  };
-
-  GrDiffBuilder.ContextButtonType = {
-    ABOVE: 'above',
-    BELOW: 'below',
-    ALL: 'all',
-  };
-
-  var PARTIAL_CONTEXT_AMOUNT = 10;
-
-  GrDiffBuilder.prototype.emitDiff = function() {
-    for (var i = 0; i < this._groups.length; i++) {
-      this.emitGroup(this._groups[i]);
-    }
-  };
-
-  GrDiffBuilder.prototype.buildSectionElement = function(
-      group, opt_beforeSection) {
-    throw Error('Subclasses must implement buildGroupElement');
-  };
-
-  GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
-    var element = this.buildSectionElement(group);
-    this._outputEl.insertBefore(element, opt_beforeSection);
-    group.element = element;
-  };
-
-  GrDiffBuilder.prototype.renderSection = function(element) {
-    for (var i = 0; i < this._groups.length; i++) {
-      var group = this._groups[i];
-      if (group.element === element) {
-        var newElement = this.buildSectionElement(group);
-        group.element.parentElement.replaceChild(newElement, group.element);
-        group.element = newElement;
-        break;
-      }
-    }
-  };
-
-  GrDiffBuilder.prototype.getSectionsByLineRange = function(
-      startLine, endLine, opt_side) {
-    var sections = [];
-    for (var i = 0; i < this._groups.length; i++) {
-      var group = this._groups[i];
-      if (group.lines.length === 0) {
-        continue;
-      }
-      var groupStartLine;
-      var groupEndLine;
-      if (opt_side === GrDiffBuilder.Side.LEFT) {
-        groupStartLine = group.lines[0].beforeNumber;
-        groupEndLine = group.lines[group.lines.length - 1].beforeNumber;
-      } else if (opt_side === GrDiffBuilder.Side.RIGHT) {
-        groupStartLine = group.lines[0].afterNumber;
-        groupEndLine = group.lines[group.lines.length - 1].afterNumber;
-      }
-      if (startLine <= groupEndLine && endLine >= groupStartLine) {
-        sections.push(group.element);
-      }
-    }
-    return sections;
-  };
-
-  GrDiffBuilder.prototype._processContent = function(content, groups, context) {
-    this._appendFileComments(groups);
-
-    var WHOLE_FILE = -1;
-    context = content.length > 1 ? context : WHOLE_FILE;
-
-    var lineNums = {
-      left: 0,
-      right: 0,
-    };
-    content = this._splitCommonGroupsWithComments(content, lineNums);
-    for (var i = 0; i < content.length; i++) {
-      var group = content[i];
-      var lines = [];
-
-      if (group[GrDiffBuilder.GroupType.BOTH] !== undefined) {
-        var rows = group[GrDiffBuilder.GroupType.BOTH];
-        this._appendCommonLines(rows, lines, lineNums);
-
-        var hiddenRange = [context, rows.length - context];
-        if (i === 0) {
-          hiddenRange[0] = 0;
-        } else if (i === content.length - 1) {
-          hiddenRange[1] = rows.length;
-        }
-
-        if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 0) {
-          this._insertContextGroups(groups, lines, hiddenRange);
-        } else {
-          groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, lines));
-        }
-        continue;
-      }
-
-      if (group[GrDiffBuilder.GroupType.REMOVED] !== undefined) {
-        var highlights = undefined;
-        if (group[GrDiffBuilder.Highlights.REMOVED] !== undefined) {
-          highlights = this._normalizeIntralineHighlights(
-              group[GrDiffBuilder.GroupType.REMOVED],
-              group[GrDiffBuilder.Highlights.REMOVED]);
-        }
-        this._appendRemovedLines(group[GrDiffBuilder.GroupType.REMOVED], lines,
-            lineNums, highlights);
-      }
-
-      if (group[GrDiffBuilder.GroupType.ADDED] !== undefined) {
-        var highlights = undefined;
-        if (group[GrDiffBuilder.Highlights.ADDED] !== undefined) {
-          highlights = this._normalizeIntralineHighlights(
-            group[GrDiffBuilder.GroupType.ADDED],
-            group[GrDiffBuilder.Highlights.ADDED]);
-        }
-        this._appendAddedLines(group[GrDiffBuilder.GroupType.ADDED], lines,
-            lineNums, highlights);
-      }
-      groups.push(new GrDiffGroup(GrDiffGroup.Type.DELTA, lines));
-    }
-  };
-
-  GrDiffBuilder.prototype._appendFileComments = function(groups) {
-    var line = new GrDiffLine(GrDiffLine.Type.BOTH);
-    line.beforeNumber = GrDiffLine.FILE;
-    line.afterNumber = GrDiffLine.FILE;
-    groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]));
-  };
-
-  GrDiffBuilder.prototype._getCommentLocations = function(comments) {
-    var result = {
-      left: {},
-      right: {},
-    };
-    for (var side in comments) {
-      if (side !== GrDiffBuilder.Side.LEFT &&
-          side !== GrDiffBuilder.Side.RIGHT) {
-        continue;
-      }
-      comments[side].forEach(function(c) {
-        result[side][c.line || GrDiffLine.FILE] = true;
-      });
-    }
-    return result;
-  };
-
-  GrDiffBuilder.prototype._commentIsAtLineNum = function(side, lineNum) {
-    return this._commentLocations[side][lineNum] === true;
-  };
-
-  // In order to show comments out of the bounds of the selected context,
-  // treat them as separate chunks within the model so that the content (and
-  // context surrounding it) renders correctly.
-  GrDiffBuilder.prototype._splitCommonGroupsWithComments = function(content,
-      lineNums) {
-    var result = [];
-    var leftLineNum = lineNums.left;
-    var rightLineNum = lineNums.right;
-    for (var i = 0; i < content.length; i++) {
-      if (!content[i].ab) {
-        result.push(content[i]);
-        if (content[i].a) {
-          leftLineNum += content[i].a.length;
-        }
-        if (content[i].b) {
-          rightLineNum += content[i].b.length;
-        }
-        continue;
-      }
-      var chunk = content[i].ab;
-      var currentChunk = {ab: []};
-      for (var j = 0; j < chunk.length; j++) {
-        leftLineNum++;
-        rightLineNum++;
-        if (this._commentIsAtLineNum(GrDiffBuilder.Side.LEFT, leftLineNum) ||
-            this._commentIsAtLineNum(GrDiffBuilder.Side.RIGHT, rightLineNum)) {
-          if (currentChunk.ab && currentChunk.ab.length > 0) {
-            result.push(currentChunk);
-            currentChunk = {ab: []};
-          }
-          result.push({ab: [chunk[j]]});
-        } else {
-          currentChunk.ab.push(chunk[j]);
-        }
-      }
-      // != instead of !== because we want to cover both undefined and null.
-      if (currentChunk.ab != null && currentChunk.ab.length > 0) {
-        result.push(currentChunk);
-      }
-    }
-    return result;
-  };
-
-  // The `highlights` array consists of a list of <skip length, mark length>
-  // pairs, where the skip length is the number of characters between the
-  // end of the previous edit and the start of this edit, and the mark
-  // length is the number of edited characters following the skip. The start
-  // of the edits is from the beginning of the related diff content lines.
-  //
-  // Note that the implied newline character at the end of each line is
-  // included in the length calculation, and thus it is possible for the
-  // edits to span newlines.
-  //
-  // A line highlight object consists of three fields:
-  // - contentIndex: The index of the diffChunk `content` field (the line
-  //   being referred to).
-  // - startIndex: Where the highlight should begin.
-  // - endIndex: (optional) Where the highlight should end. If omitted, the
-  //   highlight is meant to be a continuation onto the next line.
-  GrDiffBuilder.prototype._normalizeIntralineHighlights = function(content,
-      highlights) {
-    var contentIndex = 0;
-    var idx = 0;
-    var normalized = [];
-    for (var i = 0; i < highlights.length; i++) {
-      var line = content[contentIndex] + '\n';
-      var hl = highlights[i];
-      var j = 0;
-      while (j < hl[0]) {
-        if (idx === line.length) {
-          idx = 0;
-          line = content[++contentIndex] + '\n';
-          continue;
-        }
-        idx++;
-        j++;
-      }
-      var lineHighlight = {
-        contentIndex: contentIndex,
-        startIndex: idx,
-      };
-
-      j = 0;
-      while (line && j < hl[1]) {
-        if (idx === line.length) {
-          idx = 0;
-          line = content[++contentIndex] + '\n';
-          normalized.push(lineHighlight);
-          lineHighlight = {
-            contentIndex: contentIndex,
-            startIndex: idx,
-          };
-          continue;
-        }
-        idx++;
-        j++;
-      }
-      lineHighlight.endIndex = idx;
-      normalized.push(lineHighlight);
-    }
-    return normalized;
-  };
-
-  GrDiffBuilder.prototype._insertContextGroups = function(groups, lines,
-      hiddenRange) {
-    var linesBeforeCtx = lines.slice(0, hiddenRange[0]);
-    var hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]);
-    var linesAfterCtx = lines.slice(hiddenRange[1]);
-
-    if (linesBeforeCtx.length > 0) {
-      groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx));
-    }
-
-    var ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
-    ctxLine.contextGroup =
-        new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines);
-    groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
-        [ctxLine]));
-
-    if (linesAfterCtx.length > 0) {
-      groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx));
-    }
-  };
-
-  GrDiffBuilder.prototype._appendCommonLines = function(rows, lines, lineNums) {
-    for (var i = 0; i < rows.length; i++) {
-      var line = new GrDiffLine(GrDiffLine.Type.BOTH);
-      line.text = rows[i];
-      line.beforeNumber = ++lineNums.left;
-      line.afterNumber = ++lineNums.right;
-      lines.push(line);
-    }
-  };
-
-  GrDiffBuilder.prototype._appendRemovedLines = function(rows, lines, lineNums,
-      opt_highlights) {
-    for (var i = 0; i < rows.length; i++) {
-      var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
-      line.text = rows[i];
-      line.beforeNumber = ++lineNums.left;
-      if (opt_highlights) {
-        line.highlights = opt_highlights.filter(function(hl) {
-          return hl.contentIndex === i;
-        });
-      }
-      lines.push(line);
-    }
-  };
-
-  GrDiffBuilder.prototype._appendAddedLines = function(rows, lines, lineNums,
-      opt_highlights) {
-    for (var i = 0; i < rows.length; i++) {
-      var line = new GrDiffLine(GrDiffLine.Type.ADD);
-      line.text = rows[i];
-      line.afterNumber = ++lineNums.right;
-      if (opt_highlights) {
-        line.highlights = opt_highlights.filter(function(hl) {
-          return hl.contentIndex === i;
-        });
-      }
-      lines.push(line);
-    }
-  };
-
-  GrDiffBuilder.prototype._createContextControl = function(section, line) {
-    if (!line.contextGroup || !line.contextGroup.lines.length) {
-      return null;
-    }
-
-    var td = this._createElement('td');
-    var showPartialLinks =
-        line.contextGroup.lines.length > PARTIAL_CONTEXT_AMOUNT;
-
-    if (showPartialLinks) {
-      td.appendChild(this._createContextButton(
-          GrDiffBuilder.ContextButtonType.ABOVE, section, line));
-      td.appendChild(document.createTextNode(' - '));
-    }
-
-    td.appendChild(this._createContextButton(
-        GrDiffBuilder.ContextButtonType.ALL, section, line));
-
-    if (showPartialLinks) {
-      td.appendChild(document.createTextNode(' - '));
-      td.appendChild(this._createContextButton(
-          GrDiffBuilder.ContextButtonType.BELOW, section, line));
-    }
-
-    return td;
-  };
-
-  GrDiffBuilder.prototype._createContextButton = function(type, section, line) {
-    var contextLines = line.contextGroup.lines;
-    var context = PARTIAL_CONTEXT_AMOUNT;
-
-    var button = this._createElement('gr-button', 'showContext');
-    button.setAttribute('link', true);
-
-    var text;
-    var groups = []; // The groups that replace this one if tapped.
-
-    if (type === GrDiffBuilder.ContextButtonType.ALL) {
-      text = 'Show ' + contextLines.length + ' common line';
-      if (contextLines.length > 1) { text += 's'; }
-      groups.push(line.contextGroup);
-    } else if (type === GrDiffBuilder.ContextButtonType.ABOVE) {
-      text = '+' + context + '↑';
-      this._insertContextGroups(groups, contextLines,
-          [context, contextLines.length]);
-    } else if (type === GrDiffBuilder.ContextButtonType.BELOW) {
-      text = '+' + context + '↓';
-      this._insertContextGroups(groups, contextLines,
-          [0, contextLines.length - context]);
-    }
-
-    button.textContent = text;
-
-    button.addEventListener('tap', function(e) {
-      e.detail = {
-        groups: groups,
-        section: section,
-      };
-      // Let it bubble up the DOM tree.
-    });
-
-    return button;
-  };
-
-  GrDiffBuilder.prototype._getCommentsForLine = function(comments, line,
-      opt_side) {
-    function byLineNum(lineNum) {
-      return function(c) {
-        return (c.line === lineNum) ||
-               (c.line === undefined && lineNum === GrDiffLine.FILE);
-      };
-    }
-    var leftComments =
-        comments[GrDiffBuilder.Side.LEFT].filter(byLineNum(line.beforeNumber));
-    var rightComments =
-        comments[GrDiffBuilder.Side.RIGHT].filter(byLineNum(line.afterNumber));
-
-    var result;
-
-    switch (opt_side) {
-      case GrDiffBuilder.Side.LEFT:
-        result = leftComments;
-        break;
-      case GrDiffBuilder.Side.RIGHT:
-        result = rightComments;
-        break;
-      default:
-        result = leftComments.concat(rightComments);
-        break;
-    }
-
-    return result;
-  };
-
-  GrDiffBuilder.prototype.createCommentThread = function(changeNum, patchNum,
-      path, side, projectConfig) {
-    var threadEl = document.createElement('gr-diff-comment-thread');
-    threadEl.changeNum = changeNum;
-    threadEl.patchNum = patchNum;
-    threadEl.path = path;
-    threadEl.side = side;
-    threadEl.projectConfig = projectConfig;
-    return threadEl;
-  };
-
-  GrDiffBuilder.prototype._commentThreadForLine = function(line, opt_side) {
-    var comments = this._getCommentsForLine(this._comments, line, opt_side);
-    if (!comments || comments.length === 0) {
-      return null;
-    }
-
-    var patchNum = this._comments.meta.patchRange.patchNum;
-    var side = comments[0].side || 'REVISION';
-    if (line.type === GrDiffLine.Type.REMOVE ||
-        opt_side === GrDiffBuilder.Side.LEFT) {
-      if (this._comments.meta.patchRange.basePatchNum === 'PARENT') {
-        side = 'PARENT';
-      } else {
-        patchNum = this._comments.meta.patchRange.basePatchNum;
-      }
-    }
-    var threadEl = this.createCommentThread(
-        this._comments.meta.changeNum,
-        patchNum,
-        this._comments.meta.path,
-        side,
-        this._comments.meta.projectConfig);
-    threadEl.comments = comments;
-    return threadEl;
-  };
-
-  GrDiffBuilder.prototype._createLineEl = function(line, number, type,
-      opt_class) {
-    var td = this._createElement('td');
-    if (opt_class) {
-      td.classList.add(opt_class);
-    }
-    if (line.type === GrDiffLine.Type.BLANK) {
-      return td;
-    } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
-      td.classList.add('contextLineNum');
-      td.setAttribute('data-value', '@@');
-    } else if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
-      td.classList.add('lineNum');
-      td.setAttribute('data-value', number);
-    }
-    return td;
-  };
-
-  GrDiffBuilder.prototype._createTextEl = function(line) {
-    var td = this._createElement('td');
-    if (line.type !== GrDiffLine.Type.BLANK) {
-      td.classList.add('content');
-    }
-    td.classList.add(line.type);
-    var text = line.text;
-    var html = util.escapeHTML(text);
-
-    td.classList.add(line.highlights.length > 0 ?
-        'lightHighlight' : 'darkHighlight');
-
-    if (line.highlights.length > 0) {
-      html = this._addIntralineHighlights(text, html, line.highlights);
-    }
-
-    if (this._textLength(text, this._prefs.tab_size) >
-        this._prefs.line_length) {
-      html = this._addNewlines(text, html);
-    }
-    html = this._addTabWrappers(html);
-
-    // If the html is equivalent to the text then it didn't get highlighted
-    // or escaped. Use textContent which is faster than innerHTML.
-    if (html === text) {
-      td.textContent = text;
-    } else {
-      td.innerHTML = html;
-    }
-    return td;
-  };
-
-  GrDiffBuilder.prototype._textLength = function(text, tabSize) {
-    // TODO(andybons): Unicode support.
-    var numChars = 0;
-    for (var i = 0; i < text.length; i++) {
-      if (text[i] === '\t') {
-        numChars += tabSize;
-      } else {
-        numChars++;
-      }
-    }
-    return numChars;
-  };
-
-  // Advance `index` by the appropriate number of characters that would
-  // represent one source code character and return that index. For
-  // example, for source code '<span>' the escaped html string is
-  // '&lt;span&gt;'. Advancing from index 0 on the prior html string would
-  // return 4, since &lt; maps to one source code character ('<').
-  GrDiffBuilder.prototype._advanceChar = function(html, index) {
-    // TODO(andybons): Unicode is all kinds of messed up in JS. Account for it.
-    // https://mathiasbynens.be/notes/javascript-unicode
-
-    // Tags don't count as characters
-    while (index < html.length &&
-           html.charCodeAt(index) === GrDiffBuilder.LESS_THAN_CODE) {
-      while (index < html.length &&
-             html.charCodeAt(index) !== GrDiffBuilder.GREATER_THAN_CODE) {
-        index++;
-      }
-      index++;  // skip the ">" itself
-    }
-    // An HTML entity (e.g., &lt;) counts as one character.
-    if (index < html.length &&
-        html.charCodeAt(index) === GrDiffBuilder.AMPERSAND_CODE) {
-      while (index < html.length &&
-             html.charCodeAt(index) !== GrDiffBuilder.SEMICOLON_CODE) {
-        index++;
-      }
-    }
-    return index + 1;
-  };
-
-  GrDiffBuilder.prototype._addNewlines = function(text, html) {
-    var htmlIndex = 0;
-    var indices = [];
-    var numChars = 0;
-    for (var i = 0; i < text.length; i++) {
-      if (numChars > 0 && numChars % this._prefs.line_length === 0) {
-        indices.push(htmlIndex);
-      }
-      htmlIndex = this._advanceChar(html, htmlIndex);
-      if (text[i] === '\t') {
-        numChars += this._prefs.tab_size;
-      } else {
-        numChars++;
-      }
-    }
-    var result = html;
-    // Since the result string is being altered in place, start from the end
-    // of the string so that the insertion indices are not affected as the
-    // result string changes.
-    for (var i = indices.length - 1; i >= 0; i--) {
-      result = result.slice(0, indices[i]) + GrDiffBuilder.LINE_FEED_HTML +
-          result.slice(indices[i]);
-    }
-    return result;
-  };
-
-  GrDiffBuilder.prototype._addTabWrappers = function(html) {
-    var htmlStr = this._getTabWrapper(this._prefs.tab_size,
-        this._prefs.show_tabs);
-    return html.replace(GrDiffBuilder.TAB_REGEX, htmlStr);
-  };
-
-  GrDiffBuilder.prototype._addIntralineHighlights = function(content, html,
-      highlights) {
-    var START_TAG = '<hl class="style-scope gr-diff">';
-    var END_TAG = '</hl>';
-
-    for (var i = 0; i < highlights.length; i++) {
-      var hl = highlights[i];
-
-      var htmlStartIndex = 0;
-      // Find the index of the HTML string to insert the start tag.
-      for (var j = 0; j < hl.startIndex; j++) {
-        htmlStartIndex = this._advanceChar(html, htmlStartIndex);
-      }
-
-      var htmlEndIndex = 0;
-      if (hl.endIndex !== undefined) {
-        for (var j = 0; j < hl.endIndex; j++) {
-          htmlEndIndex = this._advanceChar(html, htmlEndIndex);
-        }
-      } else {
-        // If endIndex isn't present, continue to the end of the line.
-        htmlEndIndex = html.length;
-      }
-      // The start and end indices could be the same if a highlight is meant
-      // to start at the end of a line and continue onto the next one.
-      // Ignore it.
-      if (htmlStartIndex !== htmlEndIndex) {
-        html = html.slice(0, htmlStartIndex) + START_TAG +
-              html.slice(htmlStartIndex, htmlEndIndex) + END_TAG +
-              html.slice(htmlEndIndex);
-      }
-    }
-    return html;
-  };
-
-  GrDiffBuilder.prototype._getTabWrapper = function(tabSize, showTabs) {
-    // Force this to be a number to prevent arbitrary injection.
-    tabSize = +tabSize;
-    if (isNaN(tabSize)) {
-      throw Error('Invalid tab size from preferences.');
-    }
-
-    var str = '<span class="style-scope gr-diff tab ';
-    if (showTabs) {
-      str += 'withIndicator';
-    }
-    str += '" style="';
-    // TODO(andybons): CSS tab-size is not supported in IE.
-    str += 'tab-size:' + tabSize + ';';
-    str += '-moz-tab-size:' + tabSize + ';';
-    str += '">\t</span>';
-    return str;
-  };
-
-  GrDiffBuilder.prototype._createElement = function(tagName, className) {
-    var el = document.createElement(tagName);
-    // When Shady DOM is being used, these classes are added to account for
-    // Polymer's polyfill behavior. In order to guarantee sufficient
-    // specificity within the CSS rules, these are added to every element.
-    // Since the Polymer DOM utility functions (which would do this
-    // automatically) are not being used for performance reasons, this is
-    // done manually.
-    el.classList.add('style-scope', 'gr-diff');
-    if (!!className) {
-      el.classList.add(className);
-    }
-    return el;
-  };
-
-  window.GrDiffBuilder = GrDiffBuilder;
-})(window, GrDiffGroup, GrDiffLine);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
deleted file mode 100644
index 5905bd9..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
+++ /dev/null
@@ -1,602 +0,0 @@
-<!DOCTYPE html>
-<!--
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-diff-builder</title>
-
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="gr-diff-line.js"></script>
-<script src="gr-diff-group.js"></script>
-<script src="gr-diff-builder.js"></script>
-
-<script>
-  suite('gr-diff-builder tests', function() {
-    var builder;
-
-    setup(function() {
-      var prefs = {
-        line_length: 10,
-        show_tabs: true,
-        tab_size: 4,
-      };
-      builder = new GrDiffBuilder({content: []}, {left: [], right: []}, prefs);
-    });
-
-    test('process loaded content', function() {
-      var content = [
-        {
-          ab: [
-            '<!DOCTYPE html>',
-            '<meta charset="utf-8">',
-          ]
-        },
-        {
-          a: [
-            '  Welcome ',
-            '  to the wooorld of tomorrow!',
-          ],
-          b: [
-            '  Hello, world!',
-          ],
-        },
-        {
-          ab: [
-            'Leela: This is the only place the ship can’t hear us, so ',
-            'everyone pretend to shower.',
-            'Fry: Same as every day. Got it.',
-          ]
-        },
-      ];
-      var groups = [];
-
-      builder._processContent(content, groups, -1);
-
-      assert.equal(groups.length, 4);
-
-      var group = groups[0];
-      assert.equal(group.type, GrDiffGroup.Type.BOTH);
-      assert.equal(group.lines.length, 1);
-      assert.equal(group.lines[0].text, '');
-      assert.equal(group.lines[0].beforeNumber, GrDiffLine.FILE);
-      assert.equal(group.lines[0].afterNumber, GrDiffLine.FILE);
-
-      group = groups[1];
-      assert.equal(group.type, GrDiffGroup.Type.BOTH);
-      assert.equal(group.lines.length, 2);
-      assert.equal(group.lines.length, 2);
-
-      function beforeNumberFn(l) { return l.beforeNumber; }
-      function afterNumberFn(l) { return l.afterNumber; }
-      function textFn(l) { return l.text; }
-
-      assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]);
-      assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]);
-      assert.deepEqual(group.lines.map(textFn), [
-        '<!DOCTYPE html>',
-        '<meta charset="utf-8">',
-      ]);
-
-      group = groups[2];
-      assert.equal(group.type, GrDiffGroup.Type.DELTA);
-      assert.equal(group.lines.length, 3);
-      assert.equal(group.adds.length, 1);
-      assert.equal(group.removes.length, 2);
-      assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]);
-      assert.deepEqual(group.adds.map(afterNumberFn), [3]);
-      assert.deepEqual(group.removes.map(textFn), [
-        '  Welcome ',
-        '  to the wooorld of tomorrow!',
-      ]);
-      assert.deepEqual(group.adds.map(textFn), [
-        '  Hello, world!',
-      ]);
-
-      group = groups[3];
-      assert.equal(group.type, GrDiffGroup.Type.BOTH);
-      assert.equal(group.lines.length, 3);
-      assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]);
-      assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]);
-      assert.deepEqual(group.lines.map(textFn), [
-        'Leela: This is the only place the ship can’t hear us, so ',
-        'everyone pretend to shower.',
-        'Fry: Same as every day. Got it.',
-      ]);
-    });
-
-    test('insert context groups', function() {
-      var content = [
-        {ab: []},
-        {a: ['all work and no play make andybons a dull boy']},
-        {ab: []},
-        {b: ['elgoog elgoog elgoog']},
-        {ab: []},
-      ];
-      for (var i = 0; i < 100; i++) {
-        content[0].ab.push('all work and no play make jack a dull boy');
-        content[4].ab.push('all work and no play make jill a dull girl');
-      }
-      for (var i = 0; i < 5; i++) {
-        content[2].ab.push('no tv and no beer make homer go crazy');
-      }
-      var groups = [];
-      var context = 10;
-
-      builder._processContent(content, groups, context);
-
-      assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[0].lines.length, 1);
-      assert.equal(groups[0].lines[0].text, '');
-      assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
-      assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
-
-      assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.instanceOf(groups[1].lines[0].contextGroup, GrDiffGroup);
-      assert.equal(groups[1].lines[0].contextGroup.lines.length, 90);
-      groups[1].lines[0].contextGroup.lines.forEach(function(l) {
-        assert.equal(l.text, content[0].ab[0]);
-      });
-
-      assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[2].lines.length, context);
-      groups[2].lines.forEach(function(l) {
-        assert.equal(l.text, content[0].ab[0]);
-      });
-
-      assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
-      assert.equal(groups[3].lines.length, 1);
-      assert.equal(groups[3].removes.length, 1);
-      assert.equal(groups[3].removes[0].text,
-          'all work and no play make andybons a dull boy');
-
-      assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[4].lines.length, 5);
-      groups[4].lines.forEach(function(l) {
-        assert.equal(l.text, content[2].ab[0]);
-      });
-
-      assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
-      assert.equal(groups[5].lines.length, 1);
-      assert.equal(groups[5].adds.length, 1);
-      assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
-
-      assert.equal(groups[6].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[6].lines.length, context);
-      groups[6].lines.forEach(function(l) {
-        assert.equal(l.text, content[4].ab[0]);
-      });
-
-      assert.equal(groups[7].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.instanceOf(groups[7].lines[0].contextGroup, GrDiffGroup);
-      assert.equal(groups[7].lines[0].contextGroup.lines.length, 90);
-      groups[7].lines[0].contextGroup.lines.forEach(function(l) {
-        assert.equal(l.text, content[4].ab[0]);
-      });
-
-      content = [
-        {a: ['all work and no play make andybons a dull boy']},
-        {ab: []},
-        {b: ['elgoog elgoog elgoog']},
-      ];
-      for (var i = 0; i < 50; i++) {
-        content[1].ab.push('no tv and no beer make homer go crazy');
-      }
-      groups = [];
-
-      builder._processContent(content, groups, 10);
-
-      assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[0].lines.length, 1);
-      assert.equal(groups[0].lines[0].text, '');
-      assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
-      assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
-
-      assert.equal(groups[1].type, GrDiffGroup.Type.DELTA);
-      assert.equal(groups[1].lines.length, 1);
-      assert.equal(groups[1].removes.length, 1);
-      assert.equal(groups[1].removes[0].text,
-          'all work and no play make andybons a dull boy');
-
-      assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[2].lines.length, context);
-      groups[2].lines.forEach(function(l) {
-        assert.equal(l.text, content[1].ab[0]);
-      });
-
-      assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.instanceOf(groups[3].lines[0].contextGroup, GrDiffGroup);
-      assert.equal(groups[3].lines[0].contextGroup.lines.length, 30);
-      groups[3].lines[0].contextGroup.lines.forEach(function(l) {
-        assert.equal(l.text, content[1].ab[0]);
-      });
-
-      assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[4].lines.length, context);
-      groups[4].lines.forEach(function(l) {
-        assert.equal(l.text, content[1].ab[0]);
-      });
-
-      assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
-      assert.equal(groups[5].lines.length, 1);
-      assert.equal(groups[5].adds.length, 1);
-      assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
-    });
-
-    test('context control buttons', function() {
-      var section = {};
-      var line = {contextGroup: {lines: []}};
-
-      // Create 10 lines.
-      for (var i = 0; i < 10; i++) {
-        line.contextGroup.lines.push('lorem upsum');
-      }
-
-      // Does not include +10 buttons when there are fewer than 11 lines.
-      var td = builder._createContextControl(section, line);
-      var buttons = td.querySelectorAll('gr-button.showContext');
-
-      assert.equal(buttons.length, 1);
-      assert.equal(buttons[0].textContent, 'Show 10 common lines');
-
-      // Add another line.
-      line.contextGroup.lines.push('lorem upsum');
-
-      // Includes +10 buttons when there are at least 11 lines.
-      td = builder._createContextControl(section, line);
-      buttons = td.querySelectorAll('gr-button.showContext');
-
-      assert.equal(buttons.length, 3);
-      assert.equal(buttons[0].textContent, '+10↑');
-      assert.equal(buttons[1].textContent, 'Show 11 common lines');
-      assert.equal(buttons[2].textContent, '+10↓');
-    });
-
-    test('newlines', function() {
-      var text = 'abcdef';
-      assert.equal(builder._addNewlines(text, text), text);
-      text = 'a'.repeat(20);
-      assert.equal(builder._addNewlines(text, text),
-          'a'.repeat(10) +
-          GrDiffBuilder.LINE_FEED_HTML +
-          'a'.repeat(10));
-
-      text = '<span class="thumbsup">👍</span>';
-      var html = '&lt;span class=&quot;thumbsup&quot;&gt;👍&lt;&#x2F;span&gt;';
-      assert.equal(builder._addNewlines(text, html),
-          '&lt;span clas' +
-          GrDiffBuilder.LINE_FEED_HTML +
-          's=&quot;thumbsu' +
-          GrDiffBuilder.LINE_FEED_HTML +
-          'p&quot;&gt;👍&lt;&#x2F;spa' +
-          GrDiffBuilder.LINE_FEED_HTML +
-          'n&gt;');
-
-      text = '01234\t56789';
-      assert.equal(builder._addNewlines(text, text),
-          '01234\t5' +
-          GrDiffBuilder.LINE_FEED_HTML +
-          '6789');
-    });
-
-    test('text length with tabs', function() {
-      assert.equal(builder._textLength('12345', 4), 5);
-      assert.equal(builder._textLength('\t\t12', 4), 10);
-    });
-
-    test('tab wrapper insertion', function() {
-      var html = 'abc\tdef';
-      var wrapper = builder._getTabWrapper(
-          builder._prefs.tab_size,
-          builder._prefs.show_tabs);
-      assert.ok(wrapper);
-      assert.isAbove(wrapper.length, 0);
-      assert.equal(builder._addTabWrappers(html), 'abc' + wrapper + 'def');
-      assert.throws(builder._getTabWrapper.bind(
-          builder,
-          '"><img src="/" onerror="alert(1);"><span class="',
-          true));
-    });
-
-    test('comments', function() {
-      var line = new GrDiffLine(GrDiffLine.Type.BOTH);
-      line.beforeNumber = 3;
-      line.afterNumber = 5;
-
-      var comments = {left: [], right: []};
-      assert.deepEqual(builder._getCommentsForLine(comments, line), []);
-      assert.deepEqual(builder._getCommentsForLine(comments, line,
-          GrDiffBuilder.Side.LEFT), []);
-      assert.deepEqual(builder._getCommentsForLine(comments, line,
-          GrDiffBuilder.Side.RIGHT), []);
-
-      comments = {
-        left: [
-          {id: 'l3', line: 3},
-          {id: 'l5', line: 5},
-        ],
-        right: [
-          {id: 'r3', line: 3},
-          {id: 'r5', line: 5},
-        ],
-      };
-      assert.deepEqual(builder._getCommentsForLine(comments, line),
-          [{id: 'l3', line: 3}, {id: 'r5', line: 5}]);
-      assert.deepEqual(builder._getCommentsForLine(comments, line,
-          GrDiffBuilder.Side.LEFT), [{id: 'l3', line: 3}]);
-      assert.deepEqual(builder._getCommentsForLine(comments, line,
-          GrDiffBuilder.Side.RIGHT), [{id: 'r5', line: 5}]);
-    });
-
-    test('comment thread creation', function() {
-      builder._comments = {
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: '3',
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'l3', line: 3},
-          {id: 'l5', line: 5},
-        ],
-        right: [
-          {id: 'r5', line: 5},
-        ],
-      };
-
-      function checkThreadProps(threadEl, patchNum, side, comments) {
-        assert.equal(threadEl.changeNum, '42');
-        assert.equal(threadEl.patchNum, patchNum);
-        assert.equal(threadEl.path, '/path/to/foo');
-        assert.equal(threadEl.side, side);
-        assert.deepEqual(threadEl.projectConfig, {foo: 'bar'});
-        assert.deepEqual(threadEl.comments, comments);
-      }
-
-      var line = new GrDiffLine(GrDiffLine.Type.BOTH);
-      line.beforeNumber = 5;
-      line.afterNumber = 5;
-      var threadEl = builder._commentThreadForLine(line);
-      checkThreadProps(threadEl, '3', 'REVISION',
-          [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
-
-      threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
-      checkThreadProps(threadEl, '3', 'REVISION', [{id: 'r5', line: 5}]);
-
-      threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
-      checkThreadProps(threadEl, '3', 'PARENT', [{id: 'l5', line: 5}]);
-
-      builder._comments.meta.patchRange.basePatchNum = '1';
-
-      threadEl = builder._commentThreadForLine(line);
-      checkThreadProps(threadEl, '3', 'REVISION',
-          [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
-
-      threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
-      checkThreadProps(threadEl, '1', 'REVISION', [{id: 'l5', line: 5}]);
-
-      threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
-      checkThreadProps(threadEl, '3', 'REVISION', [{id: 'r5', line: 5}]);
-
-      builder._comments.meta.patchRange.basePatchNum = 'PARENT';
-
-      line = new GrDiffLine(GrDiffLine.Type.REMOVE);
-      line.beforeNumber = 5;
-      line.afterNumber = 5;
-      threadEl = builder._commentThreadForLine(line);
-      checkThreadProps(threadEl, '3', 'PARENT',
-          [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
-
-      line = new GrDiffLine(GrDiffLine.Type.ADD);
-      line.beforeNumber = 3;
-      line.afterNumber = 5;
-      threadEl = builder._commentThreadForLine(line);
-      checkThreadProps(threadEl, '3', 'REVISION',
-          [{id: 'l3', line: 3}, {id: 'r5', line: 5}]);
-    });
-
-    test('break up common diff chunks', function() {
-      builder._commentLocations = {
-        left: {1: true},
-        right: {10: true},
-      };
-      var lineNums = {
-        left: 0,
-        right: 0,
-      };
-      var content = [
-        {
-          ab: [
-            'Copyright (C) 2015 The Android Open Source Project',
-            '',
-            'Licensed under the Apache License, Version 2.0 (the "License");',
-            'you may not use this file except in compliance with the License.',
-            'You may obtain a copy of the License at',
-            '',
-            'http://www.apache.org/licenses/LICENSE-2.0',
-            '',
-            'Unless required by applicable law or agreed to in writing, ',
-            'software distributed under the License is distributed on an ',
-            '"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, ',
-            'either express or implied. See the License for the specific ',
-            'language governing permissions and limitations under the License.',
-          ]
-        }
-      ];
-      var result = builder._splitCommonGroupsWithComments(content, lineNums);
-      assert.deepEqual(result, [
-        {
-          ab: ['Copyright (C) 2015 The Android Open Source Project'],
-        },
-        {
-          ab: [
-            '',
-            'Licensed under the Apache License, Version 2.0 (the "License");',
-            'you may not use this file except in compliance with the License.',
-            'You may obtain a copy of the License at',
-            '',
-            'http://www.apache.org/licenses/LICENSE-2.0',
-            '',
-            'Unless required by applicable law or agreed to in writing, ',
-          ]
-        },
-        {
-          ab: ['software distributed under the License is distributed on an '],
-        },
-        {
-          ab: [
-            '"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, ',
-            'either express or implied. See the License for the specific ',
-            'language governing permissions and limitations under the License.',
-          ]
-        }
-      ]);
-    });
-
-    test('intraline normalization', function() {
-      // The content and highlights are in the format returned by the Gerrit
-      // REST API.
-      var content = [
-        '      <section class="summary">',
-        '        <gr-linked-text content="' +
-            '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
-        '      </section>',
-      ];
-      var highlights = [
-        [31, 34], [42, 26]
-      ];
-      var results = GrDiffBuilder.prototype._normalizeIntralineHighlights(
-          content, highlights);
-      assert.deepEqual(results, [
-        {
-          contentIndex: 0,
-          startIndex: 31,
-        },
-        {
-          contentIndex: 1,
-          startIndex: 0,
-          endIndex: 33,
-        },
-        {
-          contentIndex: 1,
-          startIndex: 75,
-        },
-        {
-          contentIndex: 2,
-          startIndex: 0,
-          endIndex: 6,
-        }
-      ]);
-
-      content = [
-        '        this._path = value.path;',
-        '',
-        '        // When navigating away from the page, there is a possibility that the',
-        '        // patch number is no longer a part of the URL (say when navigating to',
-        '        // the top-level change info view) and therefore undefined in `params`.',
-        '        if (!this._patchRange.patchNum) {',
-      ];
-      highlights = [
-        [14, 17],
-        [11, 70],
-        [12, 67],
-        [12, 67],
-        [14, 29],
-      ];
-      results = GrDiffBuilder.prototype._normalizeIntralineHighlights(content,
-          highlights);
-      assert.deepEqual(results, [
-        {
-          contentIndex: 0,
-          startIndex: 14,
-          endIndex: 31,
-        },
-        {
-          contentIndex: 2,
-          startIndex: 8,
-          endIndex: 78,
-        },
-        {
-          contentIndex: 3,
-          startIndex: 11,
-          endIndex: 78,
-        },
-        {
-          contentIndex: 4,
-          startIndex: 11,
-          endIndex: 78,
-        },
-        {
-          contentIndex: 5,
-          startIndex: 12,
-          endIndex: 41,
-        }
-      ]);
-    });
-
-    suite('rendering', function() {
-      var content;
-      var outputEl;
-
-      setup(function() {
-        var prefs = {
-          line_length: 10,
-          show_tabs: true,
-          tab_size: 4,
-          context: -1
-        };
-        content = [
-          {ab: []},
-          {a: ['all work and no play make andybons a dull boy']},
-          {ab: []},
-          {b: ['elgoog elgoog elgoog']},
-          {ab: []},
-        ];
-        outputEl = document.createElement('out');
-        builder =
-            new GrDiffBuilder(
-                {content: content}, {left: [], right: []}, prefs, outputEl);
-        builder.buildSectionElement = function(group) {
-          var section = document.createElement('stub');
-          section.textContent = group.lines.reduce(function(acc, line) {
-            return acc + line.text;
-          }, '');
-          return section;
-        };
-        builder.emitDiff();
-      });
-
-      test('renderSection', function() {
-        var section = outputEl.querySelector('stub:nth-of-type(2)');
-        var prevInnerHTML = section.innerHTML;
-        section.innerHTML = 'wiped';
-        builder.renderSection(section);
-        section = outputEl.querySelector('stub:nth-of-type(2)');
-        assert.equal(section.innerHTML, prevInnerHTML);
-      });
-
-      test('getSectionsByLineRange', function() {
-        var section = outputEl.querySelector('stub:nth-of-type(2)');
-        var sections = builder.getSectionsByLineRange(1, 1, 'left');
-        assert.equal(sections.length, 1);
-        assert.strictEqual(sections[0], section);
-      });
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 1913270..cfe5d66 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -18,6 +18,7 @@
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
+<link rel="import" href="../gr-diff-builder/gr-diff-builder.html">
 
 <dom-module id="gr-diff">
   <template>
@@ -153,15 +154,18 @@
         on-tap="_handleTap"
         on-mousedown="_handleMouseDown"
         on-copy="_handleCopy">
-      <table id="diffTable"></table>
+      <gr-diff-builder
+         id="diffBuilder"
+         view-mode="[[viewMode]]"
+         is-image-diff="[[isImageDiff]]"
+         base-image="[[_baseImage]]"
+         revision-image="[[_revisionImage]]">
+        <table id="diffTable"></table>
+      </gr-diff-builder>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-diff-line.js"></script>
   <script src="gr-diff-group.js"></script>
-  <script src="gr-diff-builder.js"></script>
-  <script src="gr-diff-builder-side-by-side.js"></script>
-  <script src="gr-diff-builder-unified.js"></script>
-  <script src="gr-diff-builder-image.js"></script>
   <script src="gr-diff.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 0056808..ea64581 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -27,12 +27,6 @@
   Polymer({
     is: 'gr-diff',
 
-    /**
-     * Fired when the diff is rendered.
-     *
-     * @event render
-     */
-
     properties: {
       changeNum: String,
       patchRange: Object,
@@ -59,7 +53,6 @@
         value: DiffViewMode.SIDE_BY_SIDE,
       },
       _diff: Object,
-      _diffBuilder: Object,
       _selectionSide: {
         type: String,
         observer: '_selectionSideChanged',
@@ -195,7 +188,7 @@
       var el = Polymer.dom(e).rootTarget;
 
       if (el.classList.contains('showContext')) {
-        this._showContext(e.detail.groups, e.detail.section);
+        this.$.diffBuilder.showContext(e.detail.groups, e.detail.section);
       } else if (el.classList.contains('lineNum')) {
         this.addDraftAtLine(el);
       }
@@ -223,8 +216,8 @@
             patchNum = this.patchRange.basePatchNum;
           }
         }
-        threadEl = this._builder.createCommentThread(this.changeNum, patchNum,
-            this.path, side, this.projectConfig);
+        threadEl = this.$.diffBuilder.createCommentThread(
+            this.changeNum, patchNum, this.path, side, this.projectConfig);
         contentEl.appendChild(threadEl);
       }
       threadEl.addDraft(opt_lineNum);
@@ -363,25 +356,6 @@
       return text;
     },
 
-    _showContext: function(newGroups, sectionEl) {
-      var groups = this._builder._groups;
-      // TODO(viktard): Polyfill findIndex for IE10.
-      var contextIndex = groups.findIndex(function(group) {
-        return group.element == sectionEl;
-      });
-
-      groups.splice.apply(groups, [contextIndex, 1].concat(newGroups));
-
-      newGroups.forEach(function(newGroup) {
-        this._builder.emitGroup(newGroup, sectionEl);
-      }.bind(this));
-      sectionEl.parentNode.removeChild(sectionEl);
-
-      this.async(function() {
-        this.fire('render', null, {bubbles: false});
-      }.bind(this), 1);
-    },
-
     _prefsChanged: function(prefsChangeRecord) {
       var prefs = prefsChangeRecord.base;
       this.customStyle['--content-width'] = prefs.line_length + 'ch';
@@ -393,17 +367,7 @@
     },
 
     _render: function() {
-      this._builder =
-          this._getDiffBuilder(this._diff, this._comments, this.prefs);
-      this._renderDiff();
-    },
-
-    _renderDiff: function() {
-      this._clearDiffContent();
-      this._builder.emitDiff();
-      this.async(function() {
-        this.fire('render', null, {bubbles: false});
-      }, 1);
+      this.$.diffBuilder.render(this._diff, this._comments, this.prefs);
     },
 
     _clearDiffContent: function() {
@@ -508,19 +472,6 @@
           this.changeNum, this._diff, this.patchRange);
     },
 
-    _getDiffBuilder: function(diff, comments, prefs) {
-      if (this.isImageDiff) {
-        return new GrDiffBuilderImage(diff, comments, prefs, this.$.diffTable,
-            this._baseImage, this._revisionImage);
-      } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
-        return new GrDiffBuilderSideBySide(diff, comments, prefs,
-            this.$.diffTable);
-      } else if (this.viewMode === DiffViewMode.UNIFIED) {
-        return new GrDiffBuilderUnified(diff, comments, prefs,
-            this.$.diffTable);
-      }
-      throw Error('Unsupported diff view mode: ' + this.viewMode);
-    },
 
     _projectConfigChanged: function(projectConfig) {
       var threadEls = this._getCommentThreads();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index aec32b6..613c7fa 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -226,8 +226,7 @@
         var rendered = function() {
           // Recognizes that it should be an image diff.
           assert.isTrue(element.isImageDiff);
-          assert.instanceOf(element._getDiffBuilder(element._diff,
-              element._comments, element.prefs), GrDiffBuilderImage);
+          assert.instanceOf(element.$.diffBuilder._builder, GrDiffBuilderImage);
 
           // Left image rendered with the parent commit's version of the file.
           var leftInmage = element.$.diffTable.querySelector('td.left img');
@@ -397,30 +396,5 @@
         });
       });
     });
-
-    suite('renderDiff', function() {
-      setup(function(done) {
-        sinon.stub(element, 'fire');
-        element._builder = {
-          emitDiff: sinon.stub(),
-        };
-        element._renderDiff();
-        flush(function() {
-          done();
-        });
-      });
-
-      teardown(function() {
-        element.fire.restore();
-      });
-
-      test('fires render', function() {
-        assert(element.fire.calledWithExactly(
-            'render', null, {bubbles: false}));
-      });
-      test('calls emitDiff on builder', function() {
-        assert(element._builder.emitDiff.calledOnce);
-      });
-    });
   });
 </script>