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); + } } };