Refactoring only: Split up the diff builder into two classes
GrDiffBuilder had two responsibilities:
1. Manage the data and the process for building a diff.
2. Create diff elements programmatically.
This change splits the builder up into two classes. It adds
`GrDiffBuilderLegacy` as a subclass of `GrDiffBuilder`.
The goal is to create alternative diff builders that are based on (Lit)
components instead of creating elements programmatically. For doing that
we will be re-using `GrDiffBuilder`, but replacing
`GrDiffBuilderLegacy`. That is why we need the split, although splitting
up large classes generally makes sense. :-)
We are also moving some stateless utilities into `gr-diff-utils.ts`.
Release-Notes: skip
Change-Id: Iaf92695139135edafdc81922fbf2435cfe2832c4
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
index 633b5f6..94a7a84 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
@@ -14,16 +14,63 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import {CommentRange} from '../../../types/common';
+import {BlameInfo, CommentRange} from '../../../types/common';
import {FILE, LineNumber} from './gr-diff-line';
import {Side} from '../../../constants/constants';
import {DiffInfo} from '../../../types/diff';
+import {
+ DiffPreferencesInfo,
+ DiffResponsiveMode,
+ RenderPreferences,
+} from '../../../api/diff';
+import {getBaseUrl} from '../../../utils/url-util';
+
+/**
+ * In JS, unicode code points above 0xFFFF occupy two elements of a string.
+ * For example '𐀏'.length is 2. An occurrence of such a code point is called a
+ * surrogate pair.
+ *
+ * This regex segments a string along tabs ('\t') and surrogate pairs, since
+ * these are two cases where '1 char' does not automatically imply '1 column'.
+ *
+ * TODO: For human languages whose orthographies use combining marks, this
+ * approach won't correctly identify the grapheme boundaries. In those cases,
+ * a grapheme consists of multiple code points that should count as only one
+ * character against the column limit. Getting that correct (if it's desired)
+ * is probably beyond the limits of a regex, but there are nonstandard APIs to
+ * do this, and proposed (but, as of Nov 2017, unimplemented) standard APIs.
+ *
+ * Further reading:
+ * On Unicode in JS: https://mathiasbynens.be/notes/javascript-unicode
+ * Graphemes: http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
+ * A proposed JS API: https://github.com/tc39/proposal-intl-segmenter
+ */
+const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
// If any line of the diff is more than the character limit, then disable
// syntax highlighting for the entire file.
export const SYNTAX_MAX_LINE_LENGTH = 500;
+export function getResponsiveMode(
+ prefs: DiffPreferencesInfo,
+ renderPrefs?: RenderPreferences
+): DiffResponsiveMode {
+ if (renderPrefs?.responsive_mode) {
+ return renderPrefs.responsive_mode;
+ }
+ // Backwards compatibility to the line_wrapping param.
+ if (prefs.line_wrapping) {
+ return 'FULL_RESPONSIVE';
+ }
+ return 'NONE';
+}
+
+export function isResponsive(responsiveMode: DiffResponsiveMode) {
+ return (
+ responsiveMode === 'FULL_RESPONSIVE' || responsiveMode === 'SHRINK_ONLY'
+ );
+}
+
/**
* Compare two ranges. Either argument may be falsy, but will only return
* true if both are falsy or if neither are falsy and have the same position
@@ -186,3 +233,139 @@
element.textContent = textContent;
return element;
}
+
+export function createLineBreak(mode: DiffResponsiveMode) {
+ return isResponsive(mode)
+ ? createElementDiff('wbr')
+ : createElementDiff('span', 'br');
+}
+
+/**
+ * Returns a <span> element holding a '\t' character, that will visually
+ * occupy |tabSize| many columns.
+ *
+ * @param tabSize The effective size of this tab stop.
+ */
+export function createTabWrapper(tabSize: number): HTMLElement {
+ // Force this to be a number to prevent arbitrary injection.
+ const result = createElementDiff('span', 'tab');
+ result.setAttribute(
+ 'style',
+ `tab-size: ${tabSize}; -moz-tab-size: ${tabSize};`
+ );
+ result.innerText = '\t';
+ return result;
+}
+
+/**
+ * Returns a 'div' element containing the supplied |text| as its innerText,
+ * with '\t' characters expanded to a width determined by |tabSize|, and the
+ * text wrapped at column |lineLimit|, which may be Infinity if no wrapping is
+ * desired.
+ *
+ * @param text The text to be formatted.
+ * @param responsiveMode The responsive mode of the diff.
+ * @param tabSize The width of each tab stop.
+ * @param lineLimit The column after which to wrap lines.
+ */
+export function formatText(
+ text: string,
+ responsiveMode: DiffResponsiveMode,
+ tabSize: number,
+ lineLimit: number
+): HTMLElement {
+ const contentText = createElementDiff('div', 'contentText');
+ contentText.ariaLabel = text;
+ let columnPos = 0;
+ let textOffset = 0;
+ for (const segment of text.split(REGEX_TAB_OR_SURROGATE_PAIR)) {
+ if (segment) {
+ // |segment| contains only normal characters. If |segment| doesn't fit
+ // entirely on the current line, append chunks of |segment| followed by
+ // line breaks.
+ let rowStart = 0;
+ let rowEnd = lineLimit - columnPos;
+ while (rowEnd < segment.length) {
+ contentText.appendChild(
+ document.createTextNode(segment.substring(rowStart, rowEnd))
+ );
+ contentText.appendChild(createLineBreak(responsiveMode));
+ columnPos = 0;
+ rowStart = rowEnd;
+ rowEnd += lineLimit;
+ }
+ // Append the last part of |segment|, which fits on the current line.
+ contentText.appendChild(
+ document.createTextNode(segment.substring(rowStart))
+ );
+ columnPos += segment.length - rowStart;
+ textOffset += segment.length;
+ }
+ if (textOffset < text.length) {
+ // Handle the special character at |textOffset|.
+ if (text.startsWith('\t', textOffset)) {
+ // Append a single '\t' character.
+ let effectiveTabSize = tabSize - (columnPos % tabSize);
+ if (columnPos + effectiveTabSize > lineLimit) {
+ contentText.appendChild(createLineBreak(responsiveMode));
+ columnPos = 0;
+ effectiveTabSize = tabSize;
+ }
+ contentText.appendChild(createTabWrapper(effectiveTabSize));
+ columnPos += effectiveTabSize;
+ textOffset++;
+ } else {
+ // Append a single surrogate pair.
+ if (columnPos >= lineLimit) {
+ contentText.appendChild(createLineBreak(responsiveMode));
+ columnPos = 0;
+ }
+ contentText.appendChild(
+ document.createTextNode(text.substring(textOffset, textOffset + 2))
+ );
+ textOffset += 2;
+ columnPos += 1;
+ }
+ }
+ }
+ return contentText;
+}
+
+/**
+ * Given the number of a base line and the BlameInfo create a <span> element
+ * with a hovercard. This is supposed to be put into a <td> cell of the diff.
+ */
+export function createBlameElement(
+ lineNum: LineNumber,
+ commit: BlameInfo
+): HTMLElement {
+ const isStartOfRange = commit.ranges.some(r => r.start === lineNum);
+
+ const date = new Date(commit.time * 1000).toLocaleDateString();
+ const blameNode = createElementDiff(
+ 'span',
+ isStartOfRange ? 'startOfRange' : ''
+ );
+
+ const shaNode = createElementDiff('a', 'blameDate');
+ shaNode.innerText = `${date}`;
+ shaNode.setAttribute('href', `${getBaseUrl()}/q/${commit.id}`);
+ blameNode.appendChild(shaNode);
+
+ const shortName = commit.author.split(' ')[0];
+ const authorNode = createElementDiff('span', 'blameAuthor');
+ authorNode.innerText = ` ${shortName}`;
+ blameNode.appendChild(authorNode);
+
+ const hoverCardFragment = createElementDiff('span', 'blameHoverCard');
+ hoverCardFragment.innerText = `Commit ${commit.id}
+Author: ${commit.author}
+Date: ${date}
+
+${commit.commit_msg}`;
+ const hovercard = createElementDiff('gr-hovercard');
+ hovercard.appendChild(hoverCardFragment);
+ blameNode.appendChild(hovercard);
+
+ return blameNode;
+}