| /** |
| * @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=<email> and CC=<email> 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<Defs.CommentLinkItem>} 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<Defs.CommentLinkItem>} 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<Defs.CommentLinkItem>} 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<Defs.CommentLinkItem>} 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<Defs.CommentLinkItem>} 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(/<a href=\"\//g) && |
| !html.match(`/<a href=\"${this.baseUrl}/g`)) { |
| html = html.replace(/<a href=\"\//g, `<a href=\"${this.baseUrl}\/`); |
| } |
| this.addItem(null, null, html, position, length, outputArray); |
| }; |
| |
| /** |
| * Does the given range overlap with anything already in the item list. |
| * @param {number} position |
| * @param {number} length |
| * @param {!Array<Defs.CommentLinkItem>} 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(/<a href=\"#\//g, '<a href="/'); |
| } else if (patterns[p].link) { |
| if (patterns[p].link[0] == '#') { |
| patterns[p].link = patterns[p].link.substr(1); |
| } |
| } |
| |
| const pattern = new RegExp(patterns[p].match, 'g'); |
| |
| let match; |
| let textToCheck = text; |
| let susbtrIndex = 0; |
| |
| while ((match = pattern.exec(textToCheck)) != null) { |
| textToCheck = textToCheck.substr(match.index + match[0].length); |
| let result = match[0].replace(pattern, |
| patterns[p].html || patterns[p].link); |
| |
| let i; |
| // Skip portion of replacement string that is equal to original. |
| for (i = 0; i < result.length; i++) { |
| if (result[i] !== match[0][i]) { break; } |
| } |
| result = result.slice(i); |
| |
| if (patterns[p].html) { |
| this.addHTML( |
| result, |
| susbtrIndex + match.index + i, |
| match[0].length - i, |
| outputArray); |
| } else if (patterns[p].link) { |
| this.addLink( |
| match[0], |
| result, |
| susbtrIndex + match.index + i, |
| match[0].length - i, |
| outputArray); |
| } else { |
| throw Error('linkconfig entry ' + p + |
| ' doesn’t contain a link or html attribute.'); |
| } |
| |
| // Update the substring location so we know where we are in relation to |
| // the initial full text string. |
| susbtrIndex = susbtrIndex + match.index + match[0].length; |
| } |
| } |
| this.processLinks(text, outputArray); |
| }; |
| |
| window.GrLinkTextParser = GrLinkTextParser; |
| })(); |