summaryrefslogtreecommitdiffstats
path: root/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.js
diff options
context:
space:
mode:
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.js500
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;
+})();