diff options
Diffstat (limited to 'polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts')
-rw-r--r-- | polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts | 368 |
1 files changed, 368 insertions, 0 deletions
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 new file mode 100644 index 0000000000..182d48e490 --- /dev/null +++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts @@ -0,0 +1,368 @@ +/** + * @license + * Copyright (C) 2020 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. + */ +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 + * values. + */ +export function rangesEqual(a?: CommentRange, b?: CommentRange): boolean { + if (!a && !b) { + return true; + } + if (!a || !b) { + return false; + } + return ( + a.start_line === b.start_line && + a.start_character === b.start_character && + a.end_line === b.end_line && + a.end_character === b.end_character + ); +} + +export function isLongCommentRange(range: CommentRange): boolean { + return range.end_line - range.start_line > 10; +} + +export function getLineNumberByChild(node?: Node) { + return getLineNumber(getLineElByChild(node)); +} + +export function lineNumberToNumber(lineNumber?: LineNumber | null): number { + if (!lineNumber) return 0; + if (lineNumber === 'LOST') return 0; + if (lineNumber === 'FILE') return 0; + return lineNumber; +} + +export function getLineElByChild(node?: Node): HTMLElement | null { + while (node) { + if (node instanceof Element) { + if (node.classList.contains('lineNum')) { + return node as HTMLElement; + } + if (node.classList.contains('section')) { + return null; + } + } + node = node.previousSibling ?? node.parentElement ?? undefined; + } + return null; +} + +export function getSideByLineEl(lineEl: Element) { + return lineEl.classList.contains(Side.RIGHT) ? Side.RIGHT : Side.LEFT; +} + +export function getLineNumber(lineEl?: Element | null): LineNumber | null { + if (!lineEl) return null; + const lineNumberStr = lineEl.getAttribute('data-value'); + if (!lineNumberStr) return null; + if (lineNumberStr === FILE) return FILE; + if (lineNumberStr === 'LOST') return 'LOST'; + const lineNumber = Number(lineNumberStr); + return Number.isInteger(lineNumber) ? lineNumber : null; +} + +export function getLine(threadEl: HTMLElement): LineNumber { + const lineAtt = threadEl.getAttribute('line-num'); + if (lineAtt === 'LOST') return lineAtt; + if (!lineAtt || lineAtt === 'FILE') return FILE; + const line = Number(lineAtt); + if (isNaN(line)) throw new Error(`cannot parse line number: ${lineAtt}`); + if (line < 1) throw new Error(`line number smaller than 1: ${line}`); + return line; +} + +export function getSide(threadEl: HTMLElement): Side | undefined { + const sideAtt = threadEl.getAttribute('diff-side'); + if (!sideAtt) { + console.warn('comment thread without side'); + return undefined; + } + if (sideAtt !== Side.LEFT && sideAtt !== Side.RIGHT) + throw Error(`unexpected value for side: ${sideAtt}`); + return sideAtt as Side; +} + +export function getRange(threadEl: HTMLElement): CommentRange | undefined { + const rangeAtt = threadEl.getAttribute('range'); + if (!rangeAtt) return undefined; + const range = JSON.parse(rangeAtt) as CommentRange; + if (!range.start_line) throw new Error(`invalid range: ${rangeAtt}`); + return range; +} + +// TODO: This type should be exposed to gr-diff clients in a separate type file. +// For Gerrit these are instances of GrCommentThread, but other gr-diff users +// have different HTML elements in use for comment threads. +// TODO: Also document the required HTML attributes that thread elements must +// have, e.g. 'diff-side', 'range', 'line-num'. +export interface GrDiffThreadElement extends HTMLElement { + rootId: string; +} + +export function isThreadEl(node: Node): node is GrDiffThreadElement { + return ( + node.nodeType === Node.ELEMENT_NODE && + (node as Element).classList.contains('comment-thread') + ); +} + +/** + * @return whether any of the lines in diff are longer + * than SYNTAX_MAX_LINE_LENGTH. + */ +export function anyLineTooLong(diff?: DiffInfo) { + if (!diff) return false; + return diff.content.some(section => { + const lines = section.ab + ? section.ab + : (section.a || []).concat(section.b || []); + return lines.some(line => line.length >= SYNTAX_MAX_LINE_LENGTH); + }); +} + +/** + * Simple helper method for creating elements in the context of gr-diff. + * + * We are adding 'style-scope', 'gr-diff' classes for compatibility with + * Shady DOM. TODO: Is that still required?? + * + * Otherwise this is just a super simple convenience function. + */ +export function createElementDiff( + tagName: string, + classStr?: string +): HTMLElement { + const 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 (classStr) { + for (const className of classStr.split(' ')) { + el.classList.add(className); + } + } + return el; +} + +export function createElementDiffWithText( + tagName: string, + textContent: string +) { + const element = createElementDiff(tagName); + 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; +} |