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
new file mode 100644
index 00000000..04871e80
--- /dev/null
+++ b/src/Linkifier.ts
@@ -0,0 +1,259 @@
+/**
+ * @license MIT
+ */
+
+export type LinkHandler = (uri: string) => void;
+
+type LinkMatcher = {id: number, regex: RegExp, matchIndex?: number, handler: LinkHandler};
+
+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 queryStringClause = '(\\?[\\w\\[\\]\\(\\)\\/\\?\\!#@$&\'*+,:;]*)?';
+const negatedPathCharacterSet = '[^\\/\\w\\.-]+';
+const bodyClause = hostClause + pathClause + queryStringClause;
+const start = '(?:^|' + negatedDomainCharacterSet + ')(';
+const end = ')($|' + negatedPathCharacterSet + ')';
+const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
+
+/**
+ * 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.
+ */
+export class Linkifier {
+ private _rows: HTMLElement[];
+ private _rowTimeoutIds: number[];
+ private _linkMatchers: LinkMatcher[];
+ private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
+
+ constructor(rows: HTMLElement[]) {
+ this._rows = rows;
+ this._rowTimeoutIds = [];
+ this._linkMatchers = [];
+ this.registerLinkMatcher(strictUrlRegex, null, 1);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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.
+ */
+ public attachHypertextLinkHandler(handler: LinkHandler): void {
+ 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 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(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.
+ */
+ public registerLinkMatcher(regex: RegExp, handler: LinkHandler, matchIndex?: number): number {
+ if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
+ throw new Error('handler cannot be falsy');
+ }
+ const matcher: LinkMatcher = {
+ id: this._nextLinkMatcherId++,
+ regex,
+ handler,
+ matchIndex
+ };
+ 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)
+ * @return {boolean} Whether a link matcher was found and deregistered.
+ */
+ 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;
+ }
+
+ /**
+ * Linkifies a row.
+ * @param {number} rowIndex The index of the row to linkify.
+ */
+ private _linkifyRow(rowIndex: number): void {
+ const row = this._rows[rowIndex];
+ if (!row) {
+ return;
+ }
+ const text = row.textContent;
+ 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) {
+ 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++) {
+ const node = nodes[i];
+ const searchIndex = node.textContent.indexOf(uri);
+ if (searchIndex >= 0) {
+ const linkElement = this._createAnchorElement(uri, handler);
+ if (node.textContent.trim().length === uri.length) {
+ // Matches entire string
+ if (node.nodeType === Node.TEXT_NODE) {
+ this._replaceNode(node, linkElement);
+ } else {
+ const element = (node);
+ if (element.nodeName === 'A') {
+ // This row has already been linkified
+ return;
+ }
+ element.innerHTML = '';
+ element.appendChild(linkElement);
+ }
+ } else {
+ // Matches part of string
+ this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex);
+ }
+ }
+ }
+ }
+
+ /**
+ * 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(text: string, regex: RegExp, matchIndex?: number): string {
+ const match = text.match(regex);
+ if (!match || match.length === 0) {
+ return null;
+ }
+ return match[typeof matchIndex !== 'number' ? 0 : matchIndex];
+ }
+
+ /**
+ * Creates a link anchor element.
+ * @param {string} uri The uri of the link.
+ * @return {HTMLAnchorElement} The link.
+ */
+ private _createAnchorElement(uri: string, handler: LinkHandler): HTMLAnchorElement {
+ const element = document.createElement('a');
+ element.textContent = uri;
+ if (handler) {
+ element.addEventListener('click', () => handler(uri));
+ } else {
+ element.href = uri;
+ // Force link on another tab so work is not lost
+ element.target = '_blank';
+ }
+ 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; 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.
+ */
+ 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 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');
+ }
+
+ const fullText = node.textContent;
+
+ if (substringIndex === 0) {
+ // 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
+ const leftText = fullText.substring(0, substringIndex);
+ const leftTextNode = document.createTextNode(leftText);
+ this._replaceNode(node, leftTextNode, newNode);
+ } else {
+ // 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);
+ }
+ }
+}
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 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');
+ }
+ this.linkifier.attachHypertextLinkHandler(handler);
+ // Refresh to force links to refresh
+ 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 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(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.
+ */
+Terminal.prototype.registerLinkMatcher = function(regex, handler, matchIndex) {
+ if (this.linkifier) {
+ var matcherId = this.linkifier.registerLinkMatcher(regex, handler, matchIndex);
+ this.refresh(0, this.rows - 1);
+ return matcherId;
+ }
+}
+
+/**
+ * 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) {
+ if (this.linkifier.deregisterLinkMatcher(matcherId)) {
+ this.refresh(0, this.rows - 1);
+ }
+ }
+}
+
/**
* Handle a keydown event
* Key Resources: