| // 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.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 contextLines = line.contextGroup.lines; |
| var td = this._createElement('td'); |
| var button = this._createElement('gr-button', 'showContext'); |
| button.setAttribute('link', true); |
| var commonLines = contextLines.length; |
| var text = 'Show ' + commonLines + ' common line'; |
| if (commonLines > 1) { |
| text += 's'; |
| } |
| text += '...'; |
| button.textContent = text; |
| button.addEventListener('tap', function(e) { |
| e.detail = { |
| group: line.contextGroup, |
| section: section, |
| }; |
| // Let it bubble up the DOM tree. |
| }); |
| td.appendChild(button); |
| return td; |
| }; |
| |
| 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 |
| // '<span>'. Advancing from index 0 on the prior html string would |
| // return 4, since < 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., <) 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); |