diff --git a/demo/main.js b/demo/main.js
index 7feb7939..d9c0151a 100644
--- a/demo/main.js
+++ b/demo/main.js
@@ -3,6 +3,7 @@ import * as attach from '../build/addons/attach/attach';
import * as fit from '../build/addons/fit/fit';
import * as fullscreen from '../build/addons/fullscreen/fullscreen';
import * as search from '../build/addons/search/search';
+import * as webLinks from '../build/addons/webLinks/webLinks';
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
@@ -10,6 +11,7 @@ Terminal.applyAddon(attach);
Terminal.applyAddon(fit);
Terminal.applyAddon(fullscreen);
Terminal.applyAddon(search);
+Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat);
@@ -121,6 +123,7 @@ function createTerminal() {
term.open(terminalContainer);
term.winptyCompatInit();
+ term.webLinksInit();
term.fit();
term.focus();
diff --git a/gulpfile.js b/gulpfile.js
index 4353e46d..af4fdce0 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -23,7 +23,7 @@ const tsProject = ts.createProject('tsconfig.json');
const srcDir = tsProject.config.compilerOptions.rootDir;
let outDir = tsProject.config.compilerOptions.outDir;
-const addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem'];
+const addons = fs.readdirSync(`${__dirname}/src/addons`);
// Under some environments like TravisCI, this comes out at absolute which can
// break the build. This ensures that the outDir is absolute.
diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts
index 14b1ac0f..ce1635a3 100644
--- a/src/Linkifier.test.ts
+++ b/src/Linkifier.test.ts
@@ -101,12 +101,6 @@ describe('Linkifier', () => {
linkifier.attachToDom(mouseZoneManager);
});
- describe('http links', () => {
- it('should allow ~ character in URI path', (done) => {
- assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done);
- });
- });
-
describe('link matcher', () => {
it('should match a single link', done => {
assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done);
@@ -200,19 +194,19 @@ describe('Linkifier', () => {
it('should order the list from highest priority to lowest #1', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 });
- assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]);
+ assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]);
});
it('should order the list from highest priority to lowest #2', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 });
- assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]);
+ assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, aId]);
});
it('should order items of equal priority in the order they are added', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 });
- assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]);
+ assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]);
});
});
});
diff --git a/src/Linkifier.ts b/src/Linkifier.ts
index a9617952..92a6ca9f 100644
--- a/src/Linkifier.ts
+++ b/src/Linkifier.ts
@@ -8,30 +8,6 @@ import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkMatcherValidatio
import { MouseZone } from './input/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
-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 localHostClause = '(localhost)';
-const portClause = '(:\\d{1,5})';
-const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
-const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
-const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
-const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
-const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
-const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
-const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
-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 Linkifier applies links to rows shortly after they have been refreshed.
*/
@@ -47,7 +23,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
private _mouseZoneManager: IMouseZoneManager;
private _rowsTimeoutId: number;
- private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
+ private _nextLinkMatcherId = 0;
private _rowsToLinkify: {start: number, end: number};
constructor(
@@ -58,7 +34,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
start: null,
end: null
};
- this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
}
/**
@@ -111,23 +86,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
this._rowsToLinkify.end = null;
}
- /**
- * Attaches a handler for hypertext links, overriding default behavior for
- * tandard http(s) links.
- * @param handler The handler to use, this can be cleared with null.
- */
- public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
- this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
- }
-
- /**
- * Attaches a validation callback for hypertext links.
- * @param callback The callback to use, this can be cleared with null.
- */
- public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
- this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback;
- }
-
/**
* Registers a link matcher, allowing custom link patterns to be matched and
* handled.
@@ -139,7 +97,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return The ID of the new matcher, this can be used to deregister.
*/
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number {
- if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
+ if (!handler) {
throw new Error('handler must be defined');
}
const matcher: ILinkMatcher = {
@@ -185,8 +143,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return 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++) {
+ for (let i = 0; i < this._linkMatchers.length; i++) {
if (this._linkMatchers[i].id === matcherId) {
this._linkMatchers.splice(i, 1);
return true;
@@ -222,7 +179,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
// Iterate over nodes as we want to consider text nodes
let result = [];
- const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
// Find the first match
let match = text.match(matcher.regex);
diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts
index eaf813c9..93957fa8 100644
--- a/src/SelectionManager.ts
+++ b/src/SelectionManager.ts
@@ -634,6 +634,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param coords The coordinates to get the word at.
*/
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
+ // Ensure coords are within viewport (eg. not within scroll bar)
+ if (coords[0] >= this._terminal.cols) {
+ return null;
+ }
+
const bufferLine = this._buffer.lines.get(coords[1]);
if (!bufferLine) {
return null;
diff --git a/src/Terminal.ts b/src/Terminal.ts
index 721996c9..fe9a1b9c 100644
--- a/src/Terminal.ts
+++ b/src/Terminal.ts
@@ -1326,36 +1326,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.customKeyEventHandler = customKeyEventHandler;
}
- /**
- * 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 handler The handler callback function.
- */
- public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
- if (!this.linkifier) {
- throw new Error('Cannot attach a hypertext link handler before Terminal.open is called');
- }
- this.linkifier.setHypertextLinkHandler(handler);
- // Refresh to force links to refresh
- this.refresh(0, this.rows - 1);
- }
-
- /**
- * Attaches a validation callback for hypertext links. This is useful to use
- * validation logic or to do something with the link's element and url.
- * @param callback The callback to use, this can
- * be cleared with null.
- */
- public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
- if (!this.linkifier) {
- throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called');
- }
- this.linkifier.setHypertextValidationCallback(callback);
- // // 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.
@@ -1367,12 +1337,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @return The ID of the new matcher, this can be used to deregister.
*/
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number {
- if (this.linkifier) {
- const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
- this.refresh(0, this.rows - 1);
- return matcherId;
- }
- return 0;
+ const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
+ this.refresh(0, this.rows - 1);
+ return matcherId;
}
/**
@@ -1380,10 +1347,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @param matcherId The link matcher's ID (returned after register)
*/
public deregisterLinkMatcher(matcherId: number): void {
- if (this.linkifier) {
- if (this.linkifier.deregisterLinkMatcher(matcherId)) {
- this.refresh(0, this.rows - 1);
- }
+ if (this.linkifier.deregisterLinkMatcher(matcherId)) {
+ this.refresh(0, this.rows - 1);
}
}
diff --git a/src/Types.ts b/src/Types.ts
index f0185123..6bc8b963 100644
--- a/src/Types.ts
+++ b/src/Types.ts
@@ -14,7 +14,7 @@ export type XtermListener = (...args: any[]) => void;
export type CharData = [number, string, number, number];
export type LineData = CharData[];
-export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
+export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export enum LinkHoverEventTypes {
@@ -299,8 +299,6 @@ export interface ISelectionManager {
export interface ILinkifier extends IEventEmitter {
attachToDom(mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
- setHypertextLinkHandler(handler: LinkMatcherHandler): void;
- setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
diff --git a/src/addons/webLinks/package.json b/src/addons/webLinks/package.json
new file mode 100644
index 00000000..f200cab4
--- /dev/null
+++ b/src/addons/webLinks/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "xterm.weblinks",
+ "main": "weblinks.js",
+ "private": true
+}
diff --git a/src/addons/webLinks/tsconfig.json b/src/addons/webLinks/tsconfig.json
new file mode 100644
index 00000000..7549370b
--- /dev/null
+++ b/src/addons/webLinks/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es5",
+ "rootDir": ".",
+ "outDir": "../../../lib/addons/webLinks/",
+ "sourceMap": true,
+ "removeComments": true,
+ "declaration": true
+ }
+}
diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts
new file mode 100644
index 00000000..b3c76703
--- /dev/null
+++ b/src/addons/webLinks/webLinks.test.ts
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { assert, expect } from 'chai';
+
+import * as webLinks from './webLinks';
+
+class MockTerminal {
+ public regex: RegExp;
+ public handler: (event: MouseEvent, uri: string) => void;
+ public options?: any;
+
+ public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number {
+ this.regex = regex;
+ this.handler = handler;
+ this.options = options;
+ return 0;
+ }
+}
+
+describe('webLinks addon', () => {
+ describe('apply', () => {
+ it('should do register the `webLinksInit` method', () => {
+ webLinks.apply(MockTerminal);
+ assert.equal(typeof (MockTerminal).prototype.webLinksInit, 'function');
+ });
+ });
+
+ it('should allow ~ character in URI path', () => {
+ const term = new MockTerminal();
+ webLinks.webLinksInit(term);
+
+ const row = ' http://foo.com/a~b#c~d?e~f ';
+
+ let match = row.match(term.regex);
+ let uri = match[term.options.matchIndex];
+
+ assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
+ });
+});
diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts
new file mode 100644
index 00000000..7ff87c60
--- /dev/null
+++ b/src/addons/webLinks/webLinks.ts
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+///
+
+import { Terminal, ILinkMatcherOptions } from 'xterm';
+
+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 localHostClause = '(localhost)';
+const portClause = '(:\\d{1,5})';
+const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
+const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
+const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
+const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
+const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
+const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
+const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
+const start = '(?:^|' + negatedDomainCharacterSet + ')(';
+const end = ')($|' + negatedPathCharacterSet + ')';
+const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
+
+function handleLink(event: MouseEvent, uri: string): void {
+ window.open(uri, '_blank');
+}
+
+/**
+ * Initialize the web links addon, registering the link matcher.
+ * @param term The terminal to use web links within.
+ * @param handler A custom handler to use.
+ * @param options Custom options to use, matchIndex will always be ignored.
+ */
+export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void {
+ options.matchIndex = 1;
+ term.registerLinkMatcher(strictUrlRegex, handler, options);
+}
+
+export function apply(terminalConstructor: typeof Terminal): void {
+ (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void {
+ webLinksInit(this, handler, options);
+ };
+}
diff --git a/src/addons/winptyCompat/package.json b/src/addons/winptyCompat/package.json
index 2e9fc164..fc929497 100644
--- a/src/addons/winptyCompat/package.json
+++ b/src/addons/winptyCompat/package.json
@@ -1,5 +1,5 @@
{
- "name": "xterm.winptyCompat",
+ "name": "xterm.winptycompat",
"main": "winptyCompat.js",
"private": true
}
diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts
index 32c8b0a0..0e8fdcc7 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -395,7 +395,7 @@ declare module 'xterm' {
* @param options Options for the link matcher.
* @return The ID of the new matcher, this can be used to deregister.
*/
- registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number;
+ registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
/**
* (EXPERIMENTAL) Deregisters a link matcher if it has been registered.