Comparing version 2.4.0 to 2.5.0
{ | ||
"name": "xterm.js", | ||
"version": "2.4.0", | ||
"version": "2.5.0", | ||
"ignore": ["demo", "test", ".gitignore"], | ||
@@ -5,0 +5,0 @@ "main": [ |
@@ -59,3 +59,3 @@ /** | ||
subjectRow.style.display = ''; // Revert style before calculating height, since they differ. | ||
characterHeight = parseInt(subjectRow.offsetHeight); | ||
characterHeight = subjectRow.getBoundingClientRect().height; | ||
subjectRow.innerHTML = contentBuffer; | ||
@@ -62,0 +62,0 @@ |
@@ -16,4 +16,6 @@ const browserify = require('browserify'); | ||
let buildDir = process.env.BUILD_DIR || 'build'; | ||
let tsProject = ts.createProject('tsconfig.json'); | ||
let srcDir = tsProject.config.compilerOptions.rootDir; | ||
let outDir = tsProject.config.compilerOptions.outDir; | ||
/** | ||
@@ -24,16 +26,15 @@ * Compile TypeScript sources to JavaScript files and create a source map file for each TypeScript | ||
gulp.task('tsc', function () { | ||
// Remove the lib/ directory to prevent confusion if files were deleted in src/ | ||
fs.emptyDirSync('lib'); | ||
// Remove the ${outDir}/ directory to prevent confusion if files were deleted in ${srcDir}/ | ||
fs.emptyDirSync(`${outDir}`); | ||
// Build all TypeScript files (including tests) to lib/, based on the configuration defined in | ||
// Build all TypeScript files (including tests) to ${outDir}/, based on the configuration defined in | ||
// `tsconfig.json`. | ||
let tsProject = ts.createProject('tsconfig.json'); | ||
let tsResult = tsProject.src().pipe(sourcemaps.init()).pipe(tsProject()); | ||
let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest('lib')); | ||
let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir)); | ||
// Copy all addons from src/ to lib/ | ||
let copyAddons = gulp.src('src/addons/**/*').pipe(gulp.dest('lib/addons')); | ||
// Copy all addons from ${srcDir}/ to ${outDir}/ | ||
let copyAddons = gulp.src(`${srcDir}/addons/**/*`).pipe(gulp.dest(`${outDir}/addons`)); | ||
// Copy stylesheets from src/ to lib/ | ||
let copyStylesheets = gulp.src('src/**/*.css').pipe(gulp.dest('lib')); | ||
// Copy stylesheets from ${srcDir}/ to ${outDir}/ | ||
let copyStylesheets = gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir)); | ||
@@ -54,3 +55,3 @@ return merge(tsc, copyAddons, copyStylesheets); | ||
debug: true, | ||
entries: ['../lib/xterm.js'], | ||
entries: [`../${outDir}/xterm.js`], | ||
standalone: 'Terminal', | ||
@@ -68,7 +69,7 @@ cache: {}, | ||
// Copy all add-ons from lib/ to buildDir | ||
let copyAddons = gulp.src('lib/addons/**/*').pipe(gulp.dest(`${buildDir}/addons`)); | ||
// Copy all add-ons from ${outDir}/ to buildDir | ||
let copyAddons = gulp.src(`${outDir}/addons/**/*`).pipe(gulp.dest(`${buildDir}/addons`)); | ||
// Copy stylesheets from src/ to lib/ | ||
let copyStylesheets = gulp.src('lib/**/*.css').pipe(gulp.dest(buildDir)); | ||
// Copy stylesheets from ${outDir}/ to ${buildDir}/ | ||
let copyStylesheets = gulp.src(`${outDir}/**/*.css`).pipe(gulp.dest(buildDir)); | ||
@@ -79,3 +80,3 @@ return merge(bundleStream, copyAddons, copyStylesheets); | ||
gulp.task('instrument-test', function () { | ||
return gulp.src(['lib/**/*.js']) | ||
return gulp.src([`${outDir}/**/*.js`]) | ||
// Covering files | ||
@@ -88,3 +89,3 @@ .pipe(istanbul()) | ||
gulp.task('mocha', ['instrument-test'], function () { | ||
return gulp.src(['lib/*test.js', 'lib/**/*test.js'], {read: false}) | ||
return gulp.src([`${outDir}/*test.js`, `${outDir}/**/*test.js`], {read: false}) | ||
.pipe(mocha()) | ||
@@ -97,7 +98,7 @@ .pipe(istanbul.writeReports()); | ||
* (Without this task the source maps produced for the JavaScript bundle points into the | ||
* compiled JavaScript files in lib/). | ||
* compiled JavaScript files in ${outDir}/). | ||
*/ | ||
gulp.task('sorcery', ['browserify'], function () { | ||
var chain = sorcery.loadSync(`${buildDir}/xterm.js`); | ||
var map = chain.apply(); | ||
chain.apply(); | ||
chain.writeSync(); | ||
@@ -104,0 +105,0 @@ }); |
@@ -59,3 +59,3 @@ /** | ||
subjectRow.style.display = ''; // Revert style before calculating height, since they differ. | ||
characterHeight = parseInt(subjectRow.offsetHeight); | ||
characterHeight = subjectRow.getBoundingClientRect().height; | ||
subjectRow.innerHTML = contentBuffer; | ||
@@ -62,0 +62,0 @@ |
@@ -247,3 +247,8 @@ "use strict"; | ||
case 3: | ||
; | ||
var scrollBackSize = this._terminal.lines.length - this._terminal.rows; | ||
if (scrollBackSize > 0) { | ||
this._terminal.lines.trimStart(scrollBackSize); | ||
this._terminal.ybase = Math.max(this._terminal.ybase - scrollBackSize, 0); | ||
this._terminal.ydisp = Math.max(this._terminal.ydisp - scrollBackSize, 0); | ||
} | ||
break; | ||
@@ -250,0 +255,0 @@ } |
@@ -13,4 +13,4 @@ "use strict"; | ||
var hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; | ||
var pathClause = '(\\/[\\/\\w\\.\\-%]*)*'; | ||
var queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;\\=\\.\\-]*'; | ||
var pathClause = '(\\/[\\/\\w\\.\\-%~]*)*'; | ||
var queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; | ||
var queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; | ||
@@ -25,6 +25,4 @@ var hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; | ||
var Linkifier = (function () { | ||
function Linkifier(document, rows) { | ||
function Linkifier() { | ||
this._nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; | ||
this._document = document; | ||
this._rows = rows; | ||
this._rowTimeoutIds = []; | ||
@@ -34,3 +32,10 @@ this._linkMatchers = []; | ||
} | ||
Linkifier.prototype.attachToDom = function (document, rows) { | ||
this._document = document; | ||
this._rows = rows; | ||
}; | ||
Linkifier.prototype.linkifyRow = function (rowIndex) { | ||
if (!this._document) { | ||
return; | ||
} | ||
var timeoutId = this._rowTimeoutIds[rowIndex]; | ||
@@ -42,5 +47,8 @@ if (timeoutId) { | ||
}; | ||
Linkifier.prototype.attachHypertextLinkHandler = function (handler) { | ||
Linkifier.prototype.setHypertextLinkHandler = function (handler) { | ||
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler; | ||
}; | ||
Linkifier.prototype.setHypertextValidationCallback = function (callback) { | ||
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback; | ||
}; | ||
Linkifier.prototype.registerLinkMatcher = function (regex, handler, options) { | ||
@@ -90,26 +98,33 @@ if (options === void 0) { options = {}; } | ||
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); | ||
} | ||
}); | ||
for (var i = 0; i < this._linkMatchers.length; i++) { | ||
var matcher = this._linkMatchers[i]; | ||
var linkElements = this._doLinkifyRow(row, matcher); | ||
if (linkElements.length > 0) { | ||
if (matcher.validationCallback) { | ||
var _loop_1 = function (j) { | ||
var element = linkElements[j]; | ||
matcher.validationCallback(element.textContent, element, function (isValid) { | ||
if (!isValid) { | ||
element.classList.add(INVALID_LINK_CLASS); | ||
} | ||
}); | ||
}; | ||
for (var j = 0; j < linkElements.length; j++) { | ||
_loop_1(j); | ||
} | ||
} | ||
return { value: void 0 }; | ||
return; | ||
} | ||
}; | ||
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; | ||
Linkifier.prototype._doLinkifyRow = function (row, matcher) { | ||
var result = []; | ||
var isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID; | ||
var nodes = row.childNodes; | ||
var match = row.textContent.match(matcher.regex); | ||
if (!match || match.length === 0) { | ||
return result; | ||
} | ||
var uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; | ||
var rowStartIndex = match.index + uri.length; | ||
for (var i = 0; i < nodes.length; i++) { | ||
@@ -119,3 +134,3 @@ var node = nodes[i]; | ||
if (searchIndex >= 0) { | ||
var linkElement = this._createAnchorElement(uri, handler, isHttpLinkMatcher); | ||
var linkElement = this._createAnchorElement(uri, matcher.handler, isHttpLinkMatcher); | ||
if (node.textContent.length === uri.length) { | ||
@@ -128,3 +143,3 @@ if (node.nodeType === 3) { | ||
if (element.nodeName === 'A') { | ||
return; | ||
return result; | ||
} | ||
@@ -136,18 +151,20 @@ element.innerHTML = ''; | ||
else { | ||
this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex); | ||
var nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex); | ||
i += nodesAdded; | ||
} | ||
return linkElement; | ||
result.push(linkElement); | ||
match = row.textContent.substring(rowStartIndex).match(matcher.regex); | ||
if (!match || match.length === 0) { | ||
return result; | ||
} | ||
uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; | ||
rowStartIndex += match.index + uri.length; | ||
} | ||
} | ||
return result; | ||
}; | ||
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; | ||
element.draggable = false; | ||
if (isHypertextLinkHandler) { | ||
@@ -188,3 +205,3 @@ element.href = uri; | ||
} | ||
if (node.childNodes.length === 0 && node.nodeType !== Node.TEXT_NODE) { | ||
if (node.childNodes.length === 0 && node.nodeType !== 3) { | ||
throw new Error('targetNode must be a text node or only contain a single text node'); | ||
@@ -194,18 +211,19 @@ } | ||
if (substringIndex === 0) { | ||
var rightText = fullText.substring(substring.length); | ||
var rightTextNode = this._document.createTextNode(rightText); | ||
this._replaceNode(node, newNode, rightTextNode); | ||
var rightText_1 = fullText.substring(substring.length); | ||
var rightTextNode_1 = this._document.createTextNode(rightText_1); | ||
this._replaceNode(node, newNode, rightTextNode_1); | ||
return 0; | ||
} | ||
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); | ||
if (substringIndex === targetNode.textContent.length - substring.length) { | ||
var leftText_1 = fullText.substring(0, substringIndex); | ||
var leftTextNode_1 = this._document.createTextNode(leftText_1); | ||
this._replaceNode(node, leftTextNode_1, newNode); | ||
return 0; | ||
} | ||
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); | ||
} | ||
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 1; | ||
}; | ||
@@ -212,0 +230,0 @@ return Linkifier; |
@@ -18,6 +18,6 @@ "use strict"; | ||
__extends(TestLinkifier, _super); | ||
function TestLinkifier(document, rows) { | ||
function TestLinkifier() { | ||
var _this = this; | ||
Linkifier_1.Linkifier.TIME_BEFORE_LINKIFY = 0; | ||
_this = _super.call(this, document, rows) || this; | ||
_this = _super.call(this) || this; | ||
return _this; | ||
@@ -39,64 +39,145 @@ } | ||
beforeEach(function (done) { | ||
rows = []; | ||
jsdom.env('', function (err, w) { | ||
window = w; | ||
document = window.document; | ||
linkifier = new TestLinkifier(document, rows); | ||
container = document.createElement('div'); | ||
document.body.appendChild(container); | ||
linkifier = new TestLinkifier(); | ||
done(); | ||
}); | ||
}); | ||
function addRow(text) { | ||
function addRow(html) { | ||
var element = document.createElement('div'); | ||
element.textContent = text; | ||
element.innerHTML = html; | ||
container.appendChild(element); | ||
rows.push(element); | ||
} | ||
function clickElement(element) { | ||
var 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', function () { | ||
it('should enable link if true', function (done) { | ||
addRow('test'); | ||
linkifier.registerLinkMatcher(/test/, function () { return done(); }, { | ||
validationCallback: function (url, cb) { | ||
cb(true); | ||
chai_1.assert.equal(rows[0].firstChild.tagName, 'A'); | ||
setTimeout(function () { return clickElement(rows[0].firstChild); }, 0); | ||
} | ||
describe('before attachToDom', function () { | ||
it('should allow link matcher registration', function (done) { | ||
chai_1.assert.doesNotThrow(function () { | ||
var linkMatcherId = linkifier.registerLinkMatcher(/foo/, function () { }); | ||
chai_1.assert.isTrue(linkifier.deregisterLinkMatcher(linkMatcherId)); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
describe('after attachToDom', function () { | ||
beforeEach(function () { | ||
rows = []; | ||
linkifier.attachToDom(document, rows); | ||
container = document.createElement('div'); | ||
document.body.appendChild(container); | ||
}); | ||
function clickElement(element) { | ||
var event = document.createEvent('MouseEvent'); | ||
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); | ||
element.dispatchEvent(event); | ||
} | ||
function assertLinkifiesEntireRow(uri, done) { | ||
addRow(uri); | ||
linkifier.linkifyRow(0); | ||
setTimeout(function () { | ||
chai_1.assert.equal(rows[0].firstChild.tagName, 'A'); | ||
chai_1.assert.equal(rows[0].firstChild.textContent, uri); | ||
done(); | ||
}, 0); | ||
} | ||
describe('http links', function () { | ||
function assertLinkifiesEntireRow(uri, done) { | ||
addRow(uri); | ||
linkifier.linkifyRow(0); | ||
setTimeout(function () { | ||
chai_1.assert.equal(rows[0].firstChild.tagName, 'A'); | ||
chai_1.assert.equal(rows[0].firstChild.textContent, uri); | ||
done(); | ||
}, 0); | ||
} | ||
it('should allow ~ character in URI path', function (done) { return assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done); }); | ||
}); | ||
it('should disable link if false', function (done) { | ||
addRow('test'); | ||
linkifier.registerLinkMatcher(/test/, function () { return chai_1.assert.fail(); }, { | ||
validationCallback: function (url, cb) { | ||
cb(false); | ||
chai_1.assert.equal(rows[0].firstChild.tagName, 'A'); | ||
setTimeout(function () { return clickElement(rows[0].firstChild); }, 0); | ||
} | ||
describe('link matcher', function () { | ||
function assertLinkifiesRow(rowText, linkMatcherRegex, expectedHtml, done) { | ||
addRow(rowText); | ||
linkifier.registerLinkMatcher(linkMatcherRegex, function () { }); | ||
linkifier.linkifyRow(0); | ||
setTimeout(function () { | ||
chai_1.assert.equal(rows[0].innerHTML, expectedHtml); | ||
done(); | ||
}, 0); | ||
} | ||
it('should match a single link', function (done) { | ||
assertLinkifiesRow('foo', /foo/, '<a>foo</a>', done); | ||
}); | ||
linkifier.linkifyRow(0); | ||
setTimeout(function () { return done(); }, 10); | ||
it('should match a single link at the start of a text node', function (done) { | ||
assertLinkifiesRow('foo bar', /foo/, '<a>foo</a> bar', done); | ||
}); | ||
it('should match a single link in the middle of a text node', function (done) { | ||
assertLinkifiesRow('foo bar baz', /bar/, 'foo <a>bar</a> baz', done); | ||
}); | ||
it('should match a single link at the end of a text node', function (done) { | ||
assertLinkifiesRow('foo bar', /bar/, 'foo <a>bar</a>', done); | ||
}); | ||
it('should match a link after a link at the start of a text node', function (done) { | ||
assertLinkifiesRow('foo bar', /foo|bar/, '<a>foo</a> <a>bar</a>', done); | ||
}); | ||
it('should match a link after a link in the middle of a text node', function (done) { | ||
assertLinkifiesRow('foo bar baz', /bar|baz/, 'foo <a>bar</a> <a>baz</a>', done); | ||
}); | ||
it('should match a link immediately after a link at the end of a text node', function (done) { | ||
assertLinkifiesRow('<span>foo bar</span>baz', /bar|baz/, '<span>foo <a>bar</a></span><a>baz</a>', done); | ||
}); | ||
}); | ||
}); | ||
describe('priority', function () { | ||
it('should order the list from highest priority to lowest #1', function () { | ||
var aId = linkifier.registerLinkMatcher(/a/, function () { }, { priority: 1 }); | ||
var bId = linkifier.registerLinkMatcher(/b/, function () { }, { priority: -1 }); | ||
chai_1.assert.deepEqual(linkifier.linkMatchers.map(function (lm) { return lm.id; }), [aId, 0, bId]); | ||
describe('validationCallback', function () { | ||
it('should enable link if true', function (done) { | ||
addRow('test'); | ||
linkifier.registerLinkMatcher(/test/, function () { return done(); }, { | ||
validationCallback: function (url, element, cb) { | ||
cb(true); | ||
chai_1.assert.equal(rows[0].firstChild.tagName, 'A'); | ||
setTimeout(function () { return clickElement(rows[0].firstChild); }, 0); | ||
} | ||
}); | ||
linkifier.linkifyRow(0); | ||
}); | ||
it('should disable link if false', function (done) { | ||
addRow('test'); | ||
linkifier.registerLinkMatcher(/test/, function () { return chai_1.assert.fail(); }, { | ||
validationCallback: function (url, element, cb) { | ||
cb(false); | ||
chai_1.assert.equal(rows[0].firstChild.tagName, 'A'); | ||
setTimeout(function () { return clickElement(rows[0].firstChild); }, 0); | ||
} | ||
}); | ||
linkifier.linkifyRow(0); | ||
setTimeout(function () { return done(); }, 10); | ||
}); | ||
it('should trigger for multiple link matches on one row', function (done) { | ||
addRow('test test'); | ||
var count = 0; | ||
linkifier.registerLinkMatcher(/test/, function () { return chai_1.assert.fail(); }, { | ||
validationCallback: function (url, element, cb) { | ||
count += 1; | ||
if (count === 2) { | ||
done(); | ||
} | ||
cb(false); | ||
} | ||
}); | ||
linkifier.linkifyRow(0); | ||
}); | ||
}); | ||
it('should order the list from highest priority to lowest #2', function () { | ||
var aId = linkifier.registerLinkMatcher(/a/, function () { }, { priority: -1 }); | ||
var bId = linkifier.registerLinkMatcher(/b/, function () { }, { priority: 1 }); | ||
chai_1.assert.deepEqual(linkifier.linkMatchers.map(function (lm) { return lm.id; }), [bId, 0, aId]); | ||
describe('priority', function () { | ||
it('should order the list from highest priority to lowest #1', function () { | ||
var aId = linkifier.registerLinkMatcher(/a/, function () { }, { priority: 1 }); | ||
var bId = linkifier.registerLinkMatcher(/b/, function () { }, { priority: -1 }); | ||
chai_1.assert.deepEqual(linkifier.linkMatchers.map(function (lm) { return lm.id; }), [aId, 0, bId]); | ||
}); | ||
it('should order the list from highest priority to lowest #2', function () { | ||
var aId = linkifier.registerLinkMatcher(/a/, function () { }, { priority: -1 }); | ||
var bId = linkifier.registerLinkMatcher(/b/, function () { }, { priority: 1 }); | ||
chai_1.assert.deepEqual(linkifier.linkMatchers.map(function (lm) { return lm.id; }), [bId, 0, aId]); | ||
}); | ||
it('should order items of equal priority in the order they are added', function () { | ||
var aId = linkifier.registerLinkMatcher(/a/, function () { }, { priority: 0 }); | ||
var bId = linkifier.registerLinkMatcher(/b/, function () { }, { priority: 0 }); | ||
chai_1.assert.deepEqual(linkifier.linkMatchers.map(function (lm) { return lm.id; }), [0, aId, bId]); | ||
}); | ||
}); | ||
it('should order items of equal priority in the order they are added', function () { | ||
var aId = linkifier.registerLinkMatcher(/a/, function () { }, { priority: 0 }); | ||
var bId = linkifier.registerLinkMatcher(/b/, function () { }, { priority: 0 }); | ||
chai_1.assert.deepEqual(linkifier.linkMatchers.map(function (lm) { return lm.id; }), [0, aId, bId]); | ||
}); | ||
}); | ||
@@ -103,0 +184,0 @@ }); |
@@ -21,3 +21,3 @@ "use strict"; | ||
if (brokenBold === null) { | ||
brokenBold = checkBoldBroken(this._terminal.document); | ||
brokenBold = checkBoldBroken(this._terminal.element); | ||
} | ||
@@ -199,14 +199,16 @@ } | ||
exports.Renderer = Renderer; | ||
function checkBoldBroken(document) { | ||
var body = document.getElementsByTagName('body')[0]; | ||
function checkBoldBroken(terminal) { | ||
var document = terminal.ownerDocument; | ||
var el = document.createElement('span'); | ||
el.innerHTML = 'hello world'; | ||
body.appendChild(el); | ||
var w1 = el.scrollWidth; | ||
terminal.appendChild(el); | ||
var w1 = el.offsetWidth; | ||
var h1 = el.offsetHeight; | ||
el.style.fontWeight = 'bold'; | ||
var w2 = el.scrollWidth; | ||
body.removeChild(el); | ||
return w1 !== w2; | ||
var w2 = el.offsetWidth; | ||
var h2 = el.offsetHeight; | ||
terminal.removeChild(el); | ||
return w1 !== w2 || h1 !== h2; | ||
} | ||
//# sourceMappingURL=Renderer.js.map |
@@ -21,3 +21,4 @@ var assert = require('chai').assert; | ||
classList: { | ||
toggle: function () { } | ||
toggle: function () { }, | ||
remove: function () { } | ||
} | ||
@@ -24,0 +25,0 @@ }; |
@@ -18,2 +18,3 @@ "use strict"; | ||
var WRITE_BATCH_SIZE = 300; | ||
var CURSOR_BLINK_INTERVAL = 600; | ||
function Terminal(options) { | ||
@@ -76,2 +77,3 @@ var self = this; | ||
this.customKeydownHandler = null; | ||
this.cursorBlinkInterval = null; | ||
this.applicationKeypad = false; | ||
@@ -115,4 +117,3 @@ this.applicationCursor = false; | ||
this.renderer = this.renderer || null; | ||
this.linkifier = this.linkifier || null; | ||
; | ||
this.linkifier = this.linkifier || new Linkifier_1.Linkifier(); | ||
this.writeBuffer = []; | ||
@@ -249,3 +250,3 @@ this.writeInProgress = false; | ||
case 'cursorBlink': | ||
this.element.classList.toggle('xterm-cursor-blink', value); | ||
this.setCursorBlinking(value); | ||
break; | ||
@@ -261,2 +262,22 @@ case 'cursorStyle': | ||
}; | ||
Terminal.prototype.restartCursorBlinking = function () { | ||
this.setCursorBlinking(this.options.cursorBlink); | ||
}; | ||
Terminal.prototype.setCursorBlinking = function (enabled) { | ||
this.element.classList.toggle('xterm-cursor-blink', enabled); | ||
this.clearCursorBlinkingInterval(); | ||
if (enabled) { | ||
var self = this; | ||
this.cursorBlinkInterval = setInterval(function () { | ||
self.element.classList.toggle('xterm-cursor-blink-on'); | ||
}, CURSOR_BLINK_INTERVAL); | ||
} | ||
}; | ||
Terminal.prototype.clearCursorBlinkingInterval = function () { | ||
this.element.classList.remove('xterm-cursor-blink-on'); | ||
if (this.cursorBlinkInterval) { | ||
clearInterval(this.cursorBlinkInterval); | ||
this.cursorBlinkInterval = null; | ||
} | ||
}; | ||
Terminal.bindFocus = function (term) { | ||
@@ -269,2 +290,3 @@ on(term.textarea, 'focus', function (ev) { | ||
term.showCursor(); | ||
term.restartCursorBlinking.apply(term); | ||
Terminal.focus = term; | ||
@@ -284,2 +306,3 @@ term.emit('focus', { terminal: term }); | ||
term.element.classList.remove('focus'); | ||
term.clearCursorBlinkingInterval.apply(term); | ||
Terminal.focus = null; | ||
@@ -371,3 +394,3 @@ term.emit('blur', { terminal: term }); | ||
this.element.classList.add('xterm-theme-' + this.theme); | ||
this.element.classList.toggle('xterm-cursor-blink', this.options.cursorBlink); | ||
this.setCursorBlinking(this.options.cursorBlink); | ||
this.element.style.height; | ||
@@ -385,3 +408,3 @@ this.element.setAttribute('tabindex', 0); | ||
this.children = []; | ||
this.linkifier = new Linkifier_1.Linkifier(document, this.children); | ||
this.linkifier.attachToDom(document, this.children); | ||
this.helperContainer = document.createElement('div'); | ||
@@ -815,9 +838,16 @@ this.helperContainer.classList.add('xterm-helpers'); | ||
}; | ||
Terminal.prototype.attachHypertextLinkHandler = function (handler) { | ||
Terminal.prototype.setHypertextLinkHandler = function (handler) { | ||
if (!this.linkifier) { | ||
throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); | ||
} | ||
this.linkifier.attachHypertextLinkHandler(handler); | ||
this.linkifier.setHypertextLinkHandler(handler); | ||
this.refresh(0, this.rows - 1); | ||
}; | ||
Terminal.prototype.setHypertextValidationCallback = function (handler) { | ||
if (!this.linkifier) { | ||
throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called'); | ||
} | ||
this.linkifier.setHypertextValidationCallback(handler); | ||
this.refresh(0, this.rows - 1); | ||
}; | ||
Terminal.prototype.registerLinkMatcher = function (regex, handler, options) { | ||
@@ -841,2 +871,3 @@ if (this.linkifier) { | ||
} | ||
this.restartCursorBlinking(); | ||
if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { | ||
@@ -1236,10 +1267,2 @@ if (this.ybase !== this.ydisp) { | ||
} | ||
else { | ||
i = this.lines.length; | ||
while (i--) { | ||
while (this.lines.get(i).length > x) { | ||
this.lines.get(i).pop(); | ||
} | ||
} | ||
} | ||
this.cols = x; | ||
@@ -1246,0 +1269,0 @@ this.setupStops(this.cols); |
{ | ||
"name": "xterm", | ||
"description": "Full xterm terminal, in your browser", | ||
"version": "2.4.0", | ||
"version": "2.5.0", | ||
"ignore": [ | ||
@@ -71,3 +71,3 @@ "demo", | ||
"start": "node demo/app", | ||
"dev": "nodemon -e js,ts --watch src --watch demo --exec npm start", | ||
"dev": "nodemon -e js,ts,css --watch src --watch demo --exec npm start", | ||
"lint": "tslint src/*.ts src/**/*.ts", | ||
@@ -74,0 +74,0 @@ "test": "gulp test", |
@@ -1,2 +0,2 @@ | ||
# xterm.js | ||
# [![xterm.js logo](logo.png)](https://xtermjs.org) | ||
@@ -33,2 +33,4 @@ [![xterm.js build status](https://api.travis-ci.org/sourcelair/xterm.js.svg)](https://travis-ci.org/sourcelair/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/sourcelair/xterm.js/badge.svg)](https://coveralls.io/github/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) | ||
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2. | ||
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE. | ||
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor. | ||
@@ -90,3 +92,3 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. | ||
var term = new Terminal(); | ||
term.open(document.getElementById('#terminal')); | ||
term.open(document.getElementById('terminal')); | ||
term.write('Hello from \033[1;3;31mxterm.js\033[0m $ ') | ||
@@ -126,24 +128,4 @@ </script> | ||
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) before. | ||
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo. | ||
The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript and a browser (if you would like to run the demo you will need Node.js to get all features). | ||
It is recommended though to use a development tool that uses xterm.js internally, to develop for xterm.js. [Eating our own dogfood](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) has been proved extremely beneficial for this project. Known tools that use xterm.js internally are: | ||
#### [SourceLair](https://www.sourcelair.com) | ||
Visit https://lair.io/sourcelair/xterm and follow the instructions. All development will happen in your browser. | ||
#### [Visual Studio Code](http://code.visualstudio.com/) | ||
[Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set. | ||
#### [Eclipse Che](http://www.eclipse.org/che) | ||
You can start Eclipse Che with `docker run eclipse/che start`. | ||
#### [Codenvy](http://www.codenvy.io) | ||
You can create a trial account or install an enterprise version with `docker run codenvy/cli start`. | ||
## License Agreement | ||
@@ -150,0 +132,0 @@ |
@@ -59,3 +59,3 @@ /** | ||
subjectRow.style.display = ''; // Revert style before calculating height, since they differ. | ||
characterHeight = parseInt(subjectRow.offsetHeight); | ||
characterHeight = subjectRow.getBoundingClientRect().height; | ||
subjectRow.innerHTML = contentBuffer; | ||
@@ -62,0 +62,0 @@ |
@@ -388,3 +388,9 @@ /** | ||
case 3: | ||
; // no saved lines | ||
// Clear scrollback (everything not in viewport) | ||
const scrollBackSize = this._terminal.lines.length - this._terminal.rows; | ||
if (scrollBackSize > 0) { | ||
this._terminal.lines.trimStart(scrollBackSize); | ||
this._terminal.ybase = Math.max(this._terminal.ybase - scrollBackSize, 0); | ||
this._terminal.ydisp = Math.max(this._terminal.ydisp - scrollBackSize, 0); | ||
} | ||
break; | ||
@@ -391,0 +397,0 @@ } |
@@ -11,5 +11,5 @@ /** | ||
class TestLinkifier extends Linkifier { | ||
constructor(document: Document, rows: HTMLElement[]) { | ||
constructor() { | ||
Linkifier.TIME_BEFORE_LINKIFY = 0; | ||
super(document, rows); | ||
super(); | ||
} | ||
@@ -29,9 +29,6 @@ | ||
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); | ||
linkifier = new TestLinkifier(); | ||
done(); | ||
@@ -41,5 +38,5 @@ }); | ||
function addRow(text: string) { | ||
function addRow(html: string) { | ||
const element = document.createElement('div'); | ||
element.textContent = text; | ||
element.innerHTML = html; | ||
container.appendChild(element); | ||
@@ -49,55 +46,146 @@ 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); | ||
} | ||
describe('before attachToDom', () => { | ||
it('should allow link matcher registration', done => { | ||
assert.doesNotThrow(() => { | ||
const linkMatcherId = linkifier.registerLinkMatcher(/foo/, () => {}); | ||
assert.isTrue(linkifier.deregisterLinkMatcher(linkMatcherId)); | ||
done(); | ||
}); | ||
linkifier.linkifyRow(0); | ||
}); | ||
}); | ||
it('should disable link if false', done => { | ||
addRow('test'); | ||
linkifier.registerLinkMatcher(/test/, () => assert.fail(), { | ||
validationCallback: (url, cb) => { | ||
cb(false); | ||
describe('after attachToDom', () => { | ||
beforeEach(() => { | ||
rows = []; | ||
linkifier.attachToDom(document, rows); | ||
container = document.createElement('div'); | ||
document.body.appendChild(container); | ||
}); | ||
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); | ||
} | ||
function assertLinkifiesEntireRow(uri: string, done: MochaDone) { | ||
addRow(uri); | ||
linkifier.linkifyRow(0); | ||
setTimeout(() => { | ||
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); | ||
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri); | ||
done(); | ||
}, 0); | ||
} | ||
describe('http links', () => { | ||
function assertLinkifiesEntireRow(uri: string, done: MochaDone) { | ||
addRow(uri); | ||
linkifier.linkifyRow(0); | ||
setTimeout(() => { | ||
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A'); | ||
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri); | ||
done(); | ||
}, 0); | ||
} | ||
it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done)); | ||
}); | ||
}); | ||
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]); | ||
describe('link matcher', () => { | ||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone) { | ||
addRow(rowText); | ||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); | ||
linkifier.linkifyRow(0); | ||
// Allow linkify to happen | ||
setTimeout(() => { | ||
assert.equal(rows[0].innerHTML, expectedHtml); | ||
done(); | ||
}, 0); | ||
} | ||
it('should match a single link', done => { | ||
assertLinkifiesRow('foo', /foo/, '<a>foo</a>', done); | ||
}); | ||
it('should match a single link at the start of a text node', done => { | ||
assertLinkifiesRow('foo bar', /foo/, '<a>foo</a> bar', done); | ||
}); | ||
it('should match a single link in the middle of a text node', done => { | ||
assertLinkifiesRow('foo bar baz', /bar/, 'foo <a>bar</a> baz', done); | ||
}); | ||
it('should match a single link at the end of a text node', done => { | ||
assertLinkifiesRow('foo bar', /bar/, 'foo <a>bar</a>', done); | ||
}); | ||
it('should match a link after a link at the start of a text node', done => { | ||
assertLinkifiesRow('foo bar', /foo|bar/, '<a>foo</a> <a>bar</a>', done); | ||
}); | ||
it('should match a link after a link in the middle of a text node', done => { | ||
assertLinkifiesRow('foo bar baz', /bar|baz/, 'foo <a>bar</a> <a>baz</a>', done); | ||
}); | ||
it('should match a link immediately after a link at the end of a text node', done => { | ||
assertLinkifiesRow('<span>foo bar</span>baz', /bar|baz/, '<span>foo <a>bar</a></span><a>baz</a>', done); | ||
}); | ||
}); | ||
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]); | ||
describe('validationCallback', () => { | ||
it('should enable link if true', done => { | ||
addRow('test'); | ||
linkifier.registerLinkMatcher(/test/, () => done(), { | ||
validationCallback: (url, element, 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, element, 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); | ||
}); | ||
it('should trigger for multiple link matches on one row', done => { | ||
addRow('test test'); | ||
let count = 0; | ||
linkifier.registerLinkMatcher(/test/, () => assert.fail(), { | ||
validationCallback: (url, element, cb) => { | ||
count += 1; | ||
if (count === 2) { | ||
done(); | ||
} | ||
cb(false); | ||
} | ||
}); | ||
linkifier.linkifyRow(0); | ||
}); | ||
}); | ||
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]); | ||
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]); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -19,4 +19,4 @@ /** | ||
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; | ||
const pathClause = '(\\/[\\/\\w\\.\\-%]*)*'; | ||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;\\=\\.\\-]*'; | ||
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*'; | ||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; | ||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; | ||
@@ -41,4 +41,4 @@ const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; | ||
* 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. | ||
* the costly operation of searching every row multiple times, potentially a | ||
* huge amount of times. | ||
*/ | ||
@@ -54,5 +54,3 @@ protected static TIME_BEFORE_LINKIFY = 200; | ||
constructor(document: Document, rows: HTMLElement[]) { | ||
this._document = document; | ||
this._rows = rows; | ||
constructor() { | ||
this._rowTimeoutIds = []; | ||
@@ -64,2 +62,12 @@ this._linkMatchers = []; | ||
/** | ||
* Attaches the linkifier to the DOM, enabling linkification. | ||
* @param document The document object. | ||
* @param rows The array of rows to apply links to. | ||
*/ | ||
public attachToDom(document: Document, rows: HTMLElement[]) { | ||
this._document = document; | ||
this._rows = rows; | ||
} | ||
/** | ||
* Queues a row for linkification. | ||
@@ -69,2 +77,7 @@ * @param {number} rowIndex The index of the row to linkify. | ||
public linkifyRow(rowIndex: number): void { | ||
// Don't attempt linkify if not yet attached to DOM | ||
if (!this._document) { | ||
return; | ||
} | ||
const timeoutId = this._rowTimeoutIds[rowIndex]; | ||
@@ -83,3 +96,3 @@ if (timeoutId) { | ||
*/ | ||
public attachHypertextLinkHandler(handler: LinkMatcherHandler): void { | ||
public setHypertextLinkHandler(handler: LinkMatcherHandler): void { | ||
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler; | ||
@@ -89,2 +102,11 @@ } | ||
/** | ||
* Attaches a validation callback for hypertext links. | ||
* @param {LinkMatcherValidationCallback} 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 | ||
@@ -165,12 +187,14 @@ * handled. | ||
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); | ||
const linkElements = this._doLinkifyRow(row, matcher); | ||
if (linkElements.length > 0) { | ||
// Fire validation callback | ||
if (linkElement && matcher.validationCallback) { | ||
matcher.validationCallback(uri, isValid => { | ||
if (!isValid) { | ||
linkElement.classList.add(INVALID_LINK_CLASS); | ||
} | ||
}); | ||
if (matcher.validationCallback) { | ||
for (let j = 0; j < linkElements.length; j++) { | ||
const element = linkElements[j]; | ||
matcher.validationCallback(element.textContent, element, isValid => { | ||
if (!isValid) { | ||
element.classList.add(INVALID_LINK_CLASS); | ||
} | ||
}); | ||
} | ||
} | ||
@@ -185,10 +209,21 @@ // Only allow a single LinkMatcher to trigger on any given row. | ||
* 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. | ||
* @param {HTMLElement} row The row to linkify. | ||
* @param {LinkMatcher} matcher The link matcher for this line. | ||
* @return The link element if it was added, otherwise undefined. | ||
*/ | ||
private _doLinkifyRow(rowIndex: number, uri: string, handler: LinkMatcherHandler, isHttpLinkMatcher: boolean): HTMLElement { | ||
private _doLinkifyRow(row: HTMLElement, matcher: LinkMatcher): HTMLElement[] { | ||
// Iterate over nodes as we want to consider text nodes | ||
const nodes = this._rows[rowIndex].childNodes; | ||
let result = []; | ||
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID; | ||
const nodes = row.childNodes; | ||
// Find the first match | ||
let match = row.textContent.match(matcher.regex); | ||
if (!match || match.length === 0) { | ||
return result; | ||
} | ||
let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; | ||
// Set the next searches start index | ||
let rowStartIndex = match.index + uri.length; | ||
for (let i = 0; i < nodes.length; i++) { | ||
@@ -198,6 +233,5 @@ const node = nodes[i]; | ||
if (searchIndex >= 0) { | ||
const linkElement = this._createAnchorElement(uri, handler, isHttpLinkMatcher); | ||
const linkElement = this._createAnchorElement(uri, matcher.handler, isHttpLinkMatcher); | ||
if (node.textContent.length === uri.length) { | ||
// Matches entire string | ||
if (node.nodeType === 3 /*Node.TEXT_NODE*/) { | ||
@@ -209,3 +243,3 @@ this._replaceNode(node, linkElement); | ||
// This row has already been linkified | ||
return; | ||
return result; | ||
} | ||
@@ -217,24 +251,21 @@ element.innerHTML = ''; | ||
// Matches part of string | ||
this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex); | ||
const nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex); | ||
// No need to consider the new nodes | ||
i += nodesAdded; | ||
} | ||
return linkElement; | ||
result.push(linkElement); | ||
// Find the next match | ||
match = row.textContent.substring(rowStartIndex).match(matcher.regex); | ||
if (!match || match.length === 0) { | ||
return result; | ||
} | ||
uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; | ||
rowStartIndex += match.index + uri.length; | ||
} | ||
} | ||
return result; | ||
} | ||
/** | ||
* 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. | ||
@@ -247,2 +278,3 @@ * @param {string} uri The uri of the link. | ||
element.textContent = uri; | ||
element.draggable = false; | ||
if (isHypertextLinkHandler) { | ||
@@ -289,4 +321,5 @@ element.href = uri; | ||
* @param {number} substringIndex The index of the substring within the string. | ||
* @return The number of nodes to skip when searching for the next uri. | ||
*/ | ||
private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): void { | ||
private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): number { | ||
let node = targetNode; | ||
@@ -300,3 +333,3 @@ if (node.nodeType !== 3/*Node.TEXT_NODE*/) { | ||
// text nodes potentially on either side. | ||
if (node.childNodes.length === 0 && node.nodeType !== Node.TEXT_NODE) { | ||
if (node.childNodes.length === 0 && node.nodeType !== 3/*Node.TEXT_NODE*/) { | ||
throw new Error('targetNode must be a text node or only contain a single text node'); | ||
@@ -312,3 +345,6 @@ } | ||
this._replaceNode(node, newNode, rightTextNode); | ||
} else if (substringIndex === targetNode.textContent.length - substring.length) { | ||
return 0; | ||
} | ||
if (substringIndex === targetNode.textContent.length - substring.length) { | ||
// Replace with <textnode><newNode> | ||
@@ -318,11 +354,13 @@ const leftText = fullText.substring(0, substringIndex); | ||
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); | ||
return 0; | ||
} | ||
// 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); | ||
return 1; | ||
} | ||
} |
@@ -37,3 +37,3 @@ /** | ||
if (brokenBold === null) { | ||
brokenBold = checkBoldBroken((<any>this._terminal).document); | ||
brokenBold = checkBoldBroken((<any>this._terminal).element); | ||
} | ||
@@ -295,12 +295,14 @@ | ||
// use it in the terminal. | ||
function checkBoldBroken(document) { | ||
const body = document.getElementsByTagName('body')[0]; | ||
function checkBoldBroken(terminal) { | ||
const document = terminal.ownerDocument; | ||
const el = document.createElement('span'); | ||
el.innerHTML = 'hello world'; | ||
body.appendChild(el); | ||
const w1 = el.scrollWidth; | ||
terminal.appendChild(el); | ||
const w1 = el.offsetWidth; | ||
const h1 = el.offsetHeight; | ||
el.style.fontWeight = 'bold'; | ||
const w2 = el.scrollWidth; | ||
body.removeChild(el); | ||
return w1 !== w2; | ||
const w2 = el.offsetWidth; | ||
const h2 = el.offsetHeight; | ||
terminal.removeChild(el); | ||
return w1 !== w2 || h1 !== h2; | ||
} |
@@ -24,3 +24,4 @@ var assert = require('chai').assert; | ||
classList: { | ||
toggle: function(){} | ||
toggle: function(){}, | ||
remove: function(){} | ||
} | ||
@@ -27,0 +28,0 @@ }; |
@@ -14,2 +14,2 @@ /** | ||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void; | ||
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; | ||
export type LinkMatcherValidationCallback = (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void; |
@@ -56,2 +56,9 @@ /** | ||
/** | ||
* The time between cursor blinks. This is driven by JS rather than a CSS | ||
* animation due to a bug in Chromium that causes it to use excessive CPU time. | ||
* See https://github.com/Microsoft/vscode/issues/22900 | ||
*/ | ||
var CURSOR_BLINK_INTERVAL = 600; | ||
/** | ||
* Terminal | ||
@@ -163,2 +170,3 @@ */ | ||
this.customKeydownHandler = null; | ||
this.cursorBlinkInterval = null; | ||
@@ -216,3 +224,3 @@ // modes | ||
this.renderer = this.renderer || null; | ||
this.linkifier = this.linkifier || null;; | ||
this.linkifier = this.linkifier || new Linkifier(); | ||
@@ -429,3 +437,3 @@ // user input states | ||
switch (key) { | ||
case 'cursorBlink': this.element.classList.toggle('xterm-cursor-blink', value); break; | ||
case 'cursorBlink': this.setCursorBlinking(value); break; | ||
case 'cursorStyle': | ||
@@ -440,2 +448,25 @@ // Style 'block' applies with no class | ||
Terminal.prototype.restartCursorBlinking = function () { | ||
this.setCursorBlinking(this.options.cursorBlink); | ||
}; | ||
Terminal.prototype.setCursorBlinking = function (enabled) { | ||
this.element.classList.toggle('xterm-cursor-blink', enabled); | ||
this.clearCursorBlinkingInterval(); | ||
if (enabled) { | ||
var self = this; | ||
this.cursorBlinkInterval = setInterval(function () { | ||
self.element.classList.toggle('xterm-cursor-blink-on'); | ||
}, CURSOR_BLINK_INTERVAL); | ||
} | ||
}; | ||
Terminal.prototype.clearCursorBlinkingInterval = function () { | ||
this.element.classList.remove('xterm-cursor-blink-on'); | ||
if (this.cursorBlinkInterval) { | ||
clearInterval(this.cursorBlinkInterval); | ||
this.cursorBlinkInterval = null; | ||
} | ||
}; | ||
/** | ||
@@ -453,2 +484,3 @@ * Binds the desired focus behavior on a given terminal object. | ||
term.showCursor(); | ||
term.restartCursorBlinking.apply(term); | ||
Terminal.focus = term; | ||
@@ -478,2 +510,3 @@ term.emit('focus', {terminal: term}); | ||
term.element.classList.remove('focus'); | ||
term.clearCursorBlinkingInterval.apply(term); | ||
Terminal.focus = null; | ||
@@ -604,3 +637,3 @@ term.emit('blur', {terminal: term}); | ||
this.element.classList.add('xterm-theme-' + this.theme); | ||
this.element.classList.toggle('xterm-cursor-blink', this.options.cursorBlink); | ||
this.setCursorBlinking(this.options.cursorBlink); | ||
@@ -623,3 +656,3 @@ this.element.style.height | ||
this.children = []; | ||
this.linkifier = new Linkifier(document, this.children); | ||
this.linkifier.attachToDom(document, this.children); | ||
@@ -1298,7 +1331,7 @@ // Create the container that will hold helpers like the textarea for | ||
*/ | ||
Terminal.prototype.attachHypertextLinkHandler = function(handler) { | ||
Terminal.prototype.setHypertextLinkHandler = function(handler) { | ||
if (!this.linkifier) { | ||
throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); | ||
} | ||
this.linkifier.attachHypertextLinkHandler(handler); | ||
this.linkifier.setHypertextLinkHandler(handler); | ||
// Refresh to force links to refresh | ||
@@ -1309,2 +1342,17 @@ 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 {LinkMatcherValidationCallback} callback The callback to use, this can | ||
* be cleared with null. | ||
*/ | ||
Terminal.prototype.setHypertextValidationCallback = function(handler) { | ||
if (!this.linkifier) { | ||
throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called'); | ||
} | ||
this.linkifier.setHypertextValidationCallback(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 | ||
@@ -1350,2 +1398,4 @@ * handled. | ||
this.restartCursorBlinking(); | ||
if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { | ||
@@ -1820,10 +1870,4 @@ if (this.ybase !== this.ydisp) { | ||
} | ||
} else { // (j > x) | ||
i = this.lines.length; | ||
while (i--) { | ||
while (this.lines.get(i).length > x) { | ||
this.lines.get(i).pop(); | ||
} | ||
} | ||
} | ||
this.cols = x; | ||
@@ -1830,0 +1874,0 @@ this.setupStops(this.cols); |
@@ -15,11 +15,4 @@ { | ||
"exclude": [ | ||
"src/addons/**/*", | ||
"build", | ||
"demo", | ||
"dist", | ||
"out", | ||
"test", | ||
"node_modules", | ||
"docs" | ||
"src/addons/**/*" | ||
] | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1300000
24510
135