From 2207d356f251f45e0b0027d858f0edbc15219840 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 8 Feb 2017 11:11:52 -0800 Subject: [PATCH 01/21] Initial linkify implementation Part of 455 --- src/Linkifier.ts | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ src/xterm.css | 4 +++ src/xterm.js | 11 +++++++ 3 files changed, 98 insertions(+) create mode 100644 src/Linkifier.ts diff --git a/src/Linkifier.ts b/src/Linkifier.ts new file mode 100644 index 00000000..6eaf82cd --- /dev/null +++ b/src/Linkifier.ts @@ -0,0 +1,83 @@ +/** + * The time to wait after a row is changed before it is linkified. This prevents + * the costly operation of searching every row multiple times, pntentially a + * huge aount of times. + */ +const TIME_BEFORE_LINKIFY = 200; + +const badUrlRegex = /https?:\/\/(\/[\/\\w\.-]*)*/; + +const protocolClause = '(https?:\\/\\/)'; +const domainCharacterSet = '[\\da-z\\.-]+'; +const negatedDomainCharacterSet = '[^\\da-z\\.-]+'; +const domainBodyClause = '(' + domainCharacterSet + ')'; +const tldClause = '([a-z\\.]{2,6})'; +const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; +const portClause = '(:\\d{1,5})'; +const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?'; +const pathClause = '(\\/[\\/\\w\\.-]*)*'; +const negatedPathCharacterSet = '[^\\/\\w\\.-]+'; +const bodyClause = hostClause + pathClause; +const start = '(?:^|' + negatedDomainCharacterSet + ')('; +const end = ')($|' + negatedPathCharacterSet + ')'; +const lenientUrlClause = start + protocolClause + '?' + bodyClause + end; +const strictUrlClause = start + protocolClause + bodyClause + end; +const lenientUrlRegex = new RegExp(lenientUrlClause); +const strictUrlRegex = new RegExp(strictUrlClause); + +export type LinkHandler = (uri: string) => void; + +export class Linkifier { + private _rows: HTMLElement[]; + private _rowTimeoutIds: number[]; + private _webLinkHandler: LinkHandler; + + constructor(rows: HTMLElement[]) { + this._rows = rows; + this._rowTimeoutIds = []; + } + + /** + * Queues a row for linkification. + * @param {number} rowIndex The index of the row to linkify. + */ + public linkifyRow(rowIndex: number): void { + const timeoutId = this._rowTimeoutIds[rowIndex]; + if (timeoutId) { + clearTimeout(timeoutId); + } + this._rowTimeoutIds[rowIndex] = setTimeout(this._linkifyRow.bind(this, rowIndex), TIME_BEFORE_LINKIFY); + } + + public attachWebLinkHandler(handler: LinkHandler): void { + this._webLinkHandler = handler; + } + + /** + * Linkifies a row. + * @param {number} rowIndex The index of the row to linkify. + */ + private _linkifyRow(rowIndex: number): void { + const rowHtml = this._rows[rowIndex].innerHTML; + const uri = this._findLinkMatch(rowHtml); + if (uri) { + const link = '' + uri + ''; + const newHtml = rowHtml.replace(uri, link); + this._rows[rowIndex].innerHTML = newHtml; + console.log(this._rows[rowIndex].innerHTML); + } + } + + /** + * Finds a link match in a piece of HTML. + * @param {string} html The HTML to search. + * @return The matching URI or null if not found. + */ + private _findLinkMatch(html): string { + const match = html.match(strictUrlRegex); + if (!match || match.length === 0) { + return null; + } + return match[1]; + } +} diff --git a/src/xterm.css b/src/xterm.css index 27638e14..3075bba4 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -71,6 +71,10 @@ resize: none; } +.terminal a { + color: inherit; +} + .terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor { background-color: #fff; color: #000; diff --git a/src/xterm.js b/src/xterm.js index b2740683..26ed1f70 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -19,6 +19,7 @@ import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; import { Parser } from './Parser'; import { Renderer } from './Renderer'; +import { Linkifier } from './Linkifier'; import { CharMeasure } from './utils/CharMeasure'; import * as Browser from './utils/Browser'; import * as Keyboard from './utils/Keyboard'; @@ -210,6 +211,7 @@ function Terminal(options) { this.parser = new Parser(this.inputHandler, this); // Reuse renderer if the Terminal is being recreated via a Terminal.reset call. this.renderer = this.renderer || null; + this.linkifier = this.linkifier || null;; // user input states this.writeBuffer = []; @@ -607,6 +609,7 @@ Terminal.prototype.open = function(parent) { this.rowContainer.classList.add('xterm-rows'); this.element.appendChild(this.rowContainer); this.children = []; + this.linkifier = new Linkifier(this.children); // Create the container that will hold helpers like the textarea for // capturing DOM Events. Then produce the helpers. @@ -1060,6 +1063,14 @@ Terminal.prototype.destroy = function() { Terminal.prototype.refresh = function(start, end) { if (this.renderer) { this.renderer.queueRefresh(start, end); + + // TODO: DO this better + if (!this.linkifier) { + return; + } + for (let i = start; i <= end; i++) { + this.linkifier.linkifyRow(i); + } } }; From a489037ec407f119a2038a248282a042bb499311 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 8 Feb 2017 13:50:36 -0800 Subject: [PATCH 02/21] Improve node insertion, support custom link handlers --- src/Linkifier.ts | 129 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 120 insertions(+), 9 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 6eaf82cd..64be3947 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -5,8 +5,6 @@ */ const TIME_BEFORE_LINKIFY = 200; -const badUrlRegex = /https?:\/\/(\/[\/\\w\.-]*)*/; - const protocolClause = '(https?:\\/\\/)'; const domainCharacterSet = '[\\da-z\\.-]+'; const negatedDomainCharacterSet = '[^\\da-z\\.-]+'; @@ -49,8 +47,10 @@ export class Linkifier { this._rowTimeoutIds[rowIndex] = setTimeout(this._linkifyRow.bind(this, rowIndex), TIME_BEFORE_LINKIFY); } + // TODO: Support local links public attachWebLinkHandler(handler: LinkHandler): void { this._webLinkHandler = handler; + // TODO: Refresh links if a handler is attached? } /** @@ -60,24 +60,135 @@ export class Linkifier { private _linkifyRow(rowIndex: number): void { const rowHtml = this._rows[rowIndex].innerHTML; const uri = this._findLinkMatch(rowHtml); - if (uri) { - const link = '' + uri + ''; - const newHtml = rowHtml.replace(uri, link); - this._rows[rowIndex].innerHTML = newHtml; - console.log(this._rows[rowIndex].innerHTML); + if (!uri) { + return; + } + + // Iterate over nodes as we want to consider text nodes + const nodes = this._rows[rowIndex].childNodes; + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + const searchIndex = node.textContent.indexOf(uri); + if (searchIndex >= 0) { + if (node.childNodes.length > 0) { + // This row has already been linkified + return; + } + + console.log('found uri: ' + uri); + const linkElement = this._createAnchorElement(uri); + // TODO: Check if childNodes check is needed + if (node.textContent.trim().length === uri.length) { + // Matches entire string + console.log('match entire string'); + if (node.nodeType === Node.TEXT_NODE) { + console.log('text node'); + this._replaceNode(node, linkElement); + } else { + console.log('element'); + const element = (node); + element.innerHTML = ''; + element.appendChild(linkElement); + } + } else { + // Matches part of string + console.log('part of string'); + this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex); + } + } + // Continue searching in case multiple URIs exist on a single + // const link = '' + uri + ''; + // const newHtml = rowHtml.replace(uri, link); + // this._rows[rowIndex].innerHTML = newHtml; } } /** * Finds a link match in a piece of HTML. * @param {string} html The HTML to search. - * @return The matching URI or null if not found. + * @return {string} The matching URI or null if not found. */ - private _findLinkMatch(html): string { + private _findLinkMatch(html: string): string { const match = html.match(strictUrlRegex); if (!match || match.length === 0) { return null; } return match[1]; } + + /** + * Creates a link anchor element. + * @param {string} uri The uri of the link. + * @return {HTMLAnchorElement} The link. + */ + private _createAnchorElement(uri: string): HTMLAnchorElement { + const element = document.createElement('a'); + element.textContent = uri; + // Force link on another tab so work is not lost + element.target = '_blank'; + if (this._webLinkHandler) { + element.href = '#'; + element.addEventListener('click', () => this._webLinkHandler(uri)); + } else { + element.href = uri; + } + return element; + } + + /** + * Replace a node with 1 or more other nodes. + * @param {Node} oldNode The node to replace. + * @param {Node[]} newNodes The new nodes to insert in order. + */ + private _replaceNode(oldNode: Node, ...newNodes: Node[]): void { + const parent = oldNode.parentNode; + for (let i = 0; i < newNodes.length; i++) { + parent.insertBefore(newNodes[i], oldNode); + } + parent.removeChild(oldNode); + } + + /** + * Replace a substring within a node with a new node. + * @param {Node} targetNode The target node. + * @param {Node} newNode The new node to insert. + * @param {string} substring The substring to replace. + * @param {number} substringIndex The index of the substring within the string. + */ + private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): void { + let node = targetNode; + if (node.nodeType !== Node.TEXT_NODE) { + node = node.childNodes[0]; + } + // The targetNode will be either a text node or a . The targetNode is + // assumed to have no children. In either case, the targetNode's text node + // must be split into 2 text nodes surrounding the newNode. + if (node.childNodes.length === 0 && node.nodeType !== Node.TEXT_NODE) { + throw new Error('targetNode must be a text node or only contain a single text node'); + } + + const fullText = node.textContent; + + if (substringIndex === 0) { + // Replace with + console.log('Replace with '); + const rightText = fullText.substring(substring.length); + const rightTextNode = document.createTextNode(rightText); + this._replaceNode(node, newNode, rightTextNode); + } else if (substringIndex === targetNode.textContent.length - substring.length) { + // Replace with + console.log('Replace with '); + const leftText = fullText.substring(0, substringIndex); + const leftTextNode = document.createTextNode(leftText); + this._replaceNode(node, leftTextNode, newNode); + } else { + // Replace with + console.log('Replace with '); + const leftText = fullText.substring(0, substringIndex); + const leftTextNode = document.createTextNode(leftText); + const rightText = fullText.substring(substringIndex + substring.length); + const rightTextNode = document.createTextNode(rightText); + this._replaceNode(node, leftTextNode, newNode, rightTextNode); + } + } } From 0f3ee21d9d8a53622b7c640d4dfe5909fd3839bb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 8 Feb 2017 14:13:30 -0800 Subject: [PATCH 03/21] Enable custom http link handlers --- src/Linkifier.ts | 30 +++++++++++++----------------- src/xterm.css | 6 ++++++ src/xterm.js | 10 ++++++++++ 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 64be3947..adb96566 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -28,7 +28,7 @@ export type LinkHandler = (uri: string) => void; export class Linkifier { private _rows: HTMLElement[]; private _rowTimeoutIds: number[]; - private _webLinkHandler: LinkHandler; + private _hypertextLinkHandler: LinkHandler; constructor(rows: HTMLElement[]) { this._rows = rows; @@ -48,9 +48,8 @@ export class Linkifier { } // TODO: Support local links - public attachWebLinkHandler(handler: LinkHandler): void { - this._webLinkHandler = handler; - // TODO: Refresh links if a handler is attached? + public attachHypertextLinkHandler(handler: LinkHandler): void { + this._hypertextLinkHandler = handler; } /** @@ -96,10 +95,6 @@ export class Linkifier { this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex); } } - // Continue searching in case multiple URIs exist on a single - // const link = '' + uri + ''; - // const newHtml = rowHtml.replace(uri, link); - // this._rows[rowIndex].innerHTML = newHtml; } } @@ -124,13 +119,12 @@ export class Linkifier { private _createAnchorElement(uri: string): HTMLAnchorElement { const element = document.createElement('a'); element.textContent = uri; - // Force link on another tab so work is not lost - element.target = '_blank'; - if (this._webLinkHandler) { - element.href = '#'; - element.addEventListener('click', () => this._webLinkHandler(uri)); + if (this._hypertextLinkHandler) { + element.addEventListener('click', () => this._hypertextLinkHandler(uri)); } else { element.href = uri; + // Force link on another tab so work is not lost + element.target = '_blank'; } return element; } @@ -150,7 +144,8 @@ export class Linkifier { /** * Replace a substring within a node with a new node. - * @param {Node} targetNode The target node. + * @param {Node} targetNode The target node; either a text node or a + * containing a single text node. * @param {Node} newNode The new node to insert. * @param {string} substring The substring to replace. * @param {number} substringIndex The index of the substring within the string. @@ -160,9 +155,10 @@ export class Linkifier { if (node.nodeType !== Node.TEXT_NODE) { node = node.childNodes[0]; } - // The targetNode will be either a text node or a . The targetNode is - // assumed to have no children. In either case, the targetNode's text node - // must be split into 2 text nodes surrounding the newNode. + + // The targetNode will be either a text node or a . The text node + // (targetNode or its only-child) needs to be replaced with newNode plus new + // text nodes potentially on either side. if (node.childNodes.length === 0 && node.nodeType !== Node.TEXT_NODE) { throw new Error('targetNode must be a text node or only contain a single text node'); } diff --git a/src/xterm.css b/src/xterm.css index 3075bba4..43f2594c 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -73,6 +73,12 @@ .terminal a { color: inherit; + /* Feature underline on links even if they have no href */ + text-decoration: underline; +} + +.terminal a:hover { + cursor: pointer; } .terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor { diff --git a/src/xterm.js b/src/xterm.js index 26ed1f70..f858af31 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1274,6 +1274,16 @@ Terminal.prototype.attachCustomKeydownHandler = function(customKeydownHandler) { this.customKeydownHandler = customKeydownHandler; } +// TODO: Doc +Terminal.prototype.attachHypertextLinkHandler = function(handler) { + if (!this.linkifier) { + throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); + } + this.linkifier.attachHypertextLinkHandler(handler); + // Refresh to force links to refresh + this.refresh(0, this.rows - 1); +} + /** * Handle a keydown event * Key Resources: From f7bc0fba4dca0cbe8581db6af6da78391b337cb6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 8 Feb 2017 18:57:45 -0800 Subject: [PATCH 04/21] Remove unnecessary variables --- src/Linkifier.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index adb96566..d97f4d14 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -18,13 +18,13 @@ const negatedPathCharacterSet = '[^\\/\\w\\.-]+'; const bodyClause = hostClause + pathClause; const start = '(?:^|' + negatedDomainCharacterSet + ')('; const end = ')($|' + negatedPathCharacterSet + ')'; -const lenientUrlClause = start + protocolClause + '?' + bodyClause + end; -const strictUrlClause = start + protocolClause + bodyClause + end; -const lenientUrlRegex = new RegExp(lenientUrlClause); -const strictUrlRegex = new RegExp(strictUrlClause); +const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); export type LinkHandler = (uri: string) => void; +/** + * The Linkifier applies links to rows shortly after they have been refreshed. + */ export class Linkifier { private _rows: HTMLElement[]; private _rowTimeoutIds: number[]; From 8dc9ec5733a8a081438f90f93ba3a775bb4abad9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 8 Feb 2017 20:20:18 -0800 Subject: [PATCH 05/21] Only show underline on hover --- src/xterm.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xterm.css b/src/xterm.css index 43f2594c..41dda53c 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -73,12 +73,12 @@ .terminal a { color: inherit; - /* Feature underline on links even if they have no href */ - text-decoration: underline; + text-decoration: none; } .terminal a:hover { cursor: pointer; + text-decoration: underline; } .terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor { From 34582f9f65a0a1c778762d05bb5a4ddf97494982 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 8 Feb 2017 20:21:18 -0800 Subject: [PATCH 06/21] Remove logs --- src/Linkifier.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index d97f4d14..07573421 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -74,24 +74,19 @@ export class Linkifier { return; } - console.log('found uri: ' + uri); const linkElement = this._createAnchorElement(uri); // TODO: Check if childNodes check is needed if (node.textContent.trim().length === uri.length) { // Matches entire string - console.log('match entire string'); if (node.nodeType === Node.TEXT_NODE) { - console.log('text node'); this._replaceNode(node, linkElement); } else { - console.log('element'); const element = (node); element.innerHTML = ''; element.appendChild(linkElement); } } else { // Matches part of string - console.log('part of string'); this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex); } } @@ -167,19 +162,16 @@ export class Linkifier { if (substringIndex === 0) { // Replace with - console.log('Replace with '); const rightText = fullText.substring(substring.length); const rightTextNode = document.createTextNode(rightText); this._replaceNode(node, newNode, rightTextNode); } else if (substringIndex === targetNode.textContent.length - substring.length) { // Replace with - console.log('Replace with '); const leftText = fullText.substring(0, substringIndex); const leftTextNode = document.createTextNode(leftText); this._replaceNode(node, leftTextNode, newNode); } else { // Replace with - console.log('Replace with '); const leftText = fullText.substring(0, substringIndex); const leftTextNode = document.createTextNode(leftText); const rightText = fullText.substring(substringIndex + substring.length); From 7167b06bf85ac8dc02649944f2d9dda2225619bb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 8 Feb 2017 20:44:22 -0800 Subject: [PATCH 07/21] Add custom link handlers, use generic way for http handler --- src/Linkifier.ts | 82 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 07573421..94a7f55d 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -22,17 +22,25 @@ const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); export type LinkHandler = (uri: string) => void; +type LinkMatcher = {id: number, regex: RegExp, handler: LinkHandler}; + +const HYPERTEXT_LINK_MATCHER_ID = 0; + /** * The Linkifier applies links to rows shortly after they have been refreshed. */ export class Linkifier { + private static _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; + private _rows: HTMLElement[]; private _rowTimeoutIds: number[]; - private _hypertextLinkHandler: LinkHandler; + private _linkMatchers: LinkMatcher[]; constructor(rows: HTMLElement[]) { this._rows = rows; this._rowTimeoutIds = []; + this._linkMatchers = []; + this.registerLinkMatcher(strictUrlRegex, null); } /** @@ -47,9 +55,46 @@ export class Linkifier { this._rowTimeoutIds[rowIndex] = setTimeout(this._linkifyRow.bind(this, rowIndex), TIME_BEFORE_LINKIFY); } - // TODO: Support local links + /** + * Attaches a handler for hypertext links, overriding default behavior. + * @param {LinkHandler} handler The handler to use, this can be cleared with + * null. + */ public attachHypertextLinkHandler(handler: LinkHandler): void { - this._hypertextLinkHandler = handler; + this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler; + } + + /** + * Registers a link matcher, allowing custom link patterns to be matched and + * handled. + * @param {RegExp} regex The regular expression the search for. + * @param {LinkHandler} handler The callback when the link is called. + * @return {number} The ID of the new matcher, this can be used to deregister. + */ + public registerLinkMatcher(regex: RegExp, handler: LinkHandler): number { + if (Linkifier._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) { + throw new Error('handler cannot be falsy'); + } + const matcher: LinkMatcher = { + id: Linkifier._nextLinkMatcherId++, + regex, + handler + }; + this._linkMatchers.push(matcher); + return matcher.id; + } + + /** + * Deregisters a link matcher if it has been registered. + * @param {number} matcherId The link matcher's ID (returned after register) + */ + public deregisterLinkMatcher(matcherId: number): void { + // ID 0 is the hypertext link matcher which cannot be deregistered + for (let i = 1; i < this._linkMatchers.length; i++) { + if (this._linkMatchers[i].id === matcherId) { + this._linkMatchers.splice(i, 1); + } + } } /** @@ -58,11 +103,24 @@ export class Linkifier { */ private _linkifyRow(rowIndex: number): void { const rowHtml = this._rows[rowIndex].innerHTML; - const uri = this._findLinkMatch(rowHtml); - if (!uri) { - return; + for (let i = 0; i < this._linkMatchers.length; i++) { + const matcher = this._linkMatchers[i]; + const uri = this._findLinkMatch(rowHtml, matcher.regex); + if (uri) { + this._doLinkifyRow(rowIndex, uri, matcher.handler); + // Only allow a single LinkMatcher to trigger on any given row. + return; + } } + } + /** + * Linkifies a row given a specific handler. + * @param {number} rowIndex The index of the row to linkify. + * @param {string} uri The uri that has been found. + * @param {handler} handler The handler to trigger when the link is triggered. + */ + private _doLinkifyRow(rowIndex: number, uri: string, handler?: LinkHandler): void { // Iterate over nodes as we want to consider text nodes const nodes = this._rows[rowIndex].childNodes; for (let i = 0; i < nodes.length; i++) { @@ -74,7 +132,7 @@ export class Linkifier { return; } - const linkElement = this._createAnchorElement(uri); + const linkElement = this._createAnchorElement(uri, handler); // TODO: Check if childNodes check is needed if (node.textContent.trim().length === uri.length) { // Matches entire string @@ -98,8 +156,8 @@ export class Linkifier { * @param {string} html The HTML to search. * @return {string} The matching URI or null if not found. */ - private _findLinkMatch(html: string): string { - const match = html.match(strictUrlRegex); + private _findLinkMatch(html: string, regex: RegExp): string { + const match = html.match(regex); if (!match || match.length === 0) { return null; } @@ -111,11 +169,11 @@ export class Linkifier { * @param {string} uri The uri of the link. * @return {HTMLAnchorElement} The link. */ - private _createAnchorElement(uri: string): HTMLAnchorElement { + private _createAnchorElement(uri: string, handler: LinkHandler): HTMLAnchorElement { const element = document.createElement('a'); element.textContent = uri; - if (this._hypertextLinkHandler) { - element.addEventListener('click', () => this._hypertextLinkHandler(uri)); + if (handler) { + element.addEventListener('click', () => handler(uri)); } else { element.href = uri; // Force link on another tab so work is not lost From 3bf31aa478ef8d7747f7c30face8f28382190594 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Feb 2017 20:09:47 -0800 Subject: [PATCH 08/21] Polish --- src/Linkifier.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 94a7f55d..e502289e 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -56,7 +56,8 @@ export class Linkifier { } /** - * Attaches a handler for hypertext links, overriding default behavior. + * Attaches a handler for hypertext links, overriding default behavior + * for standard http(s) links. * @param {LinkHandler} handler The handler to use, this can be cleared with * null. */ @@ -133,7 +134,6 @@ export class Linkifier { } const linkElement = this._createAnchorElement(uri, handler); - // TODO: Check if childNodes check is needed if (node.textContent.trim().length === uri.length) { // Matches entire string if (node.nodeType === Node.TEXT_NODE) { From 551159e0c33ea72b1070c53f3c2bd4b65652cd9c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Feb 2017 20:10:01 -0800 Subject: [PATCH 09/21] Remove linkify addon --- src/addons/linkify/index.html | 36 ------ src/addons/linkify/linkify.js | 207 -------------------------------- src/addons/linkify/package.json | 5 - 3 files changed, 248 deletions(-) delete mode 100644 src/addons/linkify/index.html delete mode 100644 src/addons/linkify/linkify.js delete mode 100644 src/addons/linkify/package.json diff --git a/src/addons/linkify/index.html b/src/addons/linkify/index.html deleted file mode 100644 index 5f429326..00000000 --- a/src/addons/linkify/index.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - -
- - - \ No newline at end of file diff --git a/src/addons/linkify/linkify.js b/src/addons/linkify/linkify.js deleted file mode 100644 index d2391026..00000000 --- a/src/addons/linkify/linkify.js +++ /dev/null @@ -1,207 +0,0 @@ -/** - * Methods for turning URL subscrings in the terminal's content into links (`a` DOM elements). - * @module xterm/addons/linkify/linkify - * @license MIT - */ - -(function (linkify) { - if (typeof exports === 'object' && typeof module === 'object') { - /* - * CommonJS environment - */ - module.exports = linkify(require('../../xterm')); - } else if (typeof define == 'function') { - /* - * Require.js is available - */ - define(['../../xterm'], linkify); - } else { - /* - * Plain browser environment - */ - linkify(window.Terminal); - } -})(function (Xterm) { - 'use strict'; - - var exports = {}, - protocolClause = '(https?:\\/\\/)', - domainCharacterSet = '[\\da-z\\.-]+', - negatedDomainCharacterSet = '[^\\da-z\\.-]+', - domainBodyClause = '(' + domainCharacterSet + ')', - tldClause = '([a-z\\.]{2,6})', - ipClause = '((\\d{1,3}\\.){3}\\d{1,3})', - portClause = '(:\\d{1,5})', - hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?', - pathClause = '(\\/[\\/\\w\\.-]*)*', - negatedPathCharacterSet = '[^\\/\\w\\.-]+', - bodyClause = hostClause + pathClause, - start = '(?:^|' + negatedDomainCharacterSet + ')(', - end = ')($|' + negatedPathCharacterSet + ')', - lenientUrlClause = start + protocolClause + '?' + bodyClause + end, - strictUrlClause = start + protocolClause + bodyClause + end, - lenientUrlRegex = new RegExp(lenientUrlClause), - strictUrlRegex = new RegExp(strictUrlClause); - - /** - * Converts all valid URLs found in the given terminal line into - * hyperlinks. The terminal line can be either the HTML element itself - * or the index of the termina line in the children of the terminal - * rows container. - * - * @param {Xterm} terminal - The terminal that owns the given line. - * @param {number|HTMLDivElement} line - The terminal line that should get - * "linkified". - * @param {boolean} lenient - The regex type that will be used to identify links. If lenient is - * false, the regex requires a protocol clause. Defaults to true. - * @param {string} target - Sets target="" attribute with value provided to links. - * Default doesn't set target attribute - * @emits linkify - * @emits linkify:line - */ - exports.linkifyTerminalLine = function (terminal, line, lenient, target) { - if (typeof line == 'number') { - line = terminal.rowContainer.children[line]; - } else if (! (line instanceof HTMLDivElement)) { - var message = 'The "line" argument should be either a number'; - message += ' or an HTMLDivElement'; - - throw new TypeError(message); - } - - if (typeof target === 'undefined') { - target = ''; - } else { - target = 'target="' + target + '"'; - } - - var buffer = document.createElement('span'), - nodes = line.childNodes; - - for (var j=0; j' + url + '
', - newHTML = nodeHTML.replace(url, link); - - line.innerHTML = line.innerHTML.replace(nodeHTML, newHTML); - } - - /** - * This event gets emitted when conversion of all URL susbtrings - * to HTML anchor elements (links) has finished, for a specific - * line of the current Xterm instance. - * - * @event linkify:line - */ - terminal.emit('linkify:line', line); - }; - - /** - * Finds a link within a block of text. - * - * @param {string} text - The text to search . - * @param {boolean} lenient - Whether to use the lenient search. - * @return {string} A URL. - */ - exports.findLinkMatch = function (text, lenient) { - var match = text.match(lenient ? lenientUrlRegex : strictUrlRegex); - if (!match || match.length === 0) { - return null; - } - return match[1]; - } - - /** - * Converts all valid URLs found in the terminal view into hyperlinks. - * - * @param {Xterm} terminal - The terminal that should get "linkified". - * @param {boolean} lenient - The regex type that will be used to identify links. If lenient is - * false, the regex requires a protocol clause. Defaults to true. - * @param {string} target - Sets target="" attribute with value provided to links. - * Default doesn't set target attribute - * @emits linkify - * @emits linkify:line - */ - exports.linkify = function (terminal, lenient, target) { - var rows = terminal.rowContainer.children; - - lenient = (typeof lenient == "boolean") ? lenient : true; - for (var i=0; i Date: Thu, 9 Feb 2017 20:12:35 -0800 Subject: [PATCH 10/21] Add license --- src/EscapeSequences.ts | 4 ++++ src/Linkifier.ts | 23 +++++++++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/EscapeSequences.ts b/src/EscapeSequences.ts index 34dfde90..9542658f 100644 --- a/src/EscapeSequences.ts +++ b/src/EscapeSequences.ts @@ -1,3 +1,7 @@ +/** + * @license MIT + */ + /** * C0 control codes * See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes diff --git a/src/Linkifier.ts b/src/Linkifier.ts index e502289e..693b1429 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -1,9 +1,10 @@ /** - * The time to wait after a row is changed before it is linkified. This prevents - * the costly operation of searching every row multiple times, pntentially a - * huge aount of times. + * @license MIT */ -const TIME_BEFORE_LINKIFY = 200; + +export type LinkHandler = (uri: string) => void; + +type LinkMatcher = {id: number, regex: RegExp, handler: LinkHandler}; const protocolClause = '(https?:\\/\\/)'; const domainCharacterSet = '[\\da-z\\.-]+'; @@ -20,12 +21,18 @@ const start = '(?:^|' + negatedDomainCharacterSet + ')('; const end = ')($|' + negatedPathCharacterSet + ')'; const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); -export type LinkHandler = (uri: string) => void; - -type LinkMatcher = {id: number, regex: RegExp, handler: LinkHandler}; - +/** + * The ID of the built in http(s) link matcher. + */ const HYPERTEXT_LINK_MATCHER_ID = 0; +/** + * The time to wait after a row is changed before it is linkified. This prevents + * the costly operation of searching every row multiple times, pntentially a + * huge aount of times. + */ +const TIME_BEFORE_LINKIFY = 200; + /** * The Linkifier applies links to rows shortly after they have been refreshed. */ From a1d71c9198a79f7d644edf41b29af9836f543fee Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Feb 2017 20:35:06 -0800 Subject: [PATCH 11/21] Properly queue linkify after refresh --- src/xterm.js | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index f858af31..2a26aac0 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -548,6 +548,9 @@ Terminal.bindKeys = function(term) { on(term.textarea, 'compositionupdate', term.compositionHelper.compositionupdate.bind(term.compositionHelper)); on(term.textarea, 'compositionend', term.compositionHelper.compositionend.bind(term.compositionHelper)); term.on('refresh', term.compositionHelper.updateCompositionElements.bind(term.compositionHelper)); + term.on('refresh', function (data) { + term.queueLinkification(data.start, data.end) + }); }; @@ -1057,22 +1060,27 @@ Terminal.prototype.destroy = function() { /** * Tells the renderer to refresh terminal content between two rows (inclusive) at the next * opportunity. - * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) - * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) + * @param {number} start The row to start from (between 0 and this.rows - 1). + * @param {number} end The row to end at (between start and this.rows - 1). */ Terminal.prototype.refresh = function(start, end) { if (this.renderer) { this.renderer.queueRefresh(start, end); + } +}; - // TODO: DO this better - if (!this.linkifier) { - return; - } +/** + * Queues linkification for the specified rows. + * @param {number} start The row to start from (between 0 and this.rows - 1). + * @param {number} end The row to end at (between start and this.rows - 1). + */ +Terminal.prototype.queueLinkification = function(start, end) { + if (this.linkifier) { for (let i = start; i <= end; i++) { this.linkifier.linkifyRow(i); } } -}; +} /** * Display the cursor element @@ -1274,7 +1282,12 @@ Terminal.prototype.attachCustomKeydownHandler = function(customKeydownHandler) { this.customKeydownHandler = customKeydownHandler; } -// TODO: Doc +/** + * Attaches a http(s) link handler, forcing web links to behave differently to + * regular tags. This will trigger a refresh as links potentially need to be + * reconstructed. Calling this with null will remove the handler. + * @param {LinkHandler} handler The handler callback function. + */ Terminal.prototype.attachHypertextLinkHandler = function(handler) { if (!this.linkifier) { throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); From c8bb32165484d007bec1cbbd4c80234c93599405 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Feb 2017 21:08:50 -0800 Subject: [PATCH 12/21] Get custom link matcher working --- src/Linkifier.ts | 27 +++++++++++++++------------ src/xterm.js | 26 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 693b1429..55b9f186 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -4,7 +4,7 @@ export type LinkHandler = (uri: string) => void; -type LinkMatcher = {id: number, regex: RegExp, handler: LinkHandler}; +type LinkMatcher = {id: number, regex: RegExp, matchIndex?: number, handler: LinkHandler}; const protocolClause = '(https?:\\/\\/)'; const domainCharacterSet = '[\\da-z\\.-]+'; @@ -47,7 +47,7 @@ export class Linkifier { this._rows = rows; this._rowTimeoutIds = []; this._linkMatchers = []; - this.registerLinkMatcher(strictUrlRegex, null); + this.registerLinkMatcher(strictUrlRegex, null, 1); } /** @@ -77,16 +77,19 @@ export class Linkifier { * handled. * @param {RegExp} regex The regular expression the search for. * @param {LinkHandler} handler The callback when the link is called. + * @param {number} matchIndex The index of the link from the regex.match(html) + * call. This defaults to 0 (for regular expressions without capture groups). * @return {number} The ID of the new matcher, this can be used to deregister. */ - public registerLinkMatcher(regex: RegExp, handler: LinkHandler): number { + public registerLinkMatcher(regex: RegExp, handler: LinkHandler, matchIndex?: number): number { if (Linkifier._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) { throw new Error('handler cannot be falsy'); } const matcher: LinkMatcher = { id: Linkifier._nextLinkMatcherId++, regex, - handler + handler, + matchIndex }; this._linkMatchers.push(matcher); return matcher.id; @@ -113,7 +116,7 @@ export class Linkifier { const rowHtml = this._rows[rowIndex].innerHTML; for (let i = 0; i < this._linkMatchers.length; i++) { const matcher = this._linkMatchers[i]; - const uri = this._findLinkMatch(rowHtml, matcher.regex); + const uri = this._findLinkMatch(rowHtml, matcher.regex, matcher.matchIndex); if (uri) { this._doLinkifyRow(rowIndex, uri, matcher.handler); // Only allow a single LinkMatcher to trigger on any given row. @@ -135,11 +138,6 @@ export class Linkifier { const node = nodes[i]; const searchIndex = node.textContent.indexOf(uri); if (searchIndex >= 0) { - if (node.childNodes.length > 0) { - // This row has already been linkified - return; - } - const linkElement = this._createAnchorElement(uri, handler); if (node.textContent.trim().length === uri.length) { // Matches entire string @@ -147,6 +145,10 @@ export class Linkifier { this._replaceNode(node, linkElement); } else { const element = (node); + if (element.nodeName === 'A') { + // This row has already been linkified + return; + } element.innerHTML = ''; element.appendChild(linkElement); } @@ -161,14 +163,15 @@ export class Linkifier { /** * Finds a link match in a piece of HTML. * @param {string} html The HTML to search. + * @param {number} matchIndex The regex match index of the link. * @return {string} The matching URI or null if not found. */ - private _findLinkMatch(html: string, regex: RegExp): string { + private _findLinkMatch(html: string, regex: RegExp, matchIndex?: number): string { const match = html.match(regex); if (!match || match.length === 0) { return null; } - return match[1]; + return match[typeof matchIndex !== 'number' ? 0 : matchIndex]; } /** diff --git a/src/xterm.js b/src/xterm.js index 2a26aac0..c19b91a1 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1297,6 +1297,32 @@ Terminal.prototype.attachHypertextLinkHandler = function(handler) { this.refresh(0, this.rows - 1); } + +/** + * Registers a link matcher, allowing custom link patterns to be matched and + * handled. + * @param {RegExp} regex The regular expression the search for. + * @param {LinkHandler} handler The callback when the link is called. + * @param {number} matchIndex The index of the link from the regex.match(html) + * call. This defaults to 0 (for regular expressions without capture groups). + * @return {number} The ID of the new matcher, this can be used to deregister. + */ +Terminal.prototype.registerLinkMatcher = function(regex, handler, matchIndex) { + if (this.linkifier) { + return this.linkifier.registerLinkMatcher(regex, handler, matchIndex); + } +} + +/** + * Deregisters a link matcher if it has been registered. + * @param {number} matcherId The link matcher's ID (returned after register) + */ +Terminal.prototype.deregisterLinkMatcher = function(matcherId) { + if (this.linkifier) { + this.linkifier.deregisterLinkMatcher(matcherId); + } +} + /** * Handle a keydown event * Key Resources: From 1c030f57ef12b7a50a68d75ba34c29183feac372 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Feb 2017 21:12:28 -0800 Subject: [PATCH 13/21] Force a refresh after register/deregister link matcher --- src/Linkifier.ts | 5 ++++- src/xterm.js | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 55b9f186..29b1a953 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -98,14 +98,17 @@ export class Linkifier { /** * Deregisters a link matcher if it has been registered. * @param {number} matcherId The link matcher's ID (returned after register) + * @return {boolean} Whether a link matcher was found and deregistered. */ - public deregisterLinkMatcher(matcherId: number): void { + public deregisterLinkMatcher(matcherId: number): boolean { // ID 0 is the hypertext link matcher which cannot be deregistered for (let i = 1; i < this._linkMatchers.length; i++) { if (this._linkMatchers[i].id === matcherId) { this._linkMatchers.splice(i, 1); + return true; } } + return false; } /** diff --git a/src/xterm.js b/src/xterm.js index c19b91a1..a353dc2e 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1309,7 +1309,9 @@ Terminal.prototype.attachHypertextLinkHandler = function(handler) { */ Terminal.prototype.registerLinkMatcher = function(regex, handler, matchIndex) { if (this.linkifier) { - return this.linkifier.registerLinkMatcher(regex, handler, matchIndex); + var matcherId = this.linkifier.registerLinkMatcher(regex, handler, matchIndex); + this.refresh(0, this.rows - 1); + return matcherId; } } @@ -1319,7 +1321,9 @@ Terminal.prototype.registerLinkMatcher = function(regex, handler, matchIndex) { */ Terminal.prototype.deregisterLinkMatcher = function(matcherId) { if (this.linkifier) { - this.linkifier.deregisterLinkMatcher(matcherId); + if (this.linkifier.deregisterLinkMatcher(matcherId)) { + this.refresh(0, this.rows - 1); + } } } From 1ee774d01dfa9d1f781488c6a9560e16fc297f7c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Feb 2017 22:03:31 -0800 Subject: [PATCH 14/21] Remove linkify test --- src/Linkifier.ts | 16 ++++++----- src/test/addons/linkify-test.js | 48 --------------------------------- 2 files changed, 9 insertions(+), 55 deletions(-) delete mode 100644 src/test/addons/linkify-test.js diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 29b1a953..394d9ba7 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -75,7 +75,9 @@ export class Linkifier { /** * Registers a link matcher, allowing custom link patterns to be matched and * handled. - * @param {RegExp} regex The regular expression the search for. + * @param {RegExp} regex The regular expression the search for, specifically + * this searches the textContent of the rows. You will want to use \s to match + * a space ' ' character for example. * @param {LinkHandler} handler The callback when the link is called. * @param {number} matchIndex The index of the link from the regex.match(html) * call. This defaults to 0 (for regular expressions without capture groups). @@ -116,10 +118,10 @@ export class Linkifier { * @param {number} rowIndex The index of the row to linkify. */ private _linkifyRow(rowIndex: number): void { - const rowHtml = this._rows[rowIndex].innerHTML; + const text = this._rows[rowIndex].textContent; for (let i = 0; i < this._linkMatchers.length; i++) { const matcher = this._linkMatchers[i]; - const uri = this._findLinkMatch(rowHtml, matcher.regex, matcher.matchIndex); + const uri = this._findLinkMatch(text, matcher.regex, matcher.matchIndex); if (uri) { this._doLinkifyRow(rowIndex, uri, matcher.handler); // Only allow a single LinkMatcher to trigger on any given row. @@ -164,13 +166,13 @@ export class Linkifier { } /** - * Finds a link match in a piece of HTML. - * @param {string} html The HTML to search. + * Finds a link match in a piece of text. + * @param {string} text The text to search. * @param {number} matchIndex The regex match index of the link. * @return {string} The matching URI or null if not found. */ - private _findLinkMatch(html: string, regex: RegExp, matchIndex?: number): string { - const match = html.match(regex); + private _findLinkMatch(text: string, regex: RegExp, matchIndex?: number): string { + const match = text.match(regex); if (!match || match.length === 0) { return null; } diff --git a/src/test/addons/linkify-test.js b/src/test/addons/linkify-test.js deleted file mode 100644 index 285bfbb1..00000000 --- a/src/test/addons/linkify-test.js +++ /dev/null @@ -1,48 +0,0 @@ -var assert = require('chai').assert; -var Terminal = require('../../xterm'); -var linkify = require('../../addons/linkify/linkify'); - -describe('linkify addon', function () { - var xterm; - - describe('API', function () { - it('should define Terminal.prototype.linkify', function () { - assert.isDefined(Terminal.prototype.linkify); - }); - it('should define Terminal.prototype.linkifyTerminalLine', function () { - assert.isDefined(Terminal.prototype.linkifyTerminalLine); - }); - }); - - describe('findUrlMatchOnLine', function () { - describe('strict regex', function () { - it('should match when the entire text is a match', function () { - assert.equal(linkify.findLinkMatch('http://github.com', false), 'http://github.com'); - assert.equal(linkify.findLinkMatch('http://127.0.0.1', false), 'http://127.0.0.1'); - }); - it('should match simple domains', function () { - assert.equal(linkify.findLinkMatch('foo http://github.com bar', false), 'http://github.com'); - assert.equal(linkify.findLinkMatch('foo http://www.github.com bar', false), 'http://www.github.com'); - assert.equal(linkify.findLinkMatch('foo https://github.com bar', false), 'https://github.com'); - assert.equal(linkify.findLinkMatch('foo https://www.github.com bar', false), 'https://www.github.com'); - }); - it('should match web addresses with alpha paths', function () { - assert.equal(linkify.findLinkMatch('foo http://github.com/a/b/c bar', false), 'http://github.com/a/b/c'); - assert.equal(linkify.findLinkMatch('foo http://www.github.com/a/b/c bar', false), 'http://www.github.com/a/b/c'); - }); - it('should not include whitespace surrounding a match', function () { - assert.equal(linkify.findLinkMatch(' http://github.com', false), 'http://github.com'); - assert.equal(linkify.findLinkMatch('http://github.com ', false), 'http://github.com'); - assert.equal(linkify.findLinkMatch(' http://github.com ', false), 'http://github.com'); - }); - it('should match IP addresses', function () { - assert.equal(linkify.findLinkMatch('foo http://127.0.0.1 bar', false), 'http://127.0.0.1'); - assert.equal(linkify.findLinkMatch('foo https://127.0.0.1 bar', false), 'https://127.0.0.1'); - }); - it('should match ports on both domains and IP addresses', function () { - assert.equal(linkify.findLinkMatch('foo http://127.0.0.1:8080 bar', false), 'http://127.0.0.1:8080'); - assert.equal(linkify.findLinkMatch('foo http://www.github.com:8080 bar', false), 'http://www.github.com:8080'); - }); - }); - }); -}); From 5183332f6e7b3d460cb8135905713180e398ec21 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 10 Feb 2017 12:29:36 -0800 Subject: [PATCH 15/21] Next link matcher ID must be non-static --- src/Linkifier.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 394d9ba7..8bd300cc 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -37,11 +37,10 @@ const TIME_BEFORE_LINKIFY = 200; * The Linkifier applies links to rows shortly after they have been refreshed. */ export class Linkifier { - private static _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; - private _rows: HTMLElement[]; private _rowTimeoutIds: number[]; private _linkMatchers: LinkMatcher[]; + private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; constructor(rows: HTMLElement[]) { this._rows = rows; @@ -84,11 +83,11 @@ export class Linkifier { * @return {number} The ID of the new matcher, this can be used to deregister. */ public registerLinkMatcher(regex: RegExp, handler: LinkHandler, matchIndex?: number): number { - if (Linkifier._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) { + if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) { throw new Error('handler cannot be falsy'); } const matcher: LinkMatcher = { - id: Linkifier._nextLinkMatcherId++, + id: this._nextLinkMatcherId++, regex, handler, matchIndex From 3b62aa444bb1ddac26981936c358b2dc7c33d9a7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 14 Feb 2017 19:45:08 -0800 Subject: [PATCH 16/21] Improve jsdoc --- src/Linkifier.ts | 2 +- src/xterm.js | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 8bd300cc..9da532dd 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -74,7 +74,7 @@ export class Linkifier { /** * Registers a link matcher, allowing custom link patterns to be matched and * handled. - * @param {RegExp} regex The regular expression the search for, specifically + * @param {RegExp} regex The regular expression to search for, specifically * this searches the textContent of the rows. You will want to use \s to match * a space ' ' character for example. * @param {LinkHandler} handler The callback when the link is called. diff --git a/src/xterm.js b/src/xterm.js index a353dc2e..358040a0 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1299,13 +1299,15 @@ Terminal.prototype.attachHypertextLinkHandler = function(handler) { /** - * Registers a link matcher, allowing custom link patterns to be matched and - * handled. - * @param {RegExp} regex The regular expression the search for. - * @param {LinkHandler} handler The callback when the link is called. - * @param {number} matchIndex The index of the link from the regex.match(html) - * call. This defaults to 0 (for regular expressions without capture groups). - * @return {number} The ID of the new matcher, this can be used to deregister. + * Registers a link matcher, allowing custom link patterns to be matched and + * handled. + * @param {RegExp} regex The regular expression to search for, specifically + * this searches the textContent of the rows. You will want to use \s to match + * a space ' ' character for example. + * @param {LinkHandler} handler The callback when the link is called. + * @param {number} matchIndex The index of the link from the regex.match(html) + * call. This defaults to 0 (for regular expressions without capture groups). + * @return {number} The ID of the new matcher, this can be used to deregister. */ Terminal.prototype.registerLinkMatcher = function(regex, handler, matchIndex) { if (this.linkifier) { From c3f31b5f6535130e0d5dc1e63a395dbf775d6a99 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 14 Feb 2017 20:04:51 -0800 Subject: [PATCH 17/21] Apply link matchers in reverse This enables consumers to have full control. --- src/Linkifier.ts | 4 ++-- src/xterm.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 9da532dd..a2a5a5da 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -78,7 +78,7 @@ export class Linkifier { * this searches the textContent of the rows. You will want to use \s to match * a space ' ' character for example. * @param {LinkHandler} handler The callback when the link is called. - * @param {number} matchIndex The index of the link from the regex.match(html) + * @param {number} matchIndex The index of the link from the regex.match(text) * call. This defaults to 0 (for regular expressions without capture groups). * @return {number} The ID of the new matcher, this can be used to deregister. */ @@ -118,7 +118,7 @@ export class Linkifier { */ private _linkifyRow(rowIndex: number): void { const text = this._rows[rowIndex].textContent; - for (let i = 0; i < this._linkMatchers.length; i++) { + for (let i = this._linkMatchers.length - 1; i >= 0; i--) { const matcher = this._linkMatchers[i]; const uri = this._findLinkMatch(text, matcher.regex, matcher.matchIndex); if (uri) { diff --git a/src/xterm.js b/src/xterm.js index 358040a0..a6e756b2 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1305,7 +1305,7 @@ Terminal.prototype.attachHypertextLinkHandler = function(handler) { * this searches the textContent of the rows. You will want to use \s to match * a space ' ' character for example. * @param {LinkHandler} handler The callback when the link is called. - * @param {number} matchIndex The index of the link from the regex.match(html) + * @param {number} matchIndex The index of the link from the regex.match(text) * call. This defaults to 0 (for regular expressions without capture groups). * @return {number} The ID of the new matcher, this can be used to deregister. */ From c4f431849bc0665f32efa005c5f2ef938c920a48 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 16 Feb 2017 22:41:08 -0800 Subject: [PATCH 18/21] Add a null check on linkifyRow --- src/Linkifier.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index a2a5a5da..b5b3247c 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -117,7 +117,11 @@ export class Linkifier { * @param {number} rowIndex The index of the row to linkify. */ private _linkifyRow(rowIndex: number): void { - const text = this._rows[rowIndex].textContent; + const row = this._rows[rowIndex]; + if (!row) { + return; + } + const text = row.textContent; for (let i = this._linkMatchers.length - 1; i >= 0; i--) { const matcher = this._linkMatchers[i]; const uri = this._findLinkMatch(text, matcher.regex, matcher.matchIndex); From 3f69da2455452ba5285d476c3b6e92d231789b64 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 17 Feb 2017 10:06:35 -0800 Subject: [PATCH 19/21] Support query string in linkifier --- src/Linkifier.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index b5b3247c..74c03adc 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -15,8 +15,9 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; const portClause = '(:\\d{1,5})'; const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?'; const pathClause = '(\\/[\\/\\w\\.-]*)*'; +const queryStringClause = '(\\?[\\w\\[\\]\\(\\)\\/\\?\\!#@$&\'*+,:;]*)?'; const negatedPathCharacterSet = '[^\\/\\w\\.-]+'; -const bodyClause = hostClause + pathClause; +const bodyClause = hostClause + pathClause + queryStringClause; const start = '(?:^|' + negatedDomainCharacterSet + ')('; const end = ')($|' + negatedPathCharacterSet + ')'; const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); From a10ee8f58c5d97f7e520872a2c5ebc5c03abc295 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 17 Feb 2017 10:18:37 -0800 Subject: [PATCH 20/21] Revert priority change, http handler needs to be top --- src/Linkifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 74c03adc..6bc1d86f 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -123,7 +123,7 @@ export class Linkifier { return; } const text = row.textContent; - for (let i = this._linkMatchers.length - 1; i >= 0; i--) { + for (let i = 0; i < this._linkMatchers.length; i--) { const matcher = this._linkMatchers[i]; const uri = this._findLinkMatch(text, matcher.regex, matcher.matchIndex); if (uri) { From e6fc80c13b2e0769614f20ec8c3812f158df5951 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 17 Feb 2017 10:22:25 -0800 Subject: [PATCH 21/21] Fix link matcher iteration direction --- src/Linkifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 6bc1d86f..04871e80 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -123,7 +123,7 @@ export class Linkifier { return; } const text = row.textContent; - for (let i = 0; i < this._linkMatchers.length; i--) { + for (let i = 0; i < this._linkMatchers.length; i++) { const matcher = this._linkMatchers[i]; const uri = this._findLinkMatch(text, matcher.regex, matcher.matchIndex); if (uri) {