diff options
Diffstat (limited to 'polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js')
-rw-r--r-- | polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js | 500 |
1 files changed, 310 insertions, 190 deletions
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js index 8a489f44fd..8526c3e209 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js @@ -1,208 +1,328 @@ -// 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. - -'use strict'; - -function GrLinkTextParser(linkConfig, callback, opt_removeZeroWidthSpace) { - this.linkConfig = linkConfig; - this.callback = callback; - this.removeZeroWidthSpace = opt_removeZeroWidthSpace; - Object.preventExtensions(this); -} - -GrLinkTextParser.prototype.addText = function(text, href) { - if (!text) { - return; - } - this.callback(text, href); -}; - -GrLinkTextParser.prototype.processLinks = function(text, outputArray) { - this.sortArrayReverse(outputArray); - var fragment = document.createDocumentFragment(); - var 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(function(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); - } +/** + * @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'; - this.callback(null, null, fragment); -}; - -GrLinkTextParser.prototype.sortArrayReverse = function(outputArray) { - outputArray.sort(function(a, b) {return b.position - a.position}); -}; - -GrLinkTextParser.prototype.addItem = - function(text, href, html, position, length, outputArray) { - var htmlOutput = ''; - - if (href) { - var a = document.createElement('a'); - a.href = href; - a.textContent = text; - a.target = '_blank'; - a.rel = 'noopener'; - htmlOutput = a; - } else if (html) { - var fragment = document.createDocumentFragment(); - // Create temporary div to hold the nodes in. - var div = document.createElement('div'); - div.innerHTML = html; - while (div.firstChild) { - fragment.appendChild(div.firstChild); - } - htmlOutput = fragment; - } + const Defs = {}; - outputArray.push({ - html: htmlOutput, - position: position, - length: length, - }); -}; - -GrLinkTextParser.prototype.addLink = - function(text, href, position, length, outputArray) { - if (!text) { - return; - } - if (!this.hasOverlap(position, length, outputArray)) { - this.addItem(text, href, null, position, length, outputArray); - } -}; + /** + * @typedef {{ + * html: Node, + * position: number, + * length: number, + * }} + */ + Defs.CommentLinkItem; -GrLinkTextParser.prototype.addHTML = - function(html, position, length, outputArray) { - if (!this.hasOverlap(position, length, outputArray)) { - this.addItem(null, null, html, position, length, outputArray); - } -}; - -GrLinkTextParser.prototype.hasOverlap = - function(position, length, outputArray) { - var endPosition = position + length; - for (var i = 0; i < outputArray.length; i++) { - var arrayItemStart = outputArray[i].position; - var arrayItemEnd = outputArray[i].position + outputArray[i].length; - if ((position >= arrayItemStart && position < arrayItemEnd) || - (endPosition > arrayItemStart && endPosition <= arrayItemEnd) || - (position === arrayItemStart && position === arrayItemEnd)) { - return true; - } - } - return false; -}; - -GrLinkTextParser.prototype.parse = function(text) { - linkify(text, { - callback: this.parseChunk.bind(this), - }); -}; - -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='); - } + /** + * Pattern describing URLs with supported protocols. + * @type {RegExp} + */ + const URL_PROTOCOL_PATTERN = /^(https?:\/\/|mailto:)/; - if (href) { - this.addText(text, href); - } else { - this.parseLinks(text, this.linkConfig); + /** + * 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; + Object.preventExtensions(this); } -}; - -GrLinkTextParser.prototype.parseLinks = function(text, patterns) { - // The outputArray is used to store all of the matches found for all patterns. - var outputArray = []; - for (var p in patterns) { - if (patterns[p].enabled != null && patterns[p].enabled == false) { - continue; - } - // PolyGerrit doesn't use hash-based navigation like GWT. - // Account for this. - // TODO(andybons): Support Gerrit being served from a base other than /, - // e.g. https://git.eclipse.org/r/ - 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); + + /** + * 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); } - var pattern = new RegExp(patterns[p].match, 'g'); + this.callback(null, null, fragment); + }; - var match; - var textToCheck = text; - var susbtrIndex = 0; + /** + * 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); + }; - while ((match = pattern.exec(textToCheck)) != null) { - textToCheck = textToCheck.substr(match.index + match[0].length); - var result = match[0].replace(pattern, - patterns[p].html || patterns[p].link); + /** + * 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 = ''; - // Skip portion of replacement string that is equal to original. - for (var i = 0; i < result.length; i++) { - if (result[i] !== match[0][i]) { - break; + 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; } - } - result = result.slice(i); + 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; } + 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; } + 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) { - this.addHTML( - result, - susbtrIndex + match.index + i, - match[0].length - i, - outputArray); + patterns[p].html = + patterns[p].html.replace(/<a href=\"#\//g, '<a href="/'); } 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.'); + if (patterns[p].link[0] == '#') { + patterns[p].link = patterns[p].link.substr(1); + } } - // 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; + 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); -}; + this.processLinks(text, outputArray); + }; + + window.GrLinkTextParser = GrLinkTextParser; +})(); |