Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1619e42fd | ||
|
|
4cfec2fc09 | ||
|
|
235ae2fbb9 | ||
|
|
1f518b0b45 | ||
|
|
ab34d53d6b | ||
|
|
aafb5333b9 | ||
|
|
5e2791f981 | ||
|
|
1ecc5674ac | ||
|
|
c9ad3a595a | ||
|
|
43558fa1dc | ||
|
|
15a867d2b7 | ||
|
|
012051c1db | ||
|
|
b5483898da | ||
|
|
c7b4c2beaf | ||
|
|
8693d067e3 | ||
|
|
78d5fc9506 | ||
|
|
7ac4f1a9be | ||
|
|
670fb01fbf | ||
|
|
ec9608d54b | ||
|
|
dcffaf1c8c | ||
|
|
efac5293c0 | ||
|
|
c7563346a2 | ||
|
|
69906532b1 | ||
|
|
9404fae725 | ||
|
|
3542f15575 | ||
|
|
15d79143e2 | ||
|
|
f6dafee748 | ||
|
|
356d86ae74 | ||
|
|
e673c6ec97 | ||
|
|
3e5fcf7004 | ||
|
|
96d2a2bc68 | ||
|
|
b401992178 | ||
|
|
4c34351383 | ||
|
|
202b54afbb | ||
|
|
26ebc3d944 | ||
|
|
d0f36e9939 | ||
|
|
4c99c032ca | ||
|
|
6198556e50 | ||
|
|
514eba0e4e | ||
|
|
7279ee0f5f | ||
|
|
ccadf3fcca | ||
|
|
cb80ce0d2c | ||
|
|
9aae43961f | ||
|
|
b810687163 | ||
|
|
28d4ec779d | ||
|
|
cf4da6c748 | ||
|
|
e569b5669a | ||
|
|
695a9523fe | ||
|
|
01b8e28a7d | ||
|
|
e221030a2b | ||
|
|
88dc697392 | ||
|
|
99a270216c | ||
|
|
d06c920a2d | ||
|
|
5452986ce3 | ||
|
|
ae69841174 | ||
|
|
e6fc80c13b | ||
|
|
a10ee8f58c | ||
|
|
3f69da2455 | ||
|
|
a14a0834e9 | ||
|
|
c4f431849b | ||
|
|
ade3389cea | ||
|
|
6873ba0909 | ||
|
|
c3f31b5f65 | ||
|
|
3b62aa444b | ||
|
|
fb2b10b565 | ||
|
|
345553012f | ||
|
|
fddc13d98e | ||
|
|
aeb9f798c9 | ||
|
|
5183332f6e | ||
|
|
c53183d94b | ||
|
|
1ee774d01d | ||
|
|
1c030f57ef | ||
|
|
c8bb321654 | ||
|
|
a1d71c9198 | ||
|
|
55cb43d70b | ||
|
|
551159e0c3 | ||
|
|
3bf31aa478 | ||
|
|
7167b06bf8 | ||
|
|
34582f9f65 | ||
|
|
8dc9ec5733 | ||
|
|
e8adf8ad2b | ||
|
|
1306431e7c | ||
|
|
f7bc0fba4d | ||
|
|
0f3ee21d9d | ||
|
|
a489037ec4 | ||
|
|
2207d356f2 |
@@ -13,3 +13,6 @@ npm-debug.log
|
|||||||
.env
|
.env
|
||||||
build/
|
build/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# Directories needed for code coverage
|
||||||
|
/coverage/
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ addons:
|
|||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
script: npm run $NPM_COMMAND
|
script: npm run $NPM_COMMAND
|
||||||
|
after_success: npm run coveralls
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
|
|||||||
Bill Church <billchurch@users.noreply.github.com>
|
Bill Church <billchurch@users.noreply.github.com>
|
||||||
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
|
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
|
||||||
bottleofwater <nison.mael+bottleofwater@gmail.com>
|
bottleofwater <nison.mael+bottleofwater@gmail.com>
|
||||||
|
Brian Mock <brian@mockbrian.com>
|
||||||
Carson Anderson <carson@betterservers.com>
|
Carson Anderson <carson@betterservers.com>
|
||||||
Christian Budde Christensen <budde377@gmail.com>
|
Christian Budde Christensen <budde377@gmail.com>
|
||||||
Christopher Jeffrey <chjjeffrey@gmail.com>
|
Christopher Jeffrey <chjjeffrey@gmail.com>
|
||||||
|
coderaiser <mnemonic.enemy@gmail.com>
|
||||||
Daniel Imms <daimms@microsoft.com>
|
Daniel Imms <daimms@microsoft.com>
|
||||||
Daniel Risacher <drisacher@gmail.com>
|
Daniel Risacher <drisacher@gmail.com>
|
||||||
Dan Kaplun <dbkaplun@twitch.tv>
|
Dan Kaplun <dbkaplun@twitch.tv>
|
||||||
@@ -28,6 +30,7 @@ Jean Bruenn <himself@jeanbruenn.info>
|
|||||||
Jörg Breitbart <jerch@rockborn.de>
|
Jörg Breitbart <jerch@rockborn.de>
|
||||||
Lucian Buzzo <lucian.buzzo@gmail.com>
|
Lucian Buzzo <lucian.buzzo@gmail.com>
|
||||||
Maël Nison <nison.mael@gmail.com>
|
Maël Nison <nison.mael@gmail.com>
|
||||||
|
Martin Wang <jiahaow@ca.ibm.com>
|
||||||
Mikko Karvonen <mikko.karvonen@arm.com>
|
Mikko Karvonen <mikko.karvonen@arm.com>
|
||||||
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
||||||
Paris Kasidiaris <paris@sourcelair.com>
|
Paris Kasidiaris <paris@sourcelair.com>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# xterm.js
|
# xterm.js
|
||||||
|
|
||||||
[](https://travis-ci.org/sourcelair/xterm.js) [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
[](https://travis-ci.org/sourcelair/xterm.js) [](https://coveralls.io/github/sourcelair/xterm.js) [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
|
|
||||||
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
||||||
|
|
||||||
@@ -48,14 +48,29 @@ Xterm.js works seamlessly in Electron apps and may even work on earlier versions
|
|||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
To launch the demo simply run:
|
### Linux or macOS
|
||||||
|
|
||||||
|
Run the following commands:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install
|
$ npm install
|
||||||
npm start
|
$ npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open http://0.0.0.0:3000 in a web browser (use http://127.0.0.1:3000 if running under Windows).
|
Then open http://0.0.0.0:3000 in a web browser.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run these commands.
|
||||||
|
|
||||||
|
Note: Do not use ConEmu, as it seems to break the demo for some reason.
|
||||||
|
|
||||||
|
```
|
||||||
|
> npm install
|
||||||
|
> npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open http://127.0.0.1:3000 in a web browser.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm.js",
|
"name": "xterm.js",
|
||||||
"version": "2.3.2",
|
"version": "2.4.0",
|
||||||
"ignore": ["demo", "test", ".gitignore"],
|
"ignore": ["demo", "test", ".gitignore"],
|
||||||
"main": [
|
"main": [
|
||||||
"dist/xterm.js",
|
"dist/xterm.js",
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ app.ws('/terminals/:pid', function (ws, req) {
|
|||||||
term.write(msg);
|
term.write(msg);
|
||||||
});
|
});
|
||||||
ws.on('close', function () {
|
ws.on('close', function () {
|
||||||
process.kill(term.pid);
|
term.kill();
|
||||||
console.log('Closed terminal ' + term.pid);
|
console.log('Closed terminal ' + term.pid);
|
||||||
// Clean things up
|
// Clean things up
|
||||||
delete terminals[term.pid];
|
delete terminals[term.pid];
|
||||||
|
|||||||
Vendored
+6
-1
@@ -32,6 +32,9 @@
|
|||||||
var exports = {};
|
var exports = {};
|
||||||
|
|
||||||
exports.proposeGeometry = function (term) {
|
exports.proposeGeometry = function (term) {
|
||||||
|
if (!term.element.parentElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
||||||
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
||||||
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
||||||
@@ -66,7 +69,9 @@
|
|||||||
exports.fit = function (term) {
|
exports.fit = function (term) {
|
||||||
var geometry = exports.proposeGeometry(term);
|
var geometry = exports.proposeGeometry(term);
|
||||||
|
|
||||||
term.resize(geometry.cols, geometry.rows);
|
if (geometry) {
|
||||||
|
term.resize(geometry.cols, geometry.rows);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Xterm.prototype.proposeGeometry = function () {
|
Xterm.prototype.proposeGeometry = function () {
|
||||||
|
|||||||
Vendored
+17
-5
@@ -71,6 +71,21 @@
|
|||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal a:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal a.xterm-invalid-link:hover {
|
||||||
|
cursor: text;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
|
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -87,13 +102,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes xterm-cursor-blink {
|
@keyframes xterm-cursor-blink {
|
||||||
0% {
|
0% { }
|
||||||
background-color: #fff;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
50% {
|
50% {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: #FFF;
|
color: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+260
-17
@@ -1163,7 +1163,7 @@ var InputHandler = (function () {
|
|||||||
this._terminal.cursorHidden = false;
|
this._terminal.cursorHidden = false;
|
||||||
this._terminal.insertMode = false;
|
this._terminal.insertMode = false;
|
||||||
this._terminal.originMode = false;
|
this._terminal.originMode = false;
|
||||||
this._terminal.wraparoundMode = false;
|
this._terminal.wraparoundMode = true;
|
||||||
this._terminal.applicationKeypad = false;
|
this._terminal.applicationKeypad = false;
|
||||||
this._terminal.viewport.syncScrollArea();
|
this._terminal.viewport.syncScrollArea();
|
||||||
this._terminal.applicationCursor = false;
|
this._terminal.applicationCursor = false;
|
||||||
@@ -1314,6 +1314,214 @@ var wcwidth = (function (opts) {
|
|||||||
|
|
||||||
},{"./Charsets":1,"./EscapeSequences":3}],6:[function(require,module,exports){
|
},{"./Charsets":1,"./EscapeSequences":3}],6:[function(require,module,exports){
|
||||||
"use strict";
|
"use strict";
|
||||||
|
var INVALID_LINK_CLASS = 'xterm-invalid-link';
|
||||||
|
var protocolClause = '(https?:\\/\\/)';
|
||||||
|
var domainCharacterSet = '[\\da-z\\.-]+';
|
||||||
|
var negatedDomainCharacterSet = '[^\\da-z\\.-]+';
|
||||||
|
var domainBodyClause = '(' + domainCharacterSet + ')';
|
||||||
|
var tldClause = '([a-z\\.]{2,6})';
|
||||||
|
var ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||||
|
var localHostClause = '(localhost)';
|
||||||
|
var portClause = '(:\\d{1,5})';
|
||||||
|
var hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||||
|
var pathClause = '(\\/[\\/\\w\\.\\-%]*)*';
|
||||||
|
var queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;\\=\\.\\-]*';
|
||||||
|
var queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||||
|
var hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||||
|
var negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
|
||||||
|
var bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
|
||||||
|
var start = '(?:^|' + negatedDomainCharacterSet + ')(';
|
||||||
|
var end = ')($|' + negatedPathCharacterSet + ')';
|
||||||
|
var strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
||||||
|
var HYPERTEXT_LINK_MATCHER_ID = 0;
|
||||||
|
var Linkifier = (function () {
|
||||||
|
function Linkifier(document, rows) {
|
||||||
|
this._nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
|
||||||
|
this._document = document;
|
||||||
|
this._rows = rows;
|
||||||
|
this._rowTimeoutIds = [];
|
||||||
|
this._linkMatchers = [];
|
||||||
|
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
|
||||||
|
}
|
||||||
|
Linkifier.prototype.linkifyRow = function (rowIndex) {
|
||||||
|
var timeoutId = this._rowTimeoutIds[rowIndex];
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
this._rowTimeoutIds[rowIndex] = setTimeout(this._linkifyRow.bind(this, rowIndex), Linkifier.TIME_BEFORE_LINKIFY);
|
||||||
|
};
|
||||||
|
Linkifier.prototype.attachHypertextLinkHandler = function (handler) {
|
||||||
|
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
|
||||||
|
};
|
||||||
|
Linkifier.prototype.registerLinkMatcher = function (regex, handler, options) {
|
||||||
|
if (options === void 0) { options = {}; }
|
||||||
|
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
|
||||||
|
throw new Error('handler must be defined');
|
||||||
|
}
|
||||||
|
var matcher = {
|
||||||
|
id: this._nextLinkMatcherId++,
|
||||||
|
regex: regex,
|
||||||
|
handler: handler,
|
||||||
|
matchIndex: options.matchIndex,
|
||||||
|
validationCallback: options.validationCallback,
|
||||||
|
priority: options.priority || 0
|
||||||
|
};
|
||||||
|
this._addLinkMatcherToList(matcher);
|
||||||
|
return matcher.id;
|
||||||
|
};
|
||||||
|
Linkifier.prototype._addLinkMatcherToList = function (matcher) {
|
||||||
|
if (this._linkMatchers.length === 0) {
|
||||||
|
this._linkMatchers.push(matcher);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var i = this._linkMatchers.length - 1; i >= 0; i--) {
|
||||||
|
if (matcher.priority <= this._linkMatchers[i].priority) {
|
||||||
|
this._linkMatchers.splice(i + 1, 0, matcher);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._linkMatchers.splice(0, 0, matcher);
|
||||||
|
};
|
||||||
|
Linkifier.prototype.deregisterLinkMatcher = function (matcherId) {
|
||||||
|
for (var i = 1; i < this._linkMatchers.length; i++) {
|
||||||
|
if (this._linkMatchers[i].id === matcherId) {
|
||||||
|
this._linkMatchers.splice(i, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
Linkifier.prototype._linkifyRow = function (rowIndex) {
|
||||||
|
var row = this._rows[rowIndex];
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var text = row.textContent;
|
||||||
|
var _loop_1 = function (i) {
|
||||||
|
var matcher = this_1._linkMatchers[i];
|
||||||
|
var uri = this_1._findLinkMatch(text, matcher.regex, matcher.matchIndex);
|
||||||
|
if (uri) {
|
||||||
|
var linkElement_1 = this_1._doLinkifyRow(rowIndex, uri, matcher.handler, matcher.id === HYPERTEXT_LINK_MATCHER_ID);
|
||||||
|
if (linkElement_1 && matcher.validationCallback) {
|
||||||
|
matcher.validationCallback(uri, function (isValid) {
|
||||||
|
if (!isValid) {
|
||||||
|
linkElement_1.classList.add(INVALID_LINK_CLASS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { value: void 0 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var this_1 = this;
|
||||||
|
for (var i = 0; i < this._linkMatchers.length; i++) {
|
||||||
|
var state_1 = _loop_1(i);
|
||||||
|
if (typeof state_1 === "object")
|
||||||
|
return state_1.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Linkifier.prototype._doLinkifyRow = function (rowIndex, uri, handler, isHttpLinkMatcher) {
|
||||||
|
var nodes = this._rows[rowIndex].childNodes;
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
var node = nodes[i];
|
||||||
|
var searchIndex = node.textContent.indexOf(uri);
|
||||||
|
if (searchIndex >= 0) {
|
||||||
|
var linkElement = this._createAnchorElement(uri, handler, isHttpLinkMatcher);
|
||||||
|
if (node.textContent.length === uri.length) {
|
||||||
|
if (node.nodeType === 3) {
|
||||||
|
this._replaceNode(node, linkElement);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var element = node;
|
||||||
|
if (element.nodeName === 'A') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
element.innerHTML = '';
|
||||||
|
element.appendChild(linkElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex);
|
||||||
|
}
|
||||||
|
return linkElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Linkifier.prototype._findLinkMatch = function (text, regex, matchIndex) {
|
||||||
|
var match = text.match(regex);
|
||||||
|
if (!match || match.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return match[typeof matchIndex !== 'number' ? 0 : matchIndex];
|
||||||
|
};
|
||||||
|
Linkifier.prototype._createAnchorElement = function (uri, handler, isHypertextLinkHandler) {
|
||||||
|
var element = this._document.createElement('a');
|
||||||
|
element.textContent = uri;
|
||||||
|
if (isHypertextLinkHandler) {
|
||||||
|
element.href = uri;
|
||||||
|
element.target = '_blank';
|
||||||
|
element.addEventListener('click', function (event) {
|
||||||
|
if (handler) {
|
||||||
|
return handler(event, uri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
element.addEventListener('click', function (event) {
|
||||||
|
if (element.classList.contains(INVALID_LINK_CLASS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return handler(event, uri);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
Linkifier.prototype._replaceNode = function (oldNode) {
|
||||||
|
var newNodes = [];
|
||||||
|
for (var _i = 1; _i < arguments.length; _i++) {
|
||||||
|
newNodes[_i - 1] = arguments[_i];
|
||||||
|
}
|
||||||
|
var parent = oldNode.parentNode;
|
||||||
|
for (var i = 0; i < newNodes.length; i++) {
|
||||||
|
parent.insertBefore(newNodes[i], oldNode);
|
||||||
|
}
|
||||||
|
parent.removeChild(oldNode);
|
||||||
|
};
|
||||||
|
Linkifier.prototype._replaceNodeSubstringWithNode = function (targetNode, newNode, substring, substringIndex) {
|
||||||
|
var node = targetNode;
|
||||||
|
if (node.nodeType !== 3) {
|
||||||
|
node = node.childNodes[0];
|
||||||
|
}
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
var fullText = node.textContent;
|
||||||
|
if (substringIndex === 0) {
|
||||||
|
var rightText = fullText.substring(substring.length);
|
||||||
|
var rightTextNode = this._document.createTextNode(rightText);
|
||||||
|
this._replaceNode(node, newNode, rightTextNode);
|
||||||
|
}
|
||||||
|
else if (substringIndex === targetNode.textContent.length - substring.length) {
|
||||||
|
var leftText = fullText.substring(0, substringIndex);
|
||||||
|
var leftTextNode = this._document.createTextNode(leftText);
|
||||||
|
this._replaceNode(node, leftTextNode, newNode);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var leftText = fullText.substring(0, substringIndex);
|
||||||
|
var leftTextNode = this._document.createTextNode(leftText);
|
||||||
|
var rightText = fullText.substring(substringIndex + substring.length);
|
||||||
|
var rightTextNode = this._document.createTextNode(rightText);
|
||||||
|
this._replaceNode(node, leftTextNode, newNode, rightTextNode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Linkifier;
|
||||||
|
}());
|
||||||
|
Linkifier.TIME_BEFORE_LINKIFY = 200;
|
||||||
|
exports.Linkifier = Linkifier;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
},{}],7:[function(require,module,exports){
|
||||||
|
"use strict";
|
||||||
var EscapeSequences_1 = require("./EscapeSequences");
|
var EscapeSequences_1 = require("./EscapeSequences");
|
||||||
var Charsets_1 = require("./Charsets");
|
var Charsets_1 = require("./Charsets");
|
||||||
var normalStateHandler = {};
|
var normalStateHandler = {};
|
||||||
@@ -1787,7 +1995,7 @@ exports.Parser = Parser;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
},{"./Charsets":1,"./EscapeSequences":3}],7:[function(require,module,exports){
|
},{"./Charsets":1,"./EscapeSequences":3}],8:[function(require,module,exports){
|
||||||
"use strict";
|
"use strict";
|
||||||
var MAX_REFRESH_FRAME_SKIP = 5;
|
var MAX_REFRESH_FRAME_SKIP = 5;
|
||||||
var FLAGS;
|
var FLAGS;
|
||||||
@@ -1998,7 +2206,7 @@ function checkBoldBroken(document) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
},{}],8:[function(require,module,exports){
|
},{}],9:[function(require,module,exports){
|
||||||
"use strict";
|
"use strict";
|
||||||
var Viewport = (function () {
|
var Viewport = (function () {
|
||||||
function Viewport(terminal, viewportElement, scrollArea, charMeasure) {
|
function Viewport(terminal, viewportElement, scrollArea, charMeasure) {
|
||||||
@@ -2075,7 +2283,7 @@ exports.Viewport = Viewport;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
},{}],9:[function(require,module,exports){
|
},{}],10:[function(require,module,exports){
|
||||||
"use strict";
|
"use strict";
|
||||||
function prepareTextForClipboard(text) {
|
function prepareTextForClipboard(text) {
|
||||||
var space = String.fromCharCode(32), nonBreakingSpace = String.fromCharCode(160), allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'), processedText = text.split('\n').map(function (line) {
|
var space = String.fromCharCode(32), nonBreakingSpace = String.fromCharCode(160), allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'), processedText = text.split('\n').map(function (line) {
|
||||||
@@ -2156,7 +2364,7 @@ exports.rightClickHandler = rightClickHandler;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
},{}],10:[function(require,module,exports){
|
},{}],11:[function(require,module,exports){
|
||||||
"use strict";
|
"use strict";
|
||||||
var Generic_1 = require("./Generic");
|
var Generic_1 = require("./Generic");
|
||||||
var isNode = (typeof navigator === 'undefined') ? true : false;
|
var isNode = (typeof navigator === 'undefined') ? true : false;
|
||||||
@@ -2171,7 +2379,7 @@ exports.isMSWindows = Generic_1.contains(['Windows', 'Win16', 'Win32', 'WinCE'],
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
},{"./Generic":13}],11:[function(require,module,exports){
|
},{"./Generic":14}],12:[function(require,module,exports){
|
||||||
"use strict";
|
"use strict";
|
||||||
var __extends = (this && this.__extends) || function (d, b) {
|
var __extends = (this && this.__extends) || function (d, b) {
|
||||||
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
|
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
|
||||||
@@ -2181,8 +2389,9 @@ var __extends = (this && this.__extends) || function (d, b) {
|
|||||||
var EventEmitter_js_1 = require("../EventEmitter.js");
|
var EventEmitter_js_1 = require("../EventEmitter.js");
|
||||||
var CharMeasure = (function (_super) {
|
var CharMeasure = (function (_super) {
|
||||||
__extends(CharMeasure, _super);
|
__extends(CharMeasure, _super);
|
||||||
function CharMeasure(parentElement) {
|
function CharMeasure(document, parentElement) {
|
||||||
var _this = _super.call(this) || this;
|
var _this = _super.call(this) || this;
|
||||||
|
_this._document = document;
|
||||||
_this._parentElement = parentElement;
|
_this._parentElement = parentElement;
|
||||||
return _this;
|
return _this;
|
||||||
}
|
}
|
||||||
@@ -2203,11 +2412,12 @@ var CharMeasure = (function (_super) {
|
|||||||
CharMeasure.prototype.measure = function () {
|
CharMeasure.prototype.measure = function () {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
if (!this._measureElement) {
|
if (!this._measureElement) {
|
||||||
this._measureElement = document.createElement('span');
|
this._measureElement = this._document.createElement('span');
|
||||||
this._measureElement.style.position = 'absolute';
|
this._measureElement.style.position = 'absolute';
|
||||||
this._measureElement.style.top = '0';
|
this._measureElement.style.top = '0';
|
||||||
this._measureElement.style.left = '-9999em';
|
this._measureElement.style.left = '-9999em';
|
||||||
this._measureElement.textContent = 'W';
|
this._measureElement.textContent = 'W';
|
||||||
|
this._measureElement.setAttribute('aria-hidden', 'true');
|
||||||
this._parentElement.appendChild(this._measureElement);
|
this._parentElement.appendChild(this._measureElement);
|
||||||
setTimeout(function () { return _this._doMeasure(); }, 0);
|
setTimeout(function () { return _this._doMeasure(); }, 0);
|
||||||
}
|
}
|
||||||
@@ -2232,7 +2442,7 @@ exports.CharMeasure = CharMeasure;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
},{"../EventEmitter.js":4}],12:[function(require,module,exports){
|
},{"../EventEmitter.js":4}],13:[function(require,module,exports){
|
||||||
"use strict";
|
"use strict";
|
||||||
var CircularList = (function () {
|
var CircularList = (function () {
|
||||||
function CircularList(maxLength) {
|
function CircularList(maxLength) {
|
||||||
@@ -2370,7 +2580,7 @@ exports.CircularList = CircularList;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
},{}],13:[function(require,module,exports){
|
},{}],14:[function(require,module,exports){
|
||||||
"use strict";
|
"use strict";
|
||||||
function contains(arr, el) {
|
function contains(arr, el) {
|
||||||
return arr.indexOf(el) >= 0;
|
return arr.indexOf(el) >= 0;
|
||||||
@@ -2380,7 +2590,7 @@ exports.contains = contains;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
},{}],14:[function(require,module,exports){
|
},{}],15:[function(require,module,exports){
|
||||||
"use strict";
|
"use strict";
|
||||||
var CompositionHelper_1 = require("./CompositionHelper");
|
var CompositionHelper_1 = require("./CompositionHelper");
|
||||||
var EventEmitter_1 = require("./EventEmitter");
|
var EventEmitter_1 = require("./EventEmitter");
|
||||||
@@ -2391,6 +2601,7 @@ var EscapeSequences_1 = require("./EscapeSequences");
|
|||||||
var InputHandler_1 = require("./InputHandler");
|
var InputHandler_1 = require("./InputHandler");
|
||||||
var Parser_1 = require("./Parser");
|
var Parser_1 = require("./Parser");
|
||||||
var Renderer_1 = require("./Renderer");
|
var Renderer_1 = require("./Renderer");
|
||||||
|
var Linkifier_1 = require("./Linkifier");
|
||||||
var CharMeasure_1 = require("./utils/CharMeasure");
|
var CharMeasure_1 = require("./utils/CharMeasure");
|
||||||
var Browser = require("./utils/Browser");
|
var Browser = require("./utils/Browser");
|
||||||
var document = (typeof window != 'undefined') ? window.document : null;
|
var document = (typeof window != 'undefined') ? window.document : null;
|
||||||
@@ -2491,6 +2702,8 @@ function Terminal(options) {
|
|||||||
this.inputHandler = new InputHandler_1.InputHandler(this);
|
this.inputHandler = new InputHandler_1.InputHandler(this);
|
||||||
this.parser = new Parser_1.Parser(this.inputHandler, this);
|
this.parser = new Parser_1.Parser(this.inputHandler, this);
|
||||||
this.renderer = this.renderer || null;
|
this.renderer = this.renderer || null;
|
||||||
|
this.linkifier = this.linkifier || null;
|
||||||
|
;
|
||||||
this.writeBuffer = [];
|
this.writeBuffer = [];
|
||||||
this.writeInProgress = false;
|
this.writeInProgress = false;
|
||||||
this.xoffSentToCatchUp = false;
|
this.xoffSentToCatchUp = false;
|
||||||
@@ -2715,6 +2928,9 @@ Terminal.bindKeys = function (term) {
|
|||||||
on(term.textarea, 'compositionupdate', term.compositionHelper.compositionupdate.bind(term.compositionHelper));
|
on(term.textarea, 'compositionupdate', term.compositionHelper.compositionupdate.bind(term.compositionHelper));
|
||||||
on(term.textarea, 'compositionend', term.compositionHelper.compositionend.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', term.compositionHelper.updateCompositionElements.bind(term.compositionHelper));
|
||||||
|
term.on('refresh', function (data) {
|
||||||
|
term.queueLinkification(data.start, data.end);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
Terminal.prototype.insertRow = function (row) {
|
Terminal.prototype.insertRow = function (row) {
|
||||||
if (typeof row != 'object') {
|
if (typeof row != 'object') {
|
||||||
@@ -2750,6 +2966,7 @@ Terminal.prototype.open = function (parent) {
|
|||||||
this.rowContainer.classList.add('xterm-rows');
|
this.rowContainer.classList.add('xterm-rows');
|
||||||
this.element.appendChild(this.rowContainer);
|
this.element.appendChild(this.rowContainer);
|
||||||
this.children = [];
|
this.children = [];
|
||||||
|
this.linkifier = new Linkifier_1.Linkifier(document, this.children);
|
||||||
this.helperContainer = document.createElement('div');
|
this.helperContainer = document.createElement('div');
|
||||||
this.helperContainer.classList.add('xterm-helpers');
|
this.helperContainer.classList.add('xterm-helpers');
|
||||||
this.element.appendChild(this.helperContainer);
|
this.element.appendChild(this.helperContainer);
|
||||||
@@ -2776,7 +2993,7 @@ Terminal.prototype.open = function (parent) {
|
|||||||
this.insertRow();
|
this.insertRow();
|
||||||
}
|
}
|
||||||
this.parent.appendChild(this.element);
|
this.parent.appendChild(this.element);
|
||||||
this.charMeasure = new CharMeasure_1.CharMeasure(this.helperContainer);
|
this.charMeasure = new CharMeasure_1.CharMeasure(document, this.helperContainer);
|
||||||
this.charMeasure.on('charsizechanged', function () {
|
this.charMeasure.on('charsizechanged', function () {
|
||||||
self.updateCharSizeCSS();
|
self.updateCharSizeCSS();
|
||||||
});
|
});
|
||||||
@@ -2985,10 +3202,8 @@ Terminal.prototype.bindMouse = function () {
|
|||||||
? el.offsetParent
|
? el.offsetParent
|
||||||
: el.parentNode;
|
: el.parentNode;
|
||||||
}
|
}
|
||||||
w = self.element.clientWidth;
|
x = Math.ceil(x / self.charMeasure.width);
|
||||||
h = self.element.clientHeight;
|
y = Math.ceil(y / self.charMeasure.height);
|
||||||
x = Math.ceil((x / w) * self.cols);
|
|
||||||
y = Math.ceil((y / h) * self.rows);
|
|
||||||
if (x < 0)
|
if (x < 0)
|
||||||
x = 0;
|
x = 0;
|
||||||
if (x > self.cols)
|
if (x > self.cols)
|
||||||
@@ -3060,6 +3275,13 @@ Terminal.prototype.refresh = function (start, end) {
|
|||||||
this.renderer.queueRefresh(start, end);
|
this.renderer.queueRefresh(start, end);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Terminal.prototype.queueLinkification = function (start, end) {
|
||||||
|
if (this.linkifier) {
|
||||||
|
for (var i = start; i <= end; i++) {
|
||||||
|
this.linkifier.linkifyRow(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Terminal.prototype.showCursor = function () {
|
Terminal.prototype.showCursor = function () {
|
||||||
if (!this.cursorState) {
|
if (!this.cursorState) {
|
||||||
this.cursorState = 1;
|
this.cursorState = 1;
|
||||||
@@ -3173,6 +3395,27 @@ Terminal.prototype.writeln = function (data) {
|
|||||||
Terminal.prototype.attachCustomKeydownHandler = function (customKeydownHandler) {
|
Terminal.prototype.attachCustomKeydownHandler = function (customKeydownHandler) {
|
||||||
this.customKeydownHandler = customKeydownHandler;
|
this.customKeydownHandler = customKeydownHandler;
|
||||||
};
|
};
|
||||||
|
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);
|
||||||
|
this.refresh(0, this.rows - 1);
|
||||||
|
};
|
||||||
|
Terminal.prototype.registerLinkMatcher = function (regex, handler, options) {
|
||||||
|
if (this.linkifier) {
|
||||||
|
var matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
|
||||||
|
this.refresh(0, this.rows - 1);
|
||||||
|
return matcherId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Terminal.prototype.deregisterLinkMatcher = function (matcherId) {
|
||||||
|
if (this.linkifier) {
|
||||||
|
if (this.linkifier.deregisterLinkMatcher(matcherId)) {
|
||||||
|
this.refresh(0, this.rows - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Terminal.prototype.keyDown = function (ev) {
|
Terminal.prototype.keyDown = function (ev) {
|
||||||
if (this.customKeydownHandler && this.customKeydownHandler(ev) === false) {
|
if (this.customKeydownHandler && this.customKeydownHandler(ev) === false) {
|
||||||
return false;
|
return false;
|
||||||
@@ -3892,6 +4135,6 @@ module.exports = Terminal;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
},{"./CompositionHelper":2,"./EscapeSequences":3,"./EventEmitter":4,"./InputHandler":5,"./Parser":6,"./Renderer":7,"./Viewport":8,"./handlers/Clipboard":9,"./utils/Browser":10,"./utils/CharMeasure":11,"./utils/CircularList":12}]},{},[14])(14)
|
},{"./CompositionHelper":2,"./EscapeSequences":3,"./EventEmitter":4,"./InputHandler":5,"./Linkifier":6,"./Parser":7,"./Renderer":8,"./Viewport":9,"./handlers/Clipboard":10,"./utils/Browser":11,"./utils/CharMeasure":12,"./utils/CircularList":13}]},{},[15])(15)
|
||||||
});
|
});
|
||||||
//# sourceMappingURL=xterm.js.map
|
//# sourceMappingURL=xterm.js.map
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+25
-1
@@ -1,8 +1,11 @@
|
|||||||
const browserify = require('browserify');
|
const browserify = require('browserify');
|
||||||
const buffer = require('vinyl-buffer');
|
const buffer = require('vinyl-buffer');
|
||||||
|
const coveralls = require('gulp-coveralls');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const gulp = require('gulp');
|
const gulp = require('gulp');
|
||||||
|
const istanbul = require('gulp-istanbul');
|
||||||
const merge = require('merge-stream');
|
const merge = require('merge-stream');
|
||||||
|
const mocha = require('gulp-mocha');
|
||||||
const sorcery = require('sorcery');
|
const sorcery = require('sorcery');
|
||||||
const source = require('vinyl-source-stream');
|
const source = require('vinyl-source-stream');
|
||||||
const sourcemaps = require('gulp-sourcemaps');
|
const sourcemaps = require('gulp-sourcemaps');
|
||||||
@@ -68,6 +71,19 @@ gulp.task('browserify', ['tsc'], function() {
|
|||||||
return merge(bundleStream, copyAddons, copyStylesheets);
|
return merge(bundleStream, copyAddons, copyStylesheets);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task('instrument-test', function () {
|
||||||
|
return gulp.src(['lib/**/*.js'])
|
||||||
|
// Covering files
|
||||||
|
.pipe(istanbul())
|
||||||
|
// Force `require` to return covered files
|
||||||
|
.pipe(istanbul.hookRequire());
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('mocha', ['instrument-test'], function () {
|
||||||
|
return gulp.src(['lib/*test.js', 'lib/**/*test.js'], {read: false})
|
||||||
|
.pipe(mocha())
|
||||||
|
.pipe(istanbul.writeReports());
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use `sorcery` to resolve the source map chain and point back to the TypeScript files.
|
* Use `sorcery` to resolve the source map chain and point back to the TypeScript files.
|
||||||
@@ -80,6 +96,14 @@ gulp.task('sorcery', ['browserify'], function () {
|
|||||||
chain.writeSync();
|
chain.writeSync();
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('build', ['sorcery']);
|
/**
|
||||||
|
* Submit coverage results to coveralls.io
|
||||||
|
*/
|
||||||
|
gulp.task('coveralls', function () {
|
||||||
|
gulp.src('coverage/**/lcov.info')
|
||||||
|
.pipe(coveralls());
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('build', ['sorcery']);
|
||||||
|
gulp.task('test', ['mocha']);
|
||||||
gulp.task('default', ['build']);
|
gulp.task('default', ['build']);
|
||||||
|
|||||||
+10
-5
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm",
|
"name": "xterm",
|
||||||
"description": "Full xterm terminal, in your browser",
|
"description": "Full xterm terminal, in your browser",
|
||||||
"version": "2.3.2",
|
"version": "2.4.0",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"demo",
|
"demo",
|
||||||
"test",
|
"test",
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^3.4.34",
|
"@types/chai": "^3.4.34",
|
||||||
|
"@types/jsdom": "^2.0.30",
|
||||||
"@types/mocha": "^2.2.33",
|
"@types/mocha": "^2.2.33",
|
||||||
"@types/node": "^6.0.41",
|
"@types/node": "^6.0.41",
|
||||||
"browserify": "^13.1.0",
|
"browserify": "^13.1.0",
|
||||||
@@ -47,17 +48,20 @@
|
|||||||
"glob": "^7.0.5",
|
"glob": "^7.0.5",
|
||||||
"gulp": "^3.9.1",
|
"gulp": "^3.9.1",
|
||||||
"gulp-cli": "^1.2.2",
|
"gulp-cli": "^1.2.2",
|
||||||
|
"gulp-coveralls": "^0.1.4",
|
||||||
|
"gulp-istanbul": "^1.1.1",
|
||||||
|
"gulp-mocha": "^3.0.1",
|
||||||
"gulp-sourcemaps": "1.9.1",
|
"gulp-sourcemaps": "1.9.1",
|
||||||
"gulp-typescript": "^3.1.3",
|
"gulp-typescript": "^3.1.3",
|
||||||
"jsdoc": "3.4.3",
|
"jsdoc": "3.4.3",
|
||||||
|
"jsdom": "^9.11.0",
|
||||||
"merge-stream": "^1.0.1",
|
"merge-stream": "^1.0.1",
|
||||||
"mocha": "2.5.3",
|
|
||||||
"node-pty": "^0.4.1",
|
"node-pty": "^0.4.1",
|
||||||
"nodemon": "1.10.2",
|
"nodemon": "1.10.2",
|
||||||
"sleep": "^3.0.1",
|
"sleep": "^3.0.1",
|
||||||
"sorcery": "^0.10.0",
|
"sorcery": "^0.10.0",
|
||||||
"tslint": "^4.0.2",
|
"tslint": "^4.0.2",
|
||||||
"typescript": "^2.0.3",
|
"typescript": "~2.2.0",
|
||||||
"vinyl-buffer": "^1.0.0",
|
"vinyl-buffer": "^1.0.0",
|
||||||
"vinyl-source-stream": "^1.1.0"
|
"vinyl-source-stream": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -66,9 +70,10 @@
|
|||||||
"start": "node demo/app",
|
"start": "node demo/app",
|
||||||
"dev": "nodemon -e js,ts --watch src --watch demo --exec npm start",
|
"dev": "nodemon -e js,ts --watch src --watch demo --exec npm start",
|
||||||
"lint": "tslint src/*.ts src/**/*.ts",
|
"lint": "tslint src/*.ts src/**/*.ts",
|
||||||
"test": "mocha --recursive ./lib",
|
"test": "gulp test",
|
||||||
"build:docs": "jsdoc -c jsdoc.json",
|
"build:docs": "jsdoc -c jsdoc.json",
|
||||||
"build": "gulp build",
|
"build": "gulp build",
|
||||||
"prepublish": "npm run build"
|
"prepublish": "npm run build",
|
||||||
|
"coveralls": "gulp coveralls"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* C0 control codes
|
* C0 control codes
|
||||||
* See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes
|
* See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes
|
||||||
|
|||||||
+1
-1
@@ -1394,7 +1394,7 @@ export class InputHandler implements IInputHandler {
|
|||||||
this._terminal.cursorHidden = false;
|
this._terminal.cursorHidden = false;
|
||||||
this._terminal.insertMode = false;
|
this._terminal.insertMode = false;
|
||||||
this._terminal.originMode = false;
|
this._terminal.originMode = false;
|
||||||
this._terminal.wraparoundMode = false; // autowrap
|
this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false
|
||||||
this._terminal.applicationKeypad = false; // ?
|
this._terminal.applicationKeypad = false; // ?
|
||||||
this._terminal.viewport.syncScrollArea();
|
this._terminal.viewport.syncScrollArea();
|
||||||
this._terminal.applicationCursor = false;
|
this._terminal.applicationCursor = false;
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { LinkMatcherOptions } from './Interfaces';
|
||||||
|
import { LinkMatcherHandler, LinkMatcherValidationCallback } from './Types';
|
||||||
|
|
||||||
export interface IBrowser {
|
export interface IBrowser {
|
||||||
isNode: boolean;
|
isNode: boolean;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
@@ -44,6 +47,19 @@ export interface ITerminal {
|
|||||||
emit(event: string, data: any);
|
emit(event: string, data: any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICharMeasure {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
measure(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILinkifier {
|
||||||
|
linkifyRow(rowIndex: number): void;
|
||||||
|
attachHypertextLinkHandler(handler: LinkMatcherHandler): void;
|
||||||
|
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: LinkMatcherOptions): number;
|
||||||
|
deregisterLinkMatcher(matcherId: number): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface ICircularList<T> {
|
interface ICircularList<T> {
|
||||||
length: number;
|
length: number;
|
||||||
maxLength: number;
|
maxLength: number;
|
||||||
@@ -58,6 +74,25 @@ interface ICircularList<T> {
|
|||||||
shiftElements(start: number, count: number, offset: number): void;
|
shiftElements(start: number, count: number, offset: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LinkMatcherOptions {
|
||||||
|
/**
|
||||||
|
* The index of the link from the regex.match(text) call. This defaults to 0
|
||||||
|
* (for regular expressions without capture groups).
|
||||||
|
*/
|
||||||
|
matchIndex?: number;
|
||||||
|
/**
|
||||||
|
* A callback that validates an individual link, returning true if valid and
|
||||||
|
* false if invalid.
|
||||||
|
*/
|
||||||
|
validationCallback?: LinkMatcherValidationCallback;
|
||||||
|
/**
|
||||||
|
* The priority of the link matcher, this defines the order in which the link
|
||||||
|
* matcher is evaluated relative to others, from highest to lowest. The
|
||||||
|
* default value is 0.
|
||||||
|
*/
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles actions generated by the parser.
|
* Handles actions generated by the parser.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
import jsdom = require('jsdom');
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { ITerminal, ILinkifier } from './Interfaces';
|
||||||
|
import { Linkifier } from './Linkifier';
|
||||||
|
import { LinkMatcher } from './Types';
|
||||||
|
|
||||||
|
class TestLinkifier extends Linkifier {
|
||||||
|
constructor(document: Document, rows: HTMLElement[]) {
|
||||||
|
Linkifier.TIME_BEFORE_LINKIFY = 0;
|
||||||
|
super(document, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Linkifier', () => {
|
||||||
|
let window: Window;
|
||||||
|
let document: Document;
|
||||||
|
|
||||||
|
let container: HTMLElement;
|
||||||
|
let rows: HTMLElement[];
|
||||||
|
let linkifier: TestLinkifier;
|
||||||
|
|
||||||
|
beforeEach(done => {
|
||||||
|
rows = [];
|
||||||
|
jsdom.env('', (err, w) => {
|
||||||
|
window = w;
|
||||||
|
document = window.document;
|
||||||
|
linkifier = new TestLinkifier(document, rows);
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function addRow(text: string) {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.textContent = text;
|
||||||
|
container.appendChild(element);
|
||||||
|
rows.push(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickElement(element: Node) {
|
||||||
|
const event = document.createEvent('MouseEvent');
|
||||||
|
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||||
|
element.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('validationCallback', () => {
|
||||||
|
it('should enable link if true', done => {
|
||||||
|
addRow('test');
|
||||||
|
linkifier.registerLinkMatcher(/test/, () => done(), {
|
||||||
|
validationCallback: (url, cb) => {
|
||||||
|
cb(true);
|
||||||
|
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||||
|
setTimeout(() => clickElement(rows[0].firstChild), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
linkifier.linkifyRow(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable link if false', done => {
|
||||||
|
addRow('test');
|
||||||
|
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
||||||
|
validationCallback: (url, cb) => {
|
||||||
|
cb(false);
|
||||||
|
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||||
|
setTimeout(() => clickElement(rows[0].firstChild), 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
linkifier.linkifyRow(0);
|
||||||
|
// Allow time for the click to be performed
|
||||||
|
setTimeout(() => done(), 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('priority', () => {
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LinkMatcherOptions } from './Interfaces';
|
||||||
|
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback } from './Types';
|
||||||
|
|
||||||
|
const INVALID_LINK_CLASS = 'xterm-invalid-link';
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
export class Linkifier {
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
protected static TIME_BEFORE_LINKIFY = 200;
|
||||||
|
|
||||||
|
protected _linkMatchers: LinkMatcher[];
|
||||||
|
|
||||||
|
private _document: Document;
|
||||||
|
private _rows: HTMLElement[];
|
||||||
|
private _rowTimeoutIds: number[];
|
||||||
|
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
|
||||||
|
|
||||||
|
constructor(document: Document, rows: HTMLElement[]) {
|
||||||
|
this._document = document;
|
||||||
|
this._rows = rows;
|
||||||
|
this._rowTimeoutIds = [];
|
||||||
|
this._linkMatchers = [];
|
||||||
|
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 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), Linkifier.TIME_BEFORE_LINKIFY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches a handler for hypertext links, overriding default <a> behavior
|
||||||
|
* for standard http(s) links.
|
||||||
|
* @param {LinkHandler} handler The handler to use, this can be cleared with
|
||||||
|
* null.
|
||||||
|
*/
|
||||||
|
public attachHypertextLinkHandler(handler: LinkMatcherHandler): 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 {LinkMatcherOptions} [options] Options for the link matcher.
|
||||||
|
* @return {number} The ID of the new matcher, this can be used to deregister.
|
||||||
|
*/
|
||||||
|
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: LinkMatcherOptions = {}): number {
|
||||||
|
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
|
||||||
|
throw new Error('handler must be defined');
|
||||||
|
}
|
||||||
|
const matcher: LinkMatcher = {
|
||||||
|
id: this._nextLinkMatcherId++,
|
||||||
|
regex,
|
||||||
|
handler,
|
||||||
|
matchIndex: options.matchIndex,
|
||||||
|
validationCallback: options.validationCallback,
|
||||||
|
priority: options.priority || 0
|
||||||
|
};
|
||||||
|
this._addLinkMatcherToList(matcher);
|
||||||
|
return matcher.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a link matcher to the list in the correct position based on the
|
||||||
|
* priority of each link matcher. New link matchers of equal priority are
|
||||||
|
* considered after older link matchers.
|
||||||
|
* @param matcher The link matcher to be added.
|
||||||
|
*/
|
||||||
|
private _addLinkMatcherToList(matcher: LinkMatcher): void {
|
||||||
|
if (this._linkMatchers.length === 0) {
|
||||||
|
this._linkMatchers.push(matcher);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = this._linkMatchers.length - 1; i >= 0; i--) {
|
||||||
|
if (matcher.priority <= this._linkMatchers[i].priority) {
|
||||||
|
this._linkMatchers.splice(i + 1, 0, matcher);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._linkMatchers.splice(0, 0, matcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
const linkElement = this._doLinkifyRow(rowIndex, uri, matcher.handler, matcher.id === HYPERTEXT_LINK_MATCHER_ID);
|
||||||
|
// Fire validation callback
|
||||||
|
if (linkElement && matcher.validationCallback) {
|
||||||
|
matcher.validationCallback(uri, isValid => {
|
||||||
|
if (!isValid) {
|
||||||
|
linkElement.classList.add(INVALID_LINK_CLASS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
|
* @return The link element if it was added, otherwise undefined.
|
||||||
|
*/
|
||||||
|
private _doLinkifyRow(rowIndex: number, uri: string, handler: LinkMatcherHandler, isHttpLinkMatcher: boolean): HTMLElement {
|
||||||
|
// 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, isHttpLinkMatcher);
|
||||||
|
if (node.textContent.length === uri.length) {
|
||||||
|
// Matches entire string
|
||||||
|
|
||||||
|
if (node.nodeType === 3 /*Node.TEXT_NODE*/) {
|
||||||
|
this._replaceNode(node, linkElement);
|
||||||
|
} else {
|
||||||
|
const element = (<HTMLElement>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);
|
||||||
|
}
|
||||||
|
return linkElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: LinkMatcherHandler, isHypertextLinkHandler: boolean): HTMLAnchorElement {
|
||||||
|
const element = this._document.createElement('a');
|
||||||
|
element.textContent = uri;
|
||||||
|
if (isHypertextLinkHandler) {
|
||||||
|
element.href = uri;
|
||||||
|
// Force link on another tab so work is not lost
|
||||||
|
element.target = '_blank';
|
||||||
|
element.addEventListener('click', (event: MouseEvent) => {
|
||||||
|
if (handler) {
|
||||||
|
return handler(event, uri);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
element.addEventListener('click', (event: MouseEvent) => {
|
||||||
|
// Don't execute the handler if the link is flagged as invalid
|
||||||
|
if (element.classList.contains(INVALID_LINK_CLASS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return handler(event, 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; either a text node or a <span>
|
||||||
|
* 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 !== 3/*Node.TEXT_NODE*/) {
|
||||||
|
node = node.childNodes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The targetNode will be either a text node or a <span>. 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 <newNode><textnode>
|
||||||
|
const rightText = fullText.substring(substring.length);
|
||||||
|
const rightTextNode = this._document.createTextNode(rightText);
|
||||||
|
this._replaceNode(node, newNode, rightTextNode);
|
||||||
|
} else if (substringIndex === targetNode.textContent.length - substring.length) {
|
||||||
|
// Replace with <textnode><newNode>
|
||||||
|
const leftText = fullText.substring(0, substringIndex);
|
||||||
|
const leftTextNode = this._document.createTextNode(leftText);
|
||||||
|
this._replaceNode(node, leftTextNode, newNode);
|
||||||
|
} else {
|
||||||
|
// Replace with <textnode><newNode><textnode>
|
||||||
|
const leftText = fullText.substring(0, substringIndex);
|
||||||
|
const leftTextNode = this._document.createTextNode(leftText);
|
||||||
|
const rightText = fullText.substring(substringIndex + substring.length);
|
||||||
|
const rightTextNode = this._document.createTextNode(rightText);
|
||||||
|
this._replaceNode(node, leftTextNode, newNode, rightTextNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type LinkMatcher = {
|
||||||
|
id: number,
|
||||||
|
regex: RegExp,
|
||||||
|
handler: LinkMatcherHandler,
|
||||||
|
matchIndex?: number,
|
||||||
|
validationCallback?: LinkMatcherValidationCallback,
|
||||||
|
priority?: number
|
||||||
|
};
|
||||||
|
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
|
||||||
|
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||||
@@ -32,6 +32,9 @@
|
|||||||
var exports = {};
|
var exports = {};
|
||||||
|
|
||||||
exports.proposeGeometry = function (term) {
|
exports.proposeGeometry = function (term) {
|
||||||
|
if (!term.element.parentElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
||||||
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
||||||
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
||||||
@@ -66,7 +69,9 @@
|
|||||||
exports.fit = function (term) {
|
exports.fit = function (term) {
|
||||||
var geometry = exports.proposeGeometry(term);
|
var geometry = exports.proposeGeometry(term);
|
||||||
|
|
||||||
term.resize(geometry.cols, geometry.rows);
|
if (geometry) {
|
||||||
|
term.resize(geometry.cols, geometry.rows);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Xterm.prototype.proposeGeometry = function () {
|
Xterm.prototype.proposeGeometry = function () {
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="../../src/xterm.css" />
|
|
||||||
<link rel="stylesheet" href="../../demo/style.css" />
|
|
||||||
<script src="../../src/xterm.js"></script>
|
|
||||||
<script src="linkify.js"></script>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
color: #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
#terminal-container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#terminal-container a {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="terminal-container"></div>
|
|
||||||
<script>
|
|
||||||
var term = new Terminal(),
|
|
||||||
container = document.getElementById('terminal-container');
|
|
||||||
|
|
||||||
term.open(container);
|
|
||||||
term.writeln('Hello, welcome to xterm.js');
|
|
||||||
term.writeln('');
|
|
||||||
term.writeln('Check it out at github.com/sourcelair/xterm.js');
|
|
||||||
term.linkify();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -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<nodes.length; j++) {
|
|
||||||
var node = nodes[j],
|
|
||||||
match;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Since we cannot access the TextNode's HTML representation
|
|
||||||
* from the instance itself, we assign its data as textContent
|
|
||||||
* to a dummy buffer span, in order to retrieve the TextNode's
|
|
||||||
* HTML representation from the buffer's innerHTML.
|
|
||||||
*/
|
|
||||||
buffer.textContent = node.data;
|
|
||||||
|
|
||||||
var nodeHTML = buffer.innerHTML;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply function only on TextNodes
|
|
||||||
*/
|
|
||||||
if (node.nodeType != node.TEXT_NODE) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = exports.findLinkMatch(node.data, lenient);
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var startsWithProtocol = new RegExp('^' + protocolClause),
|
|
||||||
urlHasProtocol = url.match(startsWithProtocol),
|
|
||||||
href = (urlHasProtocol) ? url : 'http://' + url,
|
|
||||||
link = '<a href="' + href + '" ' + target + '>' + url + '</a>',
|
|
||||||
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<rows.length; i++) {
|
|
||||||
var line = rows[i];
|
|
||||||
|
|
||||||
exports.linkifyTerminalLine(terminal, line, lenient, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This event gets emitted when conversion of all URL substrings to
|
|
||||||
* HTML anchor elements (links) has finished for the current Xterm
|
|
||||||
* instance's view.
|
|
||||||
*
|
|
||||||
* @event linkify
|
|
||||||
*/
|
|
||||||
terminal.emit('linkify');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extend Xterm prototype.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts all valid URLs found in the current terminal linte into
|
|
||||||
* hyperlinks.
|
|
||||||
*
|
|
||||||
* @memberof Xterm
|
|
||||||
* @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
|
|
||||||
*/
|
|
||||||
Xterm.prototype.linkifyTerminalLine = function (line, lenient, target) {
|
|
||||||
return exports.linkifyTerminalLine(this, line, lenient, target);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts all valid URLs found in the current terminal into hyperlinks.
|
|
||||||
*
|
|
||||||
* @memberof Xterm
|
|
||||||
* @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
|
|
||||||
*/
|
|
||||||
Xterm.prototype.linkify = function (lenient, target) {
|
|
||||||
return exports.linkify(this, lenient, target);
|
|
||||||
};
|
|
||||||
|
|
||||||
return exports;
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "xterm.linkify",
|
|
||||||
"main": "linkify.js",
|
|
||||||
"private": true
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -550,6 +550,7 @@ describe('xterm.js', function() {
|
|||||||
|
|
||||||
describe('unicode - surrogates', function() {
|
describe('unicode - surrogates', function() {
|
||||||
it('2 characters per cell', function () {
|
it('2 characters per cell', function () {
|
||||||
|
this.timeout(10000); // This is needed because istanbul patches code and slows it down
|
||||||
var high = String.fromCharCode(0xD800);
|
var high = String.fromCharCode(0xD800);
|
||||||
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
for (var i=0xDC00; i<=0xDCFF; ++i) {
|
||||||
xterm.write(high + String.fromCharCode(i));
|
xterm.write(high + String.fromCharCode(i));
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
import jsdom = require('jsdom');
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { ICharMeasure, ITerminal } from '../Interfaces';
|
||||||
|
import { CharMeasure } from './CharMeasure';
|
||||||
|
|
||||||
|
describe('CharMeasure', () => {
|
||||||
|
let window: Window;
|
||||||
|
let document: Document;
|
||||||
|
|
||||||
|
let container: HTMLElement;
|
||||||
|
let charMeasure: ICharMeasure;
|
||||||
|
|
||||||
|
beforeEach(done => {
|
||||||
|
jsdom.env('', (err, w) => {
|
||||||
|
window = w;
|
||||||
|
document = window.document;
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
charMeasure = new CharMeasure(document, container);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('measure', () => {
|
||||||
|
it('should set _measureElement on first call', () => {
|
||||||
|
charMeasure.measure();
|
||||||
|
assert.isDefined((<any>charMeasure)._measureElement, 'CharMeasure.measure should have created _measureElement');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be performed async on first call', done => {
|
||||||
|
assert.equal(charMeasure.width, null);
|
||||||
|
charMeasure.measure();
|
||||||
|
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
|
||||||
|
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
|
||||||
|
return { width: 1, height: 1 };
|
||||||
|
};
|
||||||
|
assert.equal(charMeasure.width, null);
|
||||||
|
setTimeout(() => {
|
||||||
|
assert.equal(charMeasure.width, 1);
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be performed sync on successive calls', done => {
|
||||||
|
charMeasure.measure();
|
||||||
|
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
|
||||||
|
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
|
||||||
|
return { width: 1, height: 1 };
|
||||||
|
};
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstWidth = charMeasure.width;
|
||||||
|
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
|
||||||
|
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
|
||||||
|
return { width: 2, height: 2 };
|
||||||
|
};
|
||||||
|
charMeasure.measure();
|
||||||
|
assert.equal(charMeasure.width, firstWidth * 2);
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT do a measure when the parent is hidden', done => {
|
||||||
|
charMeasure.measure();
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstWidth = charMeasure.width;
|
||||||
|
container.style.display = 'none';
|
||||||
|
container.style.fontSize = '2em';
|
||||||
|
charMeasure.measure();
|
||||||
|
assert.equal(charMeasure.width, firstWidth);
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,13 +9,15 @@ import { EventEmitter } from '../EventEmitter.js';
|
|||||||
* Utility class that measures the size of a character.
|
* Utility class that measures the size of a character.
|
||||||
*/
|
*/
|
||||||
export class CharMeasure extends EventEmitter {
|
export class CharMeasure extends EventEmitter {
|
||||||
|
private _document: Document;
|
||||||
private _parentElement: HTMLElement;
|
private _parentElement: HTMLElement;
|
||||||
private _measureElement: HTMLElement;
|
private _measureElement: HTMLElement;
|
||||||
private _width: number;
|
private _width: number;
|
||||||
private _height: number;
|
private _height: number;
|
||||||
|
|
||||||
constructor(parentElement: HTMLElement) {
|
constructor(document: Document, parentElement: HTMLElement) {
|
||||||
super();
|
super();
|
||||||
|
this._document = document;
|
||||||
this._parentElement = parentElement;
|
this._parentElement = parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,11 +31,12 @@ export class CharMeasure extends EventEmitter {
|
|||||||
|
|
||||||
public measure(): void {
|
public measure(): void {
|
||||||
if (!this._measureElement) {
|
if (!this._measureElement) {
|
||||||
this._measureElement = document.createElement('span');
|
this._measureElement = this._document.createElement('span');
|
||||||
this._measureElement.style.position = 'absolute';
|
this._measureElement.style.position = 'absolute';
|
||||||
this._measureElement.style.top = '0';
|
this._measureElement.style.top = '0';
|
||||||
this._measureElement.style.left = '-9999em';
|
this._measureElement.style.left = '-9999em';
|
||||||
this._measureElement.textContent = 'W';
|
this._measureElement.textContent = 'W';
|
||||||
|
this._measureElement.setAttribute('aria-hidden', 'true');
|
||||||
this._parentElement.appendChild(this._measureElement);
|
this._parentElement.appendChild(this._measureElement);
|
||||||
// Perform _doMeasure async if the element was just attached as sometimes
|
// Perform _doMeasure async if the element was just attached as sometimes
|
||||||
// getBoundingClientRect does not return accurate values without this.
|
// getBoundingClientRect does not return accurate values without this.
|
||||||
|
|||||||
+17
-5
@@ -71,6 +71,21 @@
|
|||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal a:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal a.xterm-invalid-link:hover {
|
||||||
|
cursor: text;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
|
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -87,13 +102,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes xterm-cursor-blink {
|
@keyframes xterm-cursor-blink {
|
||||||
0% {
|
0% { }
|
||||||
background-color: #fff;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
50% {
|
50% {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: #FFF;
|
color: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+69
-7
@@ -19,6 +19,7 @@ import { C0 } from './EscapeSequences';
|
|||||||
import { InputHandler } from './InputHandler';
|
import { InputHandler } from './InputHandler';
|
||||||
import { Parser } from './Parser';
|
import { Parser } from './Parser';
|
||||||
import { Renderer } from './Renderer';
|
import { Renderer } from './Renderer';
|
||||||
|
import { Linkifier } from './Linkifier';
|
||||||
import { CharMeasure } from './utils/CharMeasure';
|
import { CharMeasure } from './utils/CharMeasure';
|
||||||
import * as Browser from './utils/Browser';
|
import * as Browser from './utils/Browser';
|
||||||
import * as Keyboard from './utils/Keyboard';
|
import * as Keyboard from './utils/Keyboard';
|
||||||
@@ -210,6 +211,7 @@ function Terminal(options) {
|
|||||||
this.parser = new Parser(this.inputHandler, this);
|
this.parser = new Parser(this.inputHandler, this);
|
||||||
// Reuse renderer if the Terminal is being recreated via a Terminal.reset call.
|
// Reuse renderer if the Terminal is being recreated via a Terminal.reset call.
|
||||||
this.renderer = this.renderer || null;
|
this.renderer = this.renderer || null;
|
||||||
|
this.linkifier = this.linkifier || null;;
|
||||||
|
|
||||||
// user input states
|
// user input states
|
||||||
this.writeBuffer = [];
|
this.writeBuffer = [];
|
||||||
@@ -546,6 +548,9 @@ Terminal.bindKeys = function(term) {
|
|||||||
on(term.textarea, 'compositionupdate', term.compositionHelper.compositionupdate.bind(term.compositionHelper));
|
on(term.textarea, 'compositionupdate', term.compositionHelper.compositionupdate.bind(term.compositionHelper));
|
||||||
on(term.textarea, 'compositionend', term.compositionHelper.compositionend.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', term.compositionHelper.updateCompositionElements.bind(term.compositionHelper));
|
||||||
|
term.on('refresh', function (data) {
|
||||||
|
term.queueLinkification(data.start, data.end)
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -607,6 +612,7 @@ Terminal.prototype.open = function(parent) {
|
|||||||
this.rowContainer.classList.add('xterm-rows');
|
this.rowContainer.classList.add('xterm-rows');
|
||||||
this.element.appendChild(this.rowContainer);
|
this.element.appendChild(this.rowContainer);
|
||||||
this.children = [];
|
this.children = [];
|
||||||
|
this.linkifier = new Linkifier(document, this.children);
|
||||||
|
|
||||||
// Create the container that will hold helpers like the textarea for
|
// Create the container that will hold helpers like the textarea for
|
||||||
// capturing DOM Events. Then produce the helpers.
|
// capturing DOM Events. Then produce the helpers.
|
||||||
@@ -641,7 +647,7 @@ Terminal.prototype.open = function(parent) {
|
|||||||
}
|
}
|
||||||
this.parent.appendChild(this.element);
|
this.parent.appendChild(this.element);
|
||||||
|
|
||||||
this.charMeasure = new CharMeasure(this.helperContainer);
|
this.charMeasure = new CharMeasure(document, this.helperContainer);
|
||||||
this.charMeasure.on('charsizechanged', function () {
|
this.charMeasure.on('charsizechanged', function () {
|
||||||
self.updateCharSizeCSS();
|
self.updateCharSizeCSS();
|
||||||
});
|
});
|
||||||
@@ -957,10 +963,8 @@ Terminal.prototype.bindMouse = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convert to cols/rows
|
// convert to cols/rows
|
||||||
w = self.element.clientWidth;
|
x = Math.ceil(x / self.charMeasure.width);
|
||||||
h = self.element.clientHeight;
|
y = Math.ceil(y / self.charMeasure.height);
|
||||||
x = Math.ceil((x / w) * self.cols);
|
|
||||||
y = Math.ceil((y / h) * self.rows);
|
|
||||||
|
|
||||||
// be sure to avoid sending
|
// be sure to avoid sending
|
||||||
// bad positions to the program
|
// bad positions to the program
|
||||||
@@ -1054,8 +1058,8 @@ Terminal.prototype.destroy = function() {
|
|||||||
/**
|
/**
|
||||||
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
|
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
|
||||||
* opportunity.
|
* opportunity.
|
||||||
* @param {number} start The row to start from (between 0 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 fromRow and terminal's height terminal - 1)
|
* @param {number} end The row to end at (between start and this.rows - 1).
|
||||||
*/
|
*/
|
||||||
Terminal.prototype.refresh = function(start, end) {
|
Terminal.prototype.refresh = function(start, end) {
|
||||||
if (this.renderer) {
|
if (this.renderer) {
|
||||||
@@ -1063,6 +1067,19 @@ Terminal.prototype.refresh = function(start, end) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Display the cursor element
|
||||||
*/
|
*/
|
||||||
@@ -1263,6 +1280,51 @@ Terminal.prototype.attachCustomKeydownHandler = function(customKeydownHandler) {
|
|||||||
this.customKeydownHandler = customKeydownHandler;
|
this.customKeydownHandler = customKeydownHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches a http(s) link handler, forcing web links to behave differently to
|
||||||
|
* regular <a> 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 {LinkMatcherOptions} [options] Options for the link matcher.
|
||||||
|
* @return {number} The ID of the new matcher, this can be used to deregister.
|
||||||
|
*/
|
||||||
|
Terminal.prototype.registerLinkMatcher = function(regex, handler, options) {
|
||||||
|
if (this.linkifier) {
|
||||||
|
var matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
|
||||||
|
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
|
* Handle a keydown event
|
||||||
* Key Resources:
|
* Key Resources:
|
||||||
|
|||||||
Reference in New Issue
Block a user