Merge "Refactor `GrAnnotation`"
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts
index e9076aa..9478d13 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer.ts
@@ -6,7 +6,10 @@
 import {DiffLayer} from '../../../types/types';
 import {GrDiffLine, Side, TokenHighlightListener} from '../../../api/diff';
 import {assertIsDefined} from '../../../utils/common-util';
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {
+  GrAnnotationImpl,
+  getStringLength,
+} from '../gr-diff-highlight/gr-annotation';
 import {debounce, DelayedTask} from '../../../utils/async-util';
 
 import {getLineElByChild, getSideByLineEl} from '../gr-diff/gr-diff-utils';
@@ -147,8 +150,8 @@
       // This is to correctly count surrogate pairs in text and token.
       // If the index calculation becomes a hotspot, we could precompute a code
       // unit to code point index map for text before iterating over the results
-      const index = GrAnnotation.getStringLength(text.slice(0, match.index));
-      const length = GrAnnotation.getStringLength(token);
+      const index = getStringLength(text.slice(0, match.index));
+      const length = getStringLength(token);
 
       atLeastOneTokenMatched = true;
       const highlightTypeClass =
@@ -158,7 +161,7 @@
       // We add the TOKEN_TEXT_PREFIX class so that we can look up the token later easily
       // even if the token element was split up into multiple smaller nodes.
       // All parts of a single token will share a common TOKEN_INDEX_PREFIX class within the line of code.
-      GrAnnotation.annotateElement(
+      GrAnnotationImpl.annotateElement(
         el,
         index,
         length,
@@ -345,7 +348,7 @@
       start_line: line,
       start_column: index + 1, // 1-based inclusive
       end_line: line,
-      end_column: index + GrAnnotation.getStringLength(token), // 1-based inclusive
+      end_column: index + getStringLength(token), // 1-based inclusive
     };
     this.tokenHighlightListener({token, element, side, range});
   }
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer_test.ts
index 5651dcf..8d0050f 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/token-highlight-layer_test.ts
@@ -11,7 +11,7 @@
 } from '../../../api/diff';
 import {GrDiffLine} from '../gr-diff/gr-diff-line';
 import {HOVER_DELAY_MS, TokenHighlightLayer} from './token-highlight-layer';
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {GrAnnotationImpl} from '../gr-diff-highlight/gr-annotation';
 import {html, render} from 'lit';
 import {_testOnly_allTasks} from '../../../utils/async-util';
 import {queryAndAssert} from '../../../test/test-utils';
@@ -123,10 +123,13 @@
     }
 
     test('annotate adds css token', () => {
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
       const el = createLine('these are words');
       annotate(el);
-      assert.isTrue(annotateElementStub.calledThrice);
+      assert.equal(annotateElementStub.callCount, 3);
       assertAnnotation(annotateElementStub.args[0], {
         parent: el,
         offset: 0,
@@ -148,7 +151,10 @@
     });
 
     test('annotate adds css tokens w/ emojis', () => {
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
       const el = createLine('these 💩 are 👨‍👩‍👧‍👦 words');
 
       annotate(el);
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation.ts
index 5669bcf..bddfcac 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation.ts
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {getSanitizeDOMValue} from '@polymer/polymer/lib/utils/settings';
+import {GrAnnotation} from '../../../api/diff';
 
 // TODO(wyatta): refactor this to be <MARK> rather than <HL>.
 const ANNOTATION_TAG = 'HL';
@@ -11,268 +12,271 @@
 // Astral code point as per https://mathiasbynens.be/notes/javascript-unicode
 const REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
 
-export const GrAnnotation = {
-  /**
-   * The DOM API textContent.length calculation is broken when the text
-   * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode .
-   *
-   */
-  getLength(node: Node) {
-    if (node instanceof Comment) return 0;
-    return GrAnnotation.getStringLength(node.textContent || '');
-  },
+/**
+ * The DOM API textContent.length calculation is broken when the text
+ * contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode .
+ */
+export function getLength(node: Node) {
+  if (node instanceof Comment) return 0;
+  return getStringLength(node.textContent || '');
+}
 
-  /**
-   * Returns the number of Unicode code points in the given string
-   *
-   * This is not necessarily the same as the number of visible symbols.
-   * See https://mathiasbynens.be/notes/javascript-unicode for more details.
-   */
-  getStringLength(str: string) {
-    return [...str].length;
-  },
+/**
+ * Returns the number of Unicode code points in the given string
+ *
+ * This is not necessarily the same as the number of visible symbols.
+ * See https://mathiasbynens.be/notes/javascript-unicode for more details.
+ */
+export function getStringLength(str: string) {
+  return [...str].length;
+}
 
-  /**
-   * Annotates the [offset, offset+length) text segment in the parent with the
-   * element definition provided as arguments.
-   *
-   * @param parent the node whose contents will be annotated.
-   * If parent is Text then parent.parentNode must not be null
-   * @param offset the 0-based offset from which the annotation will
-   * start.
-   * @param length of the annotated text.
-   * @param elementSpec the spec to create the
-   * annotating element.
-   */
-  annotateWithElement(
-    parent: Node,
-    offset: number,
-    length: number,
-    elSpec: ElementSpec
+/**
+ * Annotates the [offset, offset+length) text segment in the parent with the
+ * element definition provided as arguments.
+ *
+ * @param parent the node whose contents will be annotated.
+ * If parent is Text then parent.parentNode must not be null
+ * @param offset the 0-based offset from which the annotation will
+ * start.
+ * @param length of the annotated text.
+ * @param elementSpec the spec to create the
+ * annotating element.
+ */
+export function annotateWithElement(
+  parent: Node,
+  offset: number,
+  length: number,
+  elSpec: ElementSpec
+) {
+  const tagName = elSpec.tagName;
+  const attributes = elSpec.attributes || {};
+  let childNodes: Node[];
+
+  if (parent instanceof Element) {
+    childNodes = Array.from(parent.childNodes);
+  } else if (parent instanceof Text) {
+    childNodes = [parent];
+    parent = parent.parentNode!;
+  } else {
+    return;
+  }
+
+  const nestedNodes: Node[] = [];
+  for (let node of childNodes) {
+    const initialNodeLength = getLength(node);
+    // If the current node is completely before the offset.
+    if (offset > 0 && initialNodeLength <= offset) {
+      offset -= initialNodeLength;
+      continue;
+    }
+
+    if (offset > 0) {
+      node = splitNode(node, offset);
+      offset = 0;
+    }
+    if (getLength(node) > length) {
+      splitNode(node, length);
+    }
+    nestedNodes.push(node);
+
+    length -= getLength(node);
+    if (!length) break;
+  }
+
+  const wrapper = document.createElement(tagName);
+  const sanitizer = getSanitizeDOMValue();
+  for (let [name, value] of Object.entries(attributes)) {
+    if (!value) continue;
+    if (sanitizer) {
+      value = sanitizer(value, name, 'attribute', wrapper) as string;
+    }
+    wrapper.setAttribute(name, value);
+  }
+  for (const inner of nestedNodes) {
+    parent.replaceChild(wrapper, inner);
+    wrapper.appendChild(inner);
+  }
+}
+
+/**
+ * Surrounds the element's text at specified range in an ANNOTATION_TAG
+ * element. If the element has child elements, the range is split and
+ * applied as deeply as possible.
+ */
+export function annotateElement(
+  parent: HTMLElement,
+  offset: number,
+  length: number,
+  cssClass: string
+) {
+  const nodes: Array<HTMLElement | Text> = [].slice.apply(parent.childNodes);
+  let nodeLength;
+  let subLength;
+
+  for (const node of nodes) {
+    nodeLength = getLength(node);
+
+    // If the current node is completely before the offset.
+    if (nodeLength <= offset) {
+      offset -= nodeLength;
+      continue;
+    }
+
+    // Sublength is the annotation length for the current node.
+    subLength = Math.min(length, nodeLength - offset);
+
+    if (node instanceof Text) {
+      _annotateText(node, offset, subLength, cssClass);
+    } else if (node instanceof Element) {
+      annotateElement(node, offset, subLength, cssClass);
+    }
+
+    // If there is still more to annotate, then shift the indices, otherwise
+    // work is done, so break the loop.
+    if (subLength < length) {
+      length -= subLength;
+      offset = 0;
+    } else {
+      break;
+    }
+  }
+}
+
+/**
+ * Wraps node in annotation tag with cssClass, replacing the node in DOM.
+ */
+function wrapInHighlight(node: Element | Text, cssClass: string) {
+  let hl;
+  if (!(node instanceof Text) && node.tagName === ANNOTATION_TAG) {
+    hl = node;
+    hl.classList.add(cssClass);
+  } else {
+    hl = document.createElement(ANNOTATION_TAG);
+    hl.className = cssClass;
+    if (node.parentElement) node.parentElement.replaceChild(hl, node);
+    hl.appendChild(node);
+  }
+  return hl;
+}
+
+/**
+ * Splits Text Node and wraps it in hl with cssClass.
+ * Wraps trailing part after split, tailing one if firstPart is true.
+ */
+function splitAndWrapInHighlight(
+  node: Text,
+  offset: number,
+  cssClass: string,
+  firstPart?: boolean
+) {
+  if (
+    (getLength(node) === offset && firstPart) ||
+    (offset === 0 && !firstPart)
   ) {
-    const tagName = elSpec.tagName;
-    const attributes = elSpec.attributes || {};
-    let childNodes: Node[];
+    return wrapInHighlight(node, cssClass);
+  }
+  if (firstPart) {
+    splitNode(node, offset);
+    // Node points to first part of the Text, second one is sibling.
+  } else {
+    // if node is Text then splitNode will return a Text
+    node = splitNode(node, offset) as Text;
+  }
+  return wrapInHighlight(node, cssClass);
+}
 
-    if (parent instanceof Element) {
-      childNodes = Array.from(parent.childNodes);
-    } else if (parent instanceof Text) {
-      childNodes = [parent];
-      parent = parent.parentNode!;
-    } else {
-      return;
+/**
+ * Splits Node at offset.
+ * If Node is Element, it's cloned and the node at offset is split too.
+ */
+function splitNode(element: Node, offset: number) {
+  if (element instanceof Text) {
+    return splitTextNode(element, offset);
+  }
+  const tail = element.cloneNode(false);
+
+  if (element.parentElement)
+    element.parentElement.insertBefore(tail, element.nextSibling);
+  // Skip nodes before offset.
+  let node = element.firstChild;
+  while (node && (getLength(node) <= offset || getLength(node) === 0)) {
+    offset -= getLength(node);
+    node = node.nextSibling;
+  }
+  if (node && getLength(node) > offset) {
+    tail.appendChild(splitNode(node, offset));
+  }
+  while (node && node.nextSibling) {
+    tail.appendChild(node.nextSibling);
+  }
+  return tail;
+}
+
+/**
+ * Node.prototype.splitText Unicode-valid alternative.
+ *
+ * DOM Api for splitText() is broken for Unicode:
+ * https://mathiasbynens.be/notes/javascript-unicode
+ *
+ * @return Trailing Text Node.
+ */
+function splitTextNode(node: Text, offset: number) {
+  if (node.textContent?.match(REGEX_ASTRAL_SYMBOL)) {
+    const head = Array.from(node.textContent);
+    const tail = head.splice(offset);
+    const parent = node.parentNode;
+
+    // Split the content of the original node.
+    node.textContent = head.join('');
+
+    const tailNode = document.createTextNode(tail.join(''));
+    if (parent) {
+      parent.insertBefore(tailNode, node.nextSibling);
     }
+    return tailNode;
+  } else {
+    return node.splitText(offset);
+  }
+}
 
-    const nestedNodes: Node[] = [];
-    for (let node of childNodes) {
-      const initialNodeLength = GrAnnotation.getLength(node);
-      // If the current node is completely before the offset.
-      if (offset > 0 && initialNodeLength <= offset) {
-        offset -= initialNodeLength;
-        continue;
-      }
+function _annotateText(
+  node: Text,
+  offset: number,
+  length: number,
+  cssClass: string
+) {
+  const nodeLength = getLength(node);
 
-      if (offset > 0) {
-        node = GrAnnotation.splitNode(node, offset);
-        offset = 0;
-      }
-      if (GrAnnotation.getLength(node) > length) {
-        GrAnnotation.splitNode(node, length);
-      }
-      nestedNodes.push(node);
+  // There are four cases:
+  //  1) Entire node is highlighted.
+  //  2) Highlight is at the start.
+  //  3) Highlight is at the end.
+  //  4) Highlight is in the middle.
 
-      length -= GrAnnotation.getLength(node);
-      if (!length) break;
-    }
+  if (offset === 0 && nodeLength === length) {
+    // Case 1.
+    wrapInHighlight(node, cssClass);
+  } else if (offset === 0) {
+    // Case 2.
+    splitAndWrapInHighlight(node, length, cssClass, true);
+  } else if (offset + length === nodeLength) {
+    // Case 3
+    splitAndWrapInHighlight(node, offset, cssClass, false);
+  } else {
+    // Case 4
+    splitAndWrapInHighlight(
+      splitTextNode(node, offset),
+      length,
+      cssClass,
+      true
+    );
+  }
+}
 
-    const wrapper = document.createElement(tagName);
-    const sanitizer = getSanitizeDOMValue();
-    for (let [name, value] of Object.entries(attributes)) {
-      if (!value) continue;
-      if (sanitizer) {
-        value = sanitizer(value, name, 'attribute', wrapper) as string;
-      }
-      wrapper.setAttribute(name, value);
-    }
-    for (const inner of nestedNodes) {
-      parent.replaceChild(wrapper, inner);
-      wrapper.appendChild(inner);
-    }
-  },
-
-  /**
-   * Surrounds the element's text at specified range in an ANNOTATION_TAG
-   * element. If the element has child elements, the range is split and
-   * applied as deeply as possible.
-   */
-  annotateElement(
-    parent: HTMLElement,
-    offset: number,
-    length: number,
-    cssClass: string
-  ) {
-    const nodes: Array<HTMLElement | Text> = [].slice.apply(parent.childNodes);
-    let nodeLength;
-    let subLength;
-
-    for (const node of nodes) {
-      nodeLength = GrAnnotation.getLength(node);
-
-      // If the current node is completely before the offset.
-      if (nodeLength <= offset) {
-        offset -= nodeLength;
-        continue;
-      }
-
-      // Sublength is the annotation length for the current node.
-      subLength = Math.min(length, nodeLength - offset);
-
-      if (node instanceof Text) {
-        GrAnnotation._annotateText(node, offset, subLength, cssClass);
-      } else if (node instanceof Element) {
-        GrAnnotation.annotateElement(node, offset, subLength, cssClass);
-      }
-
-      // If there is still more to annotate, then shift the indices, otherwise
-      // work is done, so break the loop.
-      if (subLength < length) {
-        length -= subLength;
-        offset = 0;
-      } else {
-        break;
-      }
-    }
-  },
-
-  /**
-   * Wraps node in annotation tag with cssClass, replacing the node in DOM.
-   */
-  wrapInHighlight(node: Element | Text, cssClass: string) {
-    let hl;
-    if (!(node instanceof Text) && node.tagName === ANNOTATION_TAG) {
-      hl = node;
-      hl.classList.add(cssClass);
-    } else {
-      hl = document.createElement(ANNOTATION_TAG);
-      hl.className = cssClass;
-      if (node.parentElement) node.parentElement.replaceChild(hl, node);
-      hl.appendChild(node);
-    }
-    return hl;
-  },
-
-  /**
-   * Splits Text Node and wraps it in hl with cssClass.
-   * Wraps trailing part after split, tailing one if firstPart is true.
-   */
-  splitAndWrapInHighlight(
-    node: Text,
-    offset: number,
-    cssClass: string,
-    firstPart?: boolean
-  ) {
-    if (
-      (GrAnnotation.getLength(node) === offset && firstPart) ||
-      (offset === 0 && !firstPart)
-    ) {
-      return GrAnnotation.wrapInHighlight(node, cssClass);
-    }
-    if (firstPart) {
-      GrAnnotation.splitNode(node, offset);
-      // Node points to first part of the Text, second one is sibling.
-    } else {
-      // if node is Text then splitNode will return a Text
-      node = GrAnnotation.splitNode(node, offset) as Text;
-    }
-    return GrAnnotation.wrapInHighlight(node, cssClass);
-  },
-
-  /**
-   * Splits Node at offset.
-   * If Node is Element, it's cloned and the node at offset is split too.
-   */
-  splitNode(element: Node, offset: number) {
-    if (element instanceof Text) {
-      return GrAnnotation.splitTextNode(element, offset);
-    }
-    const tail = element.cloneNode(false);
-
-    if (element.parentElement)
-      element.parentElement.insertBefore(tail, element.nextSibling);
-    // Skip nodes before offset.
-    let node = element.firstChild;
-    while (
-      node &&
-      (GrAnnotation.getLength(node) <= offset ||
-        GrAnnotation.getLength(node) === 0)
-    ) {
-      offset -= GrAnnotation.getLength(node);
-      node = node.nextSibling;
-    }
-    if (node && GrAnnotation.getLength(node) > offset) {
-      tail.appendChild(GrAnnotation.splitNode(node, offset));
-    }
-    while (node && node.nextSibling) {
-      tail.appendChild(node.nextSibling);
-    }
-    return tail;
-  },
-
-  /**
-   * Node.prototype.splitText Unicode-valid alternative.
-   *
-   * DOM Api for splitText() is broken for Unicode:
-   * https://mathiasbynens.be/notes/javascript-unicode
-   *
-   * @return Trailing Text Node.
-   */
-  splitTextNode(node: Text, offset: number) {
-    if (node.textContent?.match(REGEX_ASTRAL_SYMBOL)) {
-      const head = Array.from(node.textContent);
-      const tail = head.splice(offset);
-      const parent = node.parentNode;
-
-      // Split the content of the original node.
-      node.textContent = head.join('');
-
-      const tailNode = document.createTextNode(tail.join(''));
-      if (parent) {
-        parent.insertBefore(tailNode, node.nextSibling);
-      }
-      return tailNode;
-    } else {
-      return node.splitText(offset);
-    }
-  },
-
-  _annotateText(node: Text, offset: number, length: number, cssClass: string) {
-    const nodeLength = GrAnnotation.getLength(node);
-
-    // There are four cases:
-    //  1) Entire node is highlighted.
-    //  2) Highlight is at the start.
-    //  3) Highlight is at the end.
-    //  4) Highlight is in the middle.
-
-    if (offset === 0 && nodeLength === length) {
-      // Case 1.
-      GrAnnotation.wrapInHighlight(node, cssClass);
-    } else if (offset === 0) {
-      // Case 2.
-      GrAnnotation.splitAndWrapInHighlight(node, length, cssClass, true);
-    } else if (offset + length === nodeLength) {
-      // Case 3
-      GrAnnotation.splitAndWrapInHighlight(node, offset, cssClass, false);
-    } else {
-      // Case 4
-      GrAnnotation.splitAndWrapInHighlight(
-        GrAnnotation.splitTextNode(node, offset),
-        length,
-        cssClass,
-        true
-      );
-    }
-  },
+export const GrAnnotationImpl: GrAnnotation = {
+  annotateElement,
+  annotateWithElement,
 };
 
 /**
@@ -283,3 +287,8 @@
   tagName: string;
   attributes?: {[attributeName: string]: string | undefined};
 }
+
+export const TEST_ONLY = {
+  _annotateText,
+  splitTextNode,
+};
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.ts
index 3e1ce66..15a6a15 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-annotation_test.ts
@@ -4,7 +4,12 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import '../../../test/common-test-setup';
-import {GrAnnotation} from './gr-annotation';
+import {
+  TEST_ONLY,
+  annotateElement,
+  annotateWithElement,
+  getStringLength,
+} from './gr-annotation';
 import {
   getSanitizeDOMValue,
   setSanitizeDOMValue,
@@ -27,7 +32,7 @@
   });
 
   test('_annotateText length:0 offset:0', () => {
-    GrAnnotation._annotateText(textNode, 0, 0, 'foobar');
+    TEST_ONLY._annotateText(textNode, 0, 0, 'foobar');
 
     assert.equal(parent.textContent, str);
     assert.equal(
@@ -37,7 +42,7 @@
   });
 
   test('_annotateText length:0 offset:1', () => {
-    GrAnnotation._annotateText(textNode, 1, 0, 'foobar');
+    TEST_ONLY._annotateText(textNode, 1, 0, 'foobar');
 
     assert.equal(parent.textContent, str);
     assert.equal(
@@ -47,7 +52,7 @@
   });
 
   test('_annotateText length:0 offset:str.length', () => {
-    GrAnnotation._annotateText(textNode, str.length, 0, 'foobar');
+    TEST_ONLY._annotateText(textNode, str.length, 0, 'foobar');
 
     assert.equal(parent.textContent, str);
     assert.equal(
@@ -57,7 +62,7 @@
   });
 
   test('_annotateText Case 1', () => {
-    GrAnnotation._annotateText(textNode, 0, str.length, 'foobar');
+    TEST_ONLY._annotateText(textNode, 0, str.length, 'foobar');
 
     assert.equal(parent.textContent, str);
     assert.equal(
@@ -67,7 +72,7 @@
   });
 
   test('_annotateText Case 2', () => {
-    GrAnnotation._annotateText(textNode, 0, 12, 'foobar');
+    TEST_ONLY._annotateText(textNode, 0, 12, 'foobar');
 
     assert.equal(parent.textContent, str);
     assert.equal(
@@ -77,7 +82,7 @@
   });
 
   test('_annotateText Case 3', () => {
-    GrAnnotation._annotateText(textNode, 12, str.length - 12, 'foobar');
+    TEST_ONLY._annotateText(textNode, 12, str.length - 12, 'foobar');
 
     assert.equal(parent.textContent, str);
     assert.equal(
@@ -90,7 +95,7 @@
     const index = str.indexOf('dolor');
     const length = 'dolor '.length;
 
-    GrAnnotation._annotateText(textNode, index, length, 'foobar');
+    TEST_ONLY._annotateText(textNode, index, length, 'foobar');
 
     assert.equal(parent.textContent, str);
     assert.equal(
@@ -104,7 +109,7 @@
 
     // Apply the layers successively.
     layers.forEach((layer, i) => {
-      GrAnnotation.annotateElement(
+      annotateElement(
         parent,
         str.indexOf(layer),
         layer.length,
@@ -129,13 +134,13 @@
 
     // Non-unicode path:
     node = document.createTextNode(helloString + asciiString);
-    tail = GrAnnotation.splitTextNode(node, helloString.length);
+    tail = TEST_ONLY.splitTextNode(node, helloString.length);
     assert(node.textContent, helloString);
     assert(tail.textContent, asciiString);
 
     // Unicdoe path:
     node = document.createTextNode(helloString + unicodeString);
-    tail = GrAnnotation.splitTextNode(node, helloString.length);
+    tail = TEST_ONLY.splitTextNode(node, helloString.length);
     assert(node.textContent, helloString);
     assert(tail.textContent, unicodeString);
   });
@@ -166,7 +171,7 @@
       const length = 10;
       const container = document.createElement('div');
       container.textContent = fullText;
-      GrAnnotation.annotateWithElement(container, 1, length, {
+      annotateWithElement(container, 1, length, {
         tagName: 'test-wrapper',
       });
 
@@ -180,8 +185,8 @@
       const length = 10;
       const container = document.createElement('div');
       container.textContent = fullText;
-      GrAnnotation.annotateElement(container, 5, length, 'testclass');
-      GrAnnotation.annotateWithElement(container, 1, length, {
+      annotateElement(container, 5, length, 'testclass');
+      annotateWithElement(container, 1, length, {
         tagName: 'test-wrapper',
       });
 
@@ -201,7 +206,7 @@
       const length = 10;
       const container = document.createElement('div');
       container.textContent = fullText;
-      GrAnnotation.annotateWithElement(container.childNodes[0], 1, length, {
+      annotateWithElement(container.childNodes[0], 1, length, {
         tagName: 'test-wrapper',
       });
 
@@ -216,7 +221,7 @@
       container.appendChild(document.createTextNode('0123456789'));
       container.appendChild(document.createElement('span'));
       container.appendChild(document.createTextNode('0123456789'));
-      GrAnnotation.annotateWithElement(container, 1, 10, {
+      annotateWithElement(container, 1, 10, {
         tagName: 'test-wrapper',
       });
 
@@ -233,7 +238,7 @@
       container.appendChild(document.createComment('comment2'));
       container.appendChild(document.createElement('span'));
       container.appendChild(document.createTextNode('0123456789'));
-      GrAnnotation.annotateWithElement(container, 1, 10, {
+      annotateWithElement(container, 1, 10, {
         tagName: 'test-wrapper',
       });
 
@@ -254,7 +259,7 @@
         'data-foo': 'bar',
         class: 'hello world',
       };
-      GrAnnotation.annotateWithElement(container, 1, length, {
+      annotateWithElement(container, 1, length, {
         tagName: 'test-wrapper',
         attributes,
       });
@@ -291,17 +296,17 @@
 
   suite('getStringLength', () => {
     test('ASCII characters are counted correctly', () => {
-      assert.equal(GrAnnotation.getStringLength('ASCII'), 5);
+      assert.equal(getStringLength('ASCII'), 5);
     });
 
     test('Unicode surrogate pairs count as one symbol', () => {
-      assert.equal(GrAnnotation.getStringLength('Unic💢de'), 7);
-      assert.equal(GrAnnotation.getStringLength('💢💢'), 2);
+      assert.equal(getStringLength('Unic💢de'), 7);
+      assert.equal(getStringLength('💢💢'), 2);
     });
 
     test('Grapheme clusters count as multiple symbols', () => {
-      assert.equal(GrAnnotation.getStringLength('man\u0303ana'), 7); // mañana
-      assert.equal(GrAnnotation.getStringLength('q\u0307\u0323'), 3); // q̣̇
+      assert.equal(getStringLength('man\u0303ana'), 7); // mañana
+      assert.equal(getStringLength('q\u0307\u0323'), 3); // q̣̇
     });
   });
 });
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
index 0d9250c..1cdfbc3 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -5,7 +5,7 @@
  */
 import '../../../styles/shared-styles';
 import '../gr-selection-action-box/gr-selection-action-box';
-import {GrAnnotation} from './gr-annotation';
+import {getLength} from './gr-annotation';
 import {normalize} from './gr-range-normalizer';
 import {strToClassName} from '../../../utils/dom-util';
 import {Side} from '../../../constants/constants';
@@ -508,7 +508,7 @@
     if (node instanceof Element && node.classList.contains('content')) {
       return this.getLength(queryAndAssert(node, '.contentText'));
     } else {
-      return GrAnnotation.getLength(node);
+      return getLength(node);
     }
   }
 }
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
index 98c0ab7..5db6db9 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor.ts
@@ -13,7 +13,7 @@
 import {Side} from '../../../constants/constants';
 import {debounce, DelayedTask} from '../../../utils/async-util';
 import {assert} from '../../../utils/common-util';
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {getStringLength} from '../gr-diff-highlight/gr-annotation';
 import {GrDiffLineType, LineNumber} from '../../../api/diff';
 import {FULL_CONTEXT, KeyLocations} from '../gr-diff/gr-diff-utils';
 
@@ -633,7 +633,7 @@
     intralineInfos: number[][]
   ): Highlights[] {
     // +1 to account for the \n that is not part of the rows passed here
-    const lineLengths = rows.map(r => GrAnnotation.getStringLength(r) + 1);
+    const lineLengths = rows.map(r => getStringLength(r) + 1);
 
     let rowIndex = 0;
     let idx = 0;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index 42cffdb..2834f08 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -79,7 +79,10 @@
 import {grDiffStyles} from './gr-diff-styles';
 import {getDiffLength} from '../../../utils/diff-util';
 import {GrCoverageLayer} from '../gr-coverage-layer/gr-coverage-layer';
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {
+  GrAnnotationImpl,
+  getStringLength,
+} from '../gr-diff-highlight/gr-annotation';
 import {
   GrDiffGroup,
   GrDiffGroupType,
@@ -1204,10 +1207,10 @@
           // If endIndex isn't present, continue to the end of the line.
           const endIndex =
             highlight.endIndex === undefined
-              ? GrAnnotation.getStringLength(line.text)
+              ? getStringLength(line.text)
               : highlight.endIndex;
 
-          GrAnnotation.annotateElement(
+          GrAnnotationImpl.annotateElement(
             contentEl,
             highlight.startIndex,
             endIndex - highlight.startIndex,
@@ -1255,11 +1258,9 @@
         if (match) {
           // Normalize string positions in case there is unicode before or
           // within the match.
-          const index = GrAnnotation.getStringLength(
-            line.text.substr(0, match.index)
-          );
-          const length = GrAnnotation.getStringLength(match[0]);
-          GrAnnotation.annotateElement(
+          const index = getStringLength(line.text.substr(0, match.index));
+          const length = getStringLength(match[0]);
+          GrAnnotationImpl.annotateElement(
             contentEl,
             index,
             length,
@@ -1460,7 +1461,7 @@
     // Skip forward by the length of the content
     pos += split[i].length;
 
-    GrAnnotation.annotateElement(contentEl, pos, 1, `gr-diff ${className}`);
+    GrAnnotationImpl.annotateElement(contentEl, pos, 1, `gr-diff ${className}`);
 
     pos++;
   }
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
index 88682d9..30f85cc 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
@@ -41,7 +41,10 @@
 import {fixture, html, assert} from '@open-wc/testing';
 import {createDefaultDiffPrefs} from '../../../constants/constants';
 import {GrDiffRow} from '../gr-diff-builder/gr-diff-row';
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {
+  GrAnnotationImpl,
+  getStringLength,
+} from '../gr-diff-highlight/gr-annotation';
 import {GrDiffLine} from './gr-diff-line';
 
 const DEFAULT_PREFS = createDefaultDiffPrefs();
@@ -4013,7 +4016,7 @@
         <div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
       `);
       str = el.textContent ?? '';
-      annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement');
+      annotateElementSpy = sinon.spy(GrAnnotationImpl, 'annotateElement');
       layer = element.createIntralineLayer();
     });
 
@@ -4125,7 +4128,7 @@
 
       const str0 = slice(str, 0, 6);
       const str1 = slice(str, 6);
-      const numHighlightedChars = GrAnnotation.getStringLength(str1);
+      const numHighlightedChars = getStringLength(str1);
 
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
 
@@ -4152,7 +4155,10 @@
     test('does nothing with empty line', () => {
       const l = line('');
       const el = document.createElement('div');
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
 
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
 
@@ -4164,7 +4170,10 @@
       const l = line(str);
       const el = document.createElement('div');
       el.textContent = str;
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
 
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
 
@@ -4176,7 +4185,10 @@
       const l = line(str);
       const el = document.createElement('div');
       el.textContent = str;
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
 
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
 
@@ -4195,7 +4207,10 @@
       const l = line(str);
       const el = document.createElement('div');
       el.textContent = str;
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
 
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
 
@@ -4207,7 +4222,10 @@
       const l = line(str);
       const el = document.createElement('div');
       el.textContent = str;
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
 
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
 
@@ -4231,7 +4249,10 @@
       const l = line(str);
       const el = document.createElement('div');
       el.textContent = str;
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
 
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
 
@@ -4259,7 +4280,10 @@
     test('does nothing with empty line', () => {
       const l = line('');
       const el = document.createElement('div');
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
       assert.isFalse(annotateElementStub.called);
     });
@@ -4269,7 +4293,10 @@
       const l = line(str);
       const el = document.createElement('div');
       el.textContent = str;
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
       assert.isFalse(annotateElementStub.called);
     });
@@ -4279,7 +4306,10 @@
       const l = line(str);
       const el = document.createElement('div');
       el.textContent = str;
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
       assert.isTrue(annotateElementStub.called);
       assert.equal(annotateElementStub.lastCall.args[1], 11);
@@ -4291,7 +4321,10 @@
       const l = line(str);
       const el = document.createElement('div');
       el.textContent = str;
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
       assert.isTrue(annotateElementStub.called);
       assert.equal(annotateElementStub.lastCall.args[1], 11);
@@ -4303,7 +4336,10 @@
       const l = line(str);
       const el = document.createElement('div');
       el.textContent = str;
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
       assert.isTrue(annotateElementStub.called);
       assert.equal(annotateElementStub.lastCall.args[1], 11);
@@ -4315,7 +4351,10 @@
       const l = line(str);
       const el = document.createElement('div');
       el.textContent = str;
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
       assert.isTrue(annotateElementStub.called);
       assert.equal(annotateElementStub.lastCall.args[1], 1);
@@ -4331,7 +4370,10 @@
       const l = line(str);
       const el = document.createElement('div');
       el.textContent = str;
-      const annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      const annotateElementStub = sinon.stub(
+        GrAnnotationImpl,
+        'annotateElement'
+      );
       layer.annotate(el, lineNumberEl, l, Side.LEFT);
       assert.isFalse(annotateElementStub.called);
     });
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
index e2837ab..24729ff 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
@@ -3,7 +3,7 @@
  * Copyright 2016 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {GrAnnotationImpl} from '../gr-diff-highlight/gr-annotation';
 import {GrDiffLine} from '../gr-diff/gr-diff-line';
 import {strToClassName} from '../../../utils/dom-util';
 import {Side} from '../../../constants/constants';
@@ -94,7 +94,7 @@
     }
 
     for (const range of ranges) {
-      GrAnnotation.annotateElement(
+      GrAnnotationImpl.annotateElement(
         el,
         range.start,
         range.end - range.start,
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
index b90d6f7..5bfd94d 100644
--- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
@@ -10,11 +10,11 @@
   CommentRangeLayer,
   GrRangedCommentLayer,
 } from './gr-ranged-comment-layer';
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
 import {GrDiffLine} from '../gr-diff/gr-diff-line';
 import {GrDiffLineType, Side} from '../../../api/diff';
 import {SinonStub} from 'sinon';
 import {assert} from '@open-wc/testing';
+import {GrAnnotationImpl} from '../gr-diff-highlight/gr-annotation';
 
 const rangeA: CommentRangeLayer = {
   side: Side.LEFT,
@@ -130,7 +130,7 @@
     }
 
     setup(() => {
-      annotateElementStub = sinon.stub(GrAnnotation, 'annotateElement');
+      annotateElementStub = sinon.stub(GrAnnotationImpl, 'annotateElement');
       el = document.createElement('div');
       el.setAttribute('data-side', Side.LEFT);
       line = new GrDiffLine(GrDiffLineType.BOTH);
diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
index baa2ab4..4e166ba 100644
--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
+++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
@@ -3,7 +3,7 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
+import {annotateElement} from '../gr-diff-highlight/gr-annotation';
 import {GrDiffLine} from '../gr-diff/gr-diff-line';
 import {DiffFileMetaInfo, DiffInfo} from '../../../types/diff';
 import {DiffLayer, DiffLayerListener} from '../../../types/types';
@@ -212,7 +212,7 @@
     for (const range of ranges) {
       if (!CLASS_SAFELIST.has(range.className)) continue;
       if (range.length === 0) continue;
-      GrAnnotation.annotateElement(
+      annotateElement(
         el,
         range.start,
         range.length,