/** * @license * 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. */ (function() { 'use strict'; const Defs = {}; /** * @typedef {{ * html: Node, * position: number, * length: number, * }} */ Defs.CommentLinkItem; /** * Pattern describing URLs with supported protocols. * @type {RegExp} */ const URL_PROTOCOL_PATTERN = /^(https?:\/\/|mailto:)/; /** * Construct a parser for linkifying text. Will linkify plain URLs that appear * in the text as well as custom links if any are specified in the linkConfig * parameter. * @param {Object|null|undefined} linkConfig Comment links as specified by the * commentlinks field on a project config. * @param {Function} callback The callback to be fired when an intermediate * parse result is emitted. The callback is passed text and href strings * if a link is to be created, or a document fragment otherwise. * @param {boolean|undefined} opt_removeZeroWidthSpace If true, zero-width * spaces will be removed from R= and CC= expressions. */ function GrLinkTextParser(linkConfig, callback, opt_removeZeroWidthSpace) { this.linkConfig = linkConfig; this.callback = callback; this.removeZeroWidthSpace = opt_removeZeroWidthSpace; this.baseUrl = Gerrit.BaseUrlBehavior.getBaseUrl(); Object.preventExtensions(this); } /** * Emit a callback to create a link element. * @param {string} text The text of the link. * @param {string} href The URL to use as the href of the link. */ GrLinkTextParser.prototype.addText = function(text, href) { if (!text) { return; } this.callback(text, href); }; /** * Given the source text and a list of CommentLinkItem objects that were * generated by the commentlinks config, emit parsing callbacks. * @param {string} text The chuml of source text over which the outputArray * items range. * @param {!Array} outputArray The list of items to add * resulting from commentlink matches. */ GrLinkTextParser.prototype.processLinks = function(text, outputArray) { this.sortArrayReverse(outputArray); const fragment = document.createDocumentFragment(); let cursor = text.length; // Start inserting linkified URLs from the end of the String. That way, the // string positions of the items don't change as we iterate through. outputArray.forEach(item => { // Add any text between the current linkified item and the item added // before if it exists. if (item.position + item.length !== cursor) { fragment.insertBefore( document.createTextNode( text.slice(item.position + item.length, cursor)), fragment.firstChild); } fragment.insertBefore(item.html, fragment.firstChild); cursor = item.position; }); // Add the beginning portion at the end. if (cursor !== 0) { fragment.insertBefore( document.createTextNode(text.slice(0, cursor)), fragment.firstChild); } this.callback(null, null, fragment); }; /** * Sort the given array of CommentLinkItems such that the positions are in * reverse order. * @param {!Array} outputArray */ GrLinkTextParser.prototype.sortArrayReverse = function(outputArray) { outputArray.sort((a, b) => b.position - a.position); }; /** * Create a CommentLinkItem and append it to the given output array. This * method can be called in either of two ways: * - With `text` and `href` parameters provided, and the `html` parameter * passed as `null`. In this case, the new CommentLinkItem will be a link * element with the given text and href value. * - With the `html` paremeter provided, and the `text` and `href` parameters * passed as `null`. In this case, the string of HTML will be parsed and the * first resulting node will be used as the resulting content. * @param {string|null} text The text to use if creating a link. * @param {string|null} href The href to use as the URL if creating a link. * @param {string|null} html The html to parse and use as the result. * @param {number} position The position inside the source text where the item * starts. * @param {number} length The number of characters in the source text * represented by the item. * @param {!Array} outputArray The array to which the * new item is to be appended. */ GrLinkTextParser.prototype.addItem = function(text, href, html, position, length, outputArray) { let htmlOutput = ''; if (href) { const a = document.createElement('a'); a.href = href; a.textContent = text; a.target = '_blank'; a.rel = 'noopener'; htmlOutput = a; } else if (html) { const fragment = document.createDocumentFragment(); // Create temporary div to hold the nodes in. const div = document.createElement('div'); div.innerHTML = html; while (div.firstChild) { fragment.appendChild(div.firstChild); } htmlOutput = fragment; } outputArray.push({ html: htmlOutput, position, length, }); }; /** * Create a CommentLinkItem for a link and append it to the given output * array. * @param {string|null} text The text for the link. * @param {string|null} href The href to use as the URL of the link. * @param {number} position The position inside the source text where the link * starts. * @param {number} length The number of characters in the source text * represented by the link. * @param {!Array} outputArray The array to which the * new item is to be appended. */ GrLinkTextParser.prototype.addLink = function(text, href, position, length, outputArray) { if (!text || this.hasOverlap(position, length, outputArray)) { return; } if (!!this.baseUrl && href.startsWith('/') && !href.startsWith(this.baseUrl)) { href = this.baseUrl + href; } this.addItem(text, href, null, position, length, outputArray); }; /** * Create a CommentLinkItem specified by an HTMl string and append it to the * given output array. * @param {string|null} html The html to parse and use as the result. * @param {number} position The position inside the source text where the item * starts. * @param {number} length The number of characters in the source text * represented by the item. * @param {!Array} outputArray The array to which the * new item is to be appended. */ GrLinkTextParser.prototype.addHTML = function(html, position, length, outputArray) { if (this.hasOverlap(position, length, outputArray)) { return; } if (!!this.baseUrl && html.match(/} outputArray */ GrLinkTextParser.prototype.hasOverlap = function(position, length, outputArray) { const endPosition = position + length; for (let i = 0; i < outputArray.length; i++) { const arrayItemStart = outputArray[i].position; const arrayItemEnd = outputArray[i].position + outputArray[i].length; if ((position >= arrayItemStart && position < arrayItemEnd) || (endPosition > arrayItemStart && endPosition <= arrayItemEnd) || (position === arrayItemStart && position === arrayItemEnd)) { return true; } } return false; }; /** * Parse the given source text and emit callbacks for the items that are * parsed. * @param {string} text */ GrLinkTextParser.prototype.parse = function(text) { linkify(text, { callback: this.parseChunk.bind(this), }); }; /** * Callback that is pased into the linkify function. ba-linkify will call this * method in either of two ways: * - With both a `text` and `href` parameter provided: this indicates that * ba-linkify has found a plain URL and wants it linkified. * - With only a `text` parameter provided: this represents the non-link * content that lies between the links the library has found. * @param {string} text * @param {string|null|undefined} href */ GrLinkTextParser.prototype.parseChunk = function(text, href) { // TODO(wyatta) switch linkify sequence, see issue 5526. if (this.removeZeroWidthSpace) { // Remove the zero-width space added in gr-change-view. text = text.replace(/^(CC|R)=\u200B/gm, '$1='); } // If the href is provided then ba-linkify has recognized it as a URL. If // the source text does not include a protocol, the protocol will be added // by ba-linkify. Create the link if the href is provided and its protocol // matches the expected pattern. if (href && URL_PROTOCOL_PATTERN.test(href)) { this.addText(text, href); } else { // For the sections of text that lie between the links found by // ba-linkify, we search for the project-config-specified link patterns. this.parseLinks(text, this.linkConfig); } }; /** * Walk over the given source text to find matches for comemntlink patterns * and emit parse result callbacks. * @param {string} text The raw source text. * @param {Object|null|undefined} patterns A comment links specification * object. */ GrLinkTextParser.prototype.parseLinks = function(text, patterns) { // The outputArray is used to store all of the matches found for all // patterns. const outputArray = []; for (const p in patterns) { if (patterns[p].enabled != null && patterns[p].enabled == false) { continue; } // PolyGerrit doesn't use hash-based navigation like the GWT UI. // Account for this. if (patterns[p].html) { patterns[p].html = patterns[p].html.replace(/