mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
@@ -97,12 +97,10 @@ xterm.fit();
|
||||
|
||||
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
|
||||
|
||||
- Chrome 48+
|
||||
- Edge 13+
|
||||
- Firefox 44+
|
||||
- Internet Explorer 11+
|
||||
- Opera 35+
|
||||
- Safari 8+
|
||||
- Chrome latest
|
||||
- Edge latest
|
||||
- Firefox latest
|
||||
- Safari latest
|
||||
|
||||
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
|
||||
|
||||
|
||||
+24
-25
@@ -2,9 +2,7 @@ var term,
|
||||
protocol,
|
||||
socketURL,
|
||||
socket,
|
||||
pid,
|
||||
charWidth,
|
||||
charHeight;
|
||||
pid;
|
||||
|
||||
var terminalContainer = document.getElementById('terminal-container'),
|
||||
actionElements = {
|
||||
@@ -21,11 +19,13 @@ var terminalContainer = document.getElementById('terminal-container'),
|
||||
colsElement = document.getElementById('cols'),
|
||||
rowsElement = document.getElementById('rows');
|
||||
|
||||
function setTerminalSize () {
|
||||
var cols = parseInt(colsElement.value, 10),
|
||||
rows = parseInt(rowsElement.value, 10),
|
||||
width = (cols * charWidth).toString() + 'px',
|
||||
height = (rows * charHeight).toString() + 'px';
|
||||
function setTerminalSize() {
|
||||
var cols = parseInt(colsElement.value, 10);
|
||||
var rows = parseInt(rowsElement.value, 10);
|
||||
var viewportElement = document.querySelector('.xterm-viewport');
|
||||
var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth;
|
||||
var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px';
|
||||
var height = (rows * term.charMeasure.height).toString() + 'px';
|
||||
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
@@ -92,27 +92,26 @@ function createTerminal() {
|
||||
term.open(terminalContainer);
|
||||
term.fit();
|
||||
|
||||
var initialGeometry = term.proposeGeometry(),
|
||||
cols = initialGeometry.cols,
|
||||
rows = initialGeometry.rows;
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
setTimeout(() => {
|
||||
colsElement.value = term.cols;
|
||||
rowsElement.value = term.rows;
|
||||
|
||||
colsElement.value = cols;
|
||||
rowsElement.value = rows;
|
||||
// Set terminal size again to set the specific dimensions on the demo
|
||||
setTerminalSize();
|
||||
|
||||
fetch('/terminals?cols=' + cols + '&rows=' + rows, {method: 'POST'}).then(function (res) {
|
||||
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
|
||||
|
||||
charWidth = Math.ceil(term.element.offsetWidth / cols);
|
||||
charHeight = Math.ceil(term.element.offsetHeight / rows);
|
||||
|
||||
res.text().then(function (pid) {
|
||||
window.pid = pid;
|
||||
socketURL += pid;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
res.text().then(function (pid) {
|
||||
window.pid = pid;
|
||||
socketURL += pid;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function runRealTerminal() {
|
||||
|
||||
@@ -14,9 +14,3 @@ h1 {
|
||||
margin: 0 auto;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#terminal-container .terminal {
|
||||
background-color: #111;
|
||||
color: #fafafa;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
@@ -168,6 +168,10 @@ namespace methods_core {
|
||||
t.setOption('bellStyle', 'visual');
|
||||
t.setOption('bellStyle', 'sound');
|
||||
t.setOption('bellStyle', 'both');
|
||||
t.setOption('fontSize', 1);
|
||||
t.setOption('lineHeight', 1);
|
||||
t.setOption('fontFamily', 'foo');
|
||||
t.setOption('theme', {background: '#ff0000'});
|
||||
}
|
||||
}
|
||||
namespace scrolling {
|
||||
@@ -210,9 +214,13 @@ namespace methods_experimental {
|
||||
t.registerLinkMatcher(/foo/, () => true, {
|
||||
matchIndex: 1,
|
||||
priority: 1,
|
||||
validationCallback: (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => {
|
||||
console.log(uri, element, callback);
|
||||
}
|
||||
validationCallback: (uri: string, callback: (isValid: boolean) => void) => {
|
||||
console.log(uri, callback);
|
||||
},
|
||||
tooltipCallback: (e: MouseEvent, uri: string) => {
|
||||
console.log(e, uri);
|
||||
},
|
||||
leaveCallback: () => {}
|
||||
});
|
||||
t.deregisterLinkMatcher(1);
|
||||
}
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@
|
||||
"nodemon": "1.10.2",
|
||||
"sorcery": "^0.10.0",
|
||||
"tslint": "^4.0.2",
|
||||
"typescript": "~2.2.0",
|
||||
"typescript": "~2.4.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
+12
-3
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@@ -6,8 +7,10 @@ import { ITerminal, IBuffer } from './Interfaces';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { LineData, CharData } from './Types';
|
||||
|
||||
export const CHAR_DATA_ATTR_INDEX = 0;
|
||||
export const CHAR_DATA_CHAR_INDEX = 1;
|
||||
export const CHAR_DATA_WIDTH_INDEX = 2;
|
||||
export const CHAR_DATA_CODE_INDEX = 3;
|
||||
|
||||
/**
|
||||
* This class represents a terminal buffer (an internal state of the terminal), where the
|
||||
@@ -32,8 +35,8 @@ export class Buffer implements IBuffer {
|
||||
/**
|
||||
* Create a new Buffer.
|
||||
* @param _terminal The terminal the Buffer will belong to.
|
||||
* @param _hasScrollback Whether the buffer should respecr the scrollback of
|
||||
* the terminal..
|
||||
* @param _hasScrollback Whether the buffer should respect the scrollback of
|
||||
* the terminal.
|
||||
*/
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
@@ -50,6 +53,12 @@ export class Buffer implements IBuffer {
|
||||
return this._hasScrollback && this.lines.maxLength > this._terminal.rows;
|
||||
}
|
||||
|
||||
public get isCursorInViewport(): boolean {
|
||||
const absoluteY = this.ybase + this.y;
|
||||
const relativeY = absoluteY - this.ydisp;
|
||||
return (relativeY >= 0 && relativeY < this._terminal.rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the correct buffer length based on the rows provided, the terminal's
|
||||
* scrollback and whether this buffer is flagged to have scrollback or not.
|
||||
@@ -106,7 +115,7 @@ export class Buffer implements IBuffer {
|
||||
if (this._lines.length > 0) {
|
||||
// Deal with columns increasing (we don't do anything when columns reduce)
|
||||
if (this._terminal.cols < newCols) {
|
||||
const ch: CharData = [this._terminal.defAttr, ' ', 1]; // does xterm use the default attr?
|
||||
const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr?
|
||||
for (let i = 0; i < this._lines.length; i++) {
|
||||
// TODO: This should be removed, with tests setup for the case that was
|
||||
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@@ -42,6 +43,16 @@ describe('CompositionHelper', () => {
|
||||
},
|
||||
handler: (text: string) => {
|
||||
handledText += text;
|
||||
},
|
||||
buffer: {
|
||||
isCursorInViewport: true
|
||||
},
|
||||
charMeasure: {
|
||||
height: 10,
|
||||
width: 10
|
||||
},
|
||||
options: {
|
||||
lineHeight: 1
|
||||
}
|
||||
};
|
||||
handledText = '';
|
||||
|
||||
+12
-10
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@@ -63,6 +64,7 @@ export class CompositionHelper {
|
||||
* @param {CompositionEvent} ev The event.
|
||||
*/
|
||||
public compositionupdate(ev: CompositionEvent): void {
|
||||
console.log('compositionupdate');
|
||||
this.compositionView.textContent = ev.data;
|
||||
this.updateCompositionElements();
|
||||
setTimeout(() => {
|
||||
@@ -193,26 +195,26 @@ export class CompositionHelper {
|
||||
if (!this.isComposing) {
|
||||
return;
|
||||
}
|
||||
const cursor = <HTMLElement>this.terminal.element.querySelector('.terminal-cursor');
|
||||
if (cursor) {
|
||||
// Take .xterm-rows offsetTop into account as well in case it's positioned absolutely within
|
||||
// the .xterm element.
|
||||
const xtermRows = <HTMLElement>this.terminal.element.querySelector('.xterm-rows');
|
||||
const cursorTop = xtermRows.offsetTop + cursor.offsetTop;
|
||||
|
||||
this.compositionView.style.left = cursor.offsetLeft + 'px';
|
||||
if (this.terminal.buffer.isCursorInViewport) {
|
||||
const cellHeight = Math.ceil(this.terminal.charMeasure.height * this.terminal.options.lineHeight);
|
||||
const cursorTop = this.terminal.buffer.y * cellHeight;
|
||||
const cursorLeft = this.terminal.buffer.x * this.terminal.charMeasure.width;
|
||||
|
||||
this.compositionView.style.left = cursorLeft + 'px';
|
||||
this.compositionView.style.top = cursorTop + 'px';
|
||||
this.compositionView.style.height = cursor.offsetHeight + 'px';
|
||||
this.compositionView.style.lineHeight = cursor.offsetHeight + 'px';
|
||||
this.compositionView.style.height = cellHeight + 'px';
|
||||
this.compositionView.style.lineHeight = cellHeight + 'px';
|
||||
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||
// text is.
|
||||
const compositionViewBounds = this.compositionView.getBoundingClientRect();
|
||||
this.textarea.style.left = cursor.offsetLeft + 'px';
|
||||
this.textarea.style.left = cursorLeft + 'px';
|
||||
this.textarea.style.top = cursorTop + 'px';
|
||||
this.textarea.style.width = compositionViewBounds.width + 'px';
|
||||
this.textarea.style.height = compositionViewBounds.height + 'px';
|
||||
this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
|
||||
}
|
||||
|
||||
if (!dontRecurse) {
|
||||
setTimeout(() => this.updateCompositionElements(true), 0);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
+14
-11
@@ -1,4 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@@ -36,13 +38,14 @@ export class InputHandler implements IInputHandler {
|
||||
// dont overflow left
|
||||
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) {
|
||||
if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
|
||||
|
||||
// found empty cell after fullwidth, need to go 2 cells back
|
||||
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2])
|
||||
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) {
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
|
||||
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][3] = char.charCodeAt(0);
|
||||
}
|
||||
} else {
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][3] = char.charCodeAt(0);
|
||||
}
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
}
|
||||
@@ -81,21 +84,21 @@ export class InputHandler implements IInputHandler {
|
||||
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
|
||||
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2]
|
||||
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)];
|
||||
}
|
||||
|
||||
// insert empty cell at cursor
|
||||
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1]);
|
||||
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]);
|
||||
}
|
||||
}
|
||||
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width];
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width, char.charCodeAt(0)];
|
||||
this._terminal.buffer.x++;
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
|
||||
// fullwidth char - set next cell width to zero and advance cursor
|
||||
if (ch_width === 2) {
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0];
|
||||
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined];
|
||||
this._terminal.buffer.x++;
|
||||
}
|
||||
}
|
||||
@@ -188,7 +191,7 @@ export class InputHandler implements IInputHandler {
|
||||
|
||||
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
let j = this._terminal.buffer.x;
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
|
||||
|
||||
while (param-- && j < this._terminal.cols) {
|
||||
this._terminal.buffer.lines.get(row).splice(j++, 0, ch);
|
||||
@@ -487,7 +490,7 @@ export class InputHandler implements IInputHandler {
|
||||
}
|
||||
|
||||
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
|
||||
|
||||
while (param--) {
|
||||
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1);
|
||||
@@ -535,7 +538,7 @@ export class InputHandler implements IInputHandler {
|
||||
|
||||
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
|
||||
let j = this._terminal.buffer.x;
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
|
||||
|
||||
while (param-- && j < this._terminal.cols) {
|
||||
this._terminal.buffer.lines.get(row)[j++] = ch;
|
||||
@@ -589,7 +592,7 @@ export class InputHandler implements IInputHandler {
|
||||
public repeatPrecedingCharacter(params: number[]): void {
|
||||
let param = params[0] || 1;
|
||||
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y);
|
||||
const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1];
|
||||
const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32];
|
||||
|
||||
while (param--) {
|
||||
line[this._terminal.buffer.x++] = ch;
|
||||
|
||||
+60
-10
@@ -1,9 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkMatcherOptions } from './Interfaces';
|
||||
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
|
||||
import { IColorSet } from './renderer/Interfaces';
|
||||
import { IMouseZoneManager } from './input/Interfaces';
|
||||
|
||||
export interface IBrowser {
|
||||
isNode: boolean;
|
||||
@@ -17,10 +20,19 @@ export interface IBrowser {
|
||||
isMSWindows: boolean;
|
||||
}
|
||||
|
||||
export interface ITerminal extends IEventEmitter {
|
||||
export interface IBufferAccessor {
|
||||
buffer: IBuffer;
|
||||
}
|
||||
|
||||
export interface IElementAccessor {
|
||||
element: HTMLElement;
|
||||
rowContainer: HTMLElement;
|
||||
selectionContainer: HTMLElement;
|
||||
}
|
||||
|
||||
export interface ILinkifierAccessor {
|
||||
linkifier: ILinkifier;
|
||||
}
|
||||
|
||||
export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElementAccessor, IEventEmitter {
|
||||
selectionManager: ISelectionManager;
|
||||
charMeasure: ICharMeasure;
|
||||
textarea: HTMLTextAreaElement;
|
||||
@@ -28,13 +40,12 @@ export interface ITerminal extends IEventEmitter {
|
||||
cols: number;
|
||||
browser: IBrowser;
|
||||
writeBuffer: string[];
|
||||
children: HTMLElement[];
|
||||
cursorHidden: boolean;
|
||||
cursorState: number;
|
||||
defAttr: number;
|
||||
options: ITerminalOptions;
|
||||
buffers: IBufferSet;
|
||||
buffer: IBuffer;
|
||||
isFocused: boolean;
|
||||
|
||||
/**
|
||||
* Emit the 'data' event and populate the given data.
|
||||
@@ -47,6 +58,7 @@ export interface ITerminal extends IEventEmitter {
|
||||
reset(): void;
|
||||
showCursor(): void;
|
||||
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
|
||||
refresh(start: number, end: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,20 +127,23 @@ export interface ITerminalOptions {
|
||||
bellSound?: string;
|
||||
bellStyle?: string;
|
||||
cancelEvents?: boolean;
|
||||
colors?: string[];
|
||||
cols?: number;
|
||||
convertEol?: boolean;
|
||||
cursorBlink?: boolean;
|
||||
cursorStyle?: string;
|
||||
debug?: boolean;
|
||||
disableStdin?: boolean;
|
||||
fontSize?: number;
|
||||
fontFamily?: string;
|
||||
geometry?: [number, number];
|
||||
handler?: (data: string) => void;
|
||||
lineHeight?: number;
|
||||
rows?: number;
|
||||
screenKeys?: boolean;
|
||||
scrollback?: number;
|
||||
tabStopWidth?: number;
|
||||
termName?: string;
|
||||
theme?: ITheme;
|
||||
useFlowControl?: boolean;
|
||||
}
|
||||
|
||||
@@ -143,6 +158,7 @@ export interface IBuffer {
|
||||
scrollTop: number;
|
||||
savedY: number;
|
||||
savedX: number;
|
||||
isCursorInViewport: boolean;
|
||||
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
|
||||
nextStop(x?: number): number;
|
||||
prevStop(x?: number): number;
|
||||
@@ -162,6 +178,7 @@ export interface IViewport {
|
||||
onWheel(ev: WheelEvent): void;
|
||||
onTouchStart(ev: TouchEvent): void;
|
||||
onTouchMove(ev: TouchEvent): void;
|
||||
onThemeChanged(colors: IColorSet): void;
|
||||
}
|
||||
|
||||
export interface ISelectionManager {
|
||||
@@ -186,12 +203,14 @@ export interface ICompositionHelper {
|
||||
export interface ICharMeasure {
|
||||
width: number;
|
||||
height: number;
|
||||
measure(): void;
|
||||
measure(options: ITerminalOptions): void;
|
||||
}
|
||||
|
||||
export interface ILinkifier {
|
||||
linkifyRow(rowIndex: number): void;
|
||||
attachHypertextLinkHandler(handler: LinkMatcherHandler): void;
|
||||
export interface ILinkifier extends IEventEmitter {
|
||||
attachToDom(mouseZoneManager: IMouseZoneManager): void;
|
||||
linkifyRows(start: number, end: number): void;
|
||||
setHypertextLinkHandler(handler: LinkMatcherHandler): void;
|
||||
setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
|
||||
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): boolean;
|
||||
}
|
||||
@@ -232,6 +251,14 @@ export interface ILinkMatcherOptions {
|
||||
* false if invalid.
|
||||
*/
|
||||
validationCallback?: LinkMatcherValidationCallback;
|
||||
/**
|
||||
* A callback that fires when the mouse hovers over a link.
|
||||
*/
|
||||
tooltipCallback?: LinkMatcherHandler;
|
||||
/**
|
||||
* A callback that fires when the mouse leaves a link that was hovered.
|
||||
*/
|
||||
leaveCallback?: () => void;
|
||||
/**
|
||||
* 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
|
||||
@@ -291,3 +318,26 @@ export interface IInputHandler {
|
||||
/** CSI s */ saveCursor(params?: number[]): void;
|
||||
/** CSI u */ restoreCursor(params?: number[]): void;
|
||||
}
|
||||
|
||||
export interface ITheme {
|
||||
foreground?: string;
|
||||
background?: string;
|
||||
cursor?: string;
|
||||
selection?: string;
|
||||
black?: string;
|
||||
red?: string;
|
||||
green?: string;
|
||||
yellow?: string;
|
||||
blue?: string;
|
||||
magenta?: string;
|
||||
cyan?: string;
|
||||
white?: string;
|
||||
brightBlack?: string;
|
||||
brightRed?: string;
|
||||
brightGreen?: string;
|
||||
brightYellow?: string;
|
||||
brightBlue?: string;
|
||||
brightMagenta?: string;
|
||||
brightCyan?: string;
|
||||
brightWhite?: string;
|
||||
}
|
||||
|
||||
+95
-79
@@ -1,43 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal, ILinkifier } from './Interfaces';
|
||||
import { ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { LinkMatcher } from './Types';
|
||||
import { LinkMatcher, LineData } from './Types';
|
||||
import { IMouseZoneManager, IMouseZone } from './input/Interfaces';
|
||||
import { MockBuffer } from './utils/TestUtils.test';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
|
||||
class TestLinkifier extends Linkifier {
|
||||
constructor() {
|
||||
constructor(_terminal: IBufferAccessor & IElementAccessor) {
|
||||
super(_terminal);
|
||||
Linkifier.TIME_BEFORE_LINKIFY = 0;
|
||||
super();
|
||||
}
|
||||
|
||||
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
|
||||
public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); }
|
||||
}
|
||||
|
||||
class TestMouseZoneManager implements IMouseZoneManager {
|
||||
public clears: number = 0;
|
||||
public zones: IMouseZone[] = [];
|
||||
add(zone: IMouseZone): void {
|
||||
this.zones.push(zone);
|
||||
}
|
||||
clearAll(): void {
|
||||
this.clears++;
|
||||
}
|
||||
}
|
||||
|
||||
describe('Linkifier', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let container: HTMLElement;
|
||||
let rows: HTMLElement[];
|
||||
let terminal: IBufferAccessor & IElementAccessor;
|
||||
let linkifier: TestLinkifier;
|
||||
let mouseZoneManager: TestMouseZoneManager;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
linkifier = new TestLinkifier();
|
||||
terminal = {
|
||||
buffer: new MockBuffer(),
|
||||
element: <HTMLElement>{}
|
||||
};
|
||||
terminal.buffer.lines = new CircularList<LineData>(20);
|
||||
terminal.buffer.ydisp = 0;
|
||||
linkifier = new TestLinkifier(terminal);
|
||||
mouseZoneManager = new TestMouseZoneManager();
|
||||
});
|
||||
|
||||
function addRow(html: string): void {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = html;
|
||||
container.appendChild(element);
|
||||
rows.push(element);
|
||||
function stringToRow(text: string): LineData {
|
||||
let result: LineData = [];
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function addRow(text: string): void {
|
||||
terminal.buffer.lines.push(stringToRow(text));
|
||||
}
|
||||
|
||||
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
|
||||
addRow(uri);
|
||||
linkifier.linkifyRows();
|
||||
setTimeout(() => {
|
||||
assert.equal(mouseZoneManager.zones[0].x1, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1);
|
||||
assert.equal(mouseZoneManager.zones[0].y, terminal.buffer.lines.length);
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
||||
addRow(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
linkifier.linkifyRows();
|
||||
// Allow linkify to happen
|
||||
setTimeout(() => {
|
||||
assert.equal(mouseZoneManager.zones.length, links.length);
|
||||
links.forEach((l, i) => {
|
||||
assert.equal(mouseZoneManager.zones[i].x1, l.x + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].y, terminal.buffer.lines.length);
|
||||
});
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
describe('before attachToDom', () => {
|
||||
@@ -52,78 +99,42 @@ describe('Linkifier', () => {
|
||||
|
||||
describe('after attachToDom', () => {
|
||||
beforeEach(() => {
|
||||
rows = [];
|
||||
linkifier.attachToDom(document, rows);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
linkifier.attachToDom(mouseZoneManager);
|
||||
});
|
||||
|
||||
function clickElement(element: Node): void {
|
||||
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): void {
|
||||
addRow(uri);
|
||||
linkifier.linkifyRow(0);
|
||||
setTimeout(() => {
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
describe('http links', () => {
|
||||
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
|
||||
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));
|
||||
it('should allow ~ character in URI path', (done) => {
|
||||
assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('link matcher', () => {
|
||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone): void {
|
||||
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);
|
||||
assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done);
|
||||
});
|
||||
it('should match a single link at the start of a text node', done => {
|
||||
assertLinkifiesRow('foo bar', /foo/, '<a>foo</a> bar', done);
|
||||
assertLinkifiesRow('foo bar', /foo/, [{x: 0, length: 3}], 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);
|
||||
assertLinkifiesRow('foo bar baz', /bar/, [{x: 4, length: 3}], done);
|
||||
});
|
||||
it('should match a single link at the end of a text node', done => {
|
||||
assertLinkifiesRow('foo bar', /bar/, 'foo <a>bar</a>', done);
|
||||
assertLinkifiesRow('foo bar', /bar/, [{x: 4, length: 3}], 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);
|
||||
assertLinkifiesRow('foo bar', /foo|bar/, [{x: 0, length: 3}, {x: 4, length: 3}], 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);
|
||||
assertLinkifiesRow('foo bar baz', /bar|baz/, [{x: 4, length: 3}, {x: 8, length: 3}], 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);
|
||||
assertLinkifiesRow('foo barbaz', /bar|baz/, [{x: 4, length: 3}, {x: 7, length: 3}], done);
|
||||
});
|
||||
it('should not duplicate text after a unicode character (wrapped in a span)', done => {
|
||||
// This is a regression test for an issue that came about when using
|
||||
// an oh-my-zsh theme that added the large blue diamond unicode
|
||||
// character (U+1F537) which caused the path to be duplicated. See #642.
|
||||
assertLinkifiesRow('echo \'<span class="xterm-normal-char">🔷</span>foo\'', /foo/, 'echo \'<span class="xterm-normal-char">🔷</span><a>foo</a>\'', done);
|
||||
assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,26 +142,31 @@ describe('Linkifier', () => {
|
||||
it('should enable link if true', done => {
|
||||
addRow('test');
|
||||
linkifier.registerLinkMatcher(/test/, () => done(), {
|
||||
validationCallback: (url, element, cb) => {
|
||||
validationCallback: (url, cb) => {
|
||||
assert.equal(mouseZoneManager.zones.length, 0);
|
||||
cb(true);
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||
setTimeout(() => clickElement(rows[0].firstChild), 0);
|
||||
assert.equal(mouseZoneManager.zones.length, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x1, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x2, 5);
|
||||
assert.equal(mouseZoneManager.zones[0].y, 1);
|
||||
// Fires done()
|
||||
mouseZoneManager.zones[0].clickCallback(<any>{});
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRow(0);
|
||||
linkifier.linkifyRows();
|
||||
});
|
||||
|
||||
it('should disable link if false', done => {
|
||||
addRow('test');
|
||||
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
||||
validationCallback: (url, element, cb) => {
|
||||
validationCallback: (url, cb) => {
|
||||
assert.equal(mouseZoneManager.zones.length, 0);
|
||||
cb(false);
|
||||
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
|
||||
setTimeout(() => clickElement(rows[0].firstChild), 0);
|
||||
assert.equal(mouseZoneManager.zones.length, 0);
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRow(0);
|
||||
// Allow time for the click to be performed
|
||||
linkifier.linkifyRows();
|
||||
// Allow time for the validation callback to be performed
|
||||
setTimeout(() => done(), 10);
|
||||
});
|
||||
|
||||
@@ -158,7 +174,7 @@ describe('Linkifier', () => {
|
||||
addRow('test test');
|
||||
let count = 0;
|
||||
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
||||
validationCallback: (url, element, cb) => {
|
||||
validationCallback: (url, cb) => {
|
||||
count += 1;
|
||||
if (count === 2) {
|
||||
done();
|
||||
@@ -166,7 +182,7 @@ describe('Linkifier', () => {
|
||||
cb(false);
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRow(0);
|
||||
linkifier.linkifyRows();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+124
-205
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkMatcherOptions } from './Interfaces';
|
||||
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback } from './Types';
|
||||
|
||||
const INVALID_LINK_CLASS = 'xterm-invalid-link';
|
||||
import { ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces';
|
||||
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEvent, LinkHoverEventTypes } from './Types';
|
||||
import { IMouseZoneManager } from './input/Interfaces';
|
||||
import { MouseZone } from './input/MouseZoneManager';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
|
||||
const protocolClause = '(https?:\\/\\/)';
|
||||
const domainCharacterSet = '[\\da-z\\.-]+';
|
||||
@@ -34,7 +36,7 @@ const HYPERTEXT_LINK_MATCHER_ID = 0;
|
||||
/**
|
||||
* The Linkifier applies links to rows shortly after they have been refreshed.
|
||||
*/
|
||||
export class Linkifier {
|
||||
export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
/**
|
||||
* The time to wait after a row is changed before it is linkified. This prevents
|
||||
* the costly operation of searching every row multiple times, potentially a
|
||||
@@ -42,51 +44,63 @@ export class Linkifier {
|
||||
*/
|
||||
protected static TIME_BEFORE_LINKIFY = 200;
|
||||
|
||||
protected _linkMatchers: LinkMatcher[];
|
||||
protected _linkMatchers: LinkMatcher[] = [];
|
||||
|
||||
private _document: Document;
|
||||
private _rows: HTMLElement[];
|
||||
private _rowTimeoutIds: number[];
|
||||
private _mouseZoneManager: IMouseZoneManager;
|
||||
private _rowsTimeoutId: number;
|
||||
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
|
||||
|
||||
constructor() {
|
||||
this._rowTimeoutIds = [];
|
||||
this._linkMatchers = [];
|
||||
constructor(
|
||||
protected _terminal: IBufferAccessor & IElementAccessor
|
||||
) {
|
||||
super();
|
||||
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the linkifier to the DOM, enabling linkification.
|
||||
* @param document The document object.
|
||||
* @param rows The array of rows to apply links to.
|
||||
* @param mouseZoneManager The mouse zone manager to register link zones with.
|
||||
*/
|
||||
public attachToDom(document: Document, rows: HTMLElement[]): void {
|
||||
this._document = document;
|
||||
this._rows = rows;
|
||||
public attachToDom(mouseZoneManager: IMouseZoneManager): void {
|
||||
this._mouseZoneManager = mouseZoneManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a row for linkification.
|
||||
* @param {number} rowIndex The index of the row to linkify.
|
||||
* Queue linkification on a set of rows.
|
||||
* @param start The row to linkify from (inclusive).
|
||||
* @param end The row to linkify to (inclusive).
|
||||
*/
|
||||
public linkifyRow(rowIndex: number): void {
|
||||
public linkifyRows(start: number, end: number): void {
|
||||
// Don't attempt linkify if not yet attached to DOM
|
||||
if (!this._document) {
|
||||
if (!this._mouseZoneManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = this._rowTimeoutIds[rowIndex];
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
// Clear out any existing links
|
||||
this._mouseZoneManager.clearAll();
|
||||
|
||||
if (this._rowsTimeoutId) {
|
||||
clearTimeout(this._rowsTimeoutId);
|
||||
}
|
||||
this._rowTimeoutIds[rowIndex] = setTimeout(this._linkifyRow.bind(this, rowIndex), Linkifier.TIME_BEFORE_LINKIFY);
|
||||
this._rowsTimeoutId = setTimeout(this._linkifyRows.bind(this, start, end), 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.
|
||||
* Linkifies
|
||||
* @param start The row to start at.
|
||||
* @param end The row to end at.
|
||||
*/
|
||||
private _linkifyRows(start: number, end: number): void {
|
||||
this._rowsTimeoutId = null;
|
||||
for (let i = start; i <= end; i++) {
|
||||
this._linkifyRow(i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a handler for hypertext links, overriding default <a> behavior for
|
||||
* tandard http(s) links.
|
||||
* @param handler The handler to use, this can be cleared with null.
|
||||
*/
|
||||
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
|
||||
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
|
||||
@@ -94,8 +108,7 @@ export class Linkifier {
|
||||
|
||||
/**
|
||||
* Attaches a validation callback for hypertext links.
|
||||
* @param {LinkMatcherValidationCallback} callback The callback to use, this
|
||||
* can be cleared with null.
|
||||
* @param callback The callback to use, this can be cleared with null.
|
||||
*/
|
||||
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
|
||||
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback;
|
||||
@@ -104,12 +117,12 @@ export class Linkifier {
|
||||
/**
|
||||
* 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 {ILinkMatcherOptions} [options] Options for the link matcher.
|
||||
* @return {number} The ID of the new matcher, this can be used to deregister.
|
||||
* @param 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 handler The callback when the link is called.
|
||||
* @param options Options for the link matcher.
|
||||
* @return The ID of the new matcher, this can be used to deregister.
|
||||
*/
|
||||
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number {
|
||||
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
|
||||
@@ -121,6 +134,8 @@ export class Linkifier {
|
||||
handler,
|
||||
matchIndex: options.matchIndex,
|
||||
validationCallback: options.validationCallback,
|
||||
hoverTooltipCallback: options.tooltipCallback,
|
||||
hoverLeaveCallback: options.leaveCallback,
|
||||
priority: options.priority || 0
|
||||
};
|
||||
this._addLinkMatcherToList(matcher);
|
||||
@@ -151,8 +166,8 @@ export class Linkifier {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param matcherId The link matcher's ID (returned after register)
|
||||
* @return Whether a link matcher was found and deregistered.
|
||||
*/
|
||||
public deregisterLinkMatcher(matcherId: number): boolean {
|
||||
// ID 0 is the hypertext link matcher which cannot be deregistered
|
||||
@@ -167,197 +182,101 @@ export class Linkifier {
|
||||
|
||||
/**
|
||||
* Linkifies a row.
|
||||
* @param {number} rowIndex The index of the row to linkify.
|
||||
* @param rowIndex The index of the row to linkify.
|
||||
*/
|
||||
private _linkifyRow(rowIndex: number): void {
|
||||
const row = this._rows[rowIndex];
|
||||
if (!row) {
|
||||
const absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
|
||||
if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
|
||||
return;
|
||||
}
|
||||
const text = row.textContent;
|
||||
const text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
|
||||
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||
const matcher = this._linkMatchers[i];
|
||||
const linkElements = this._doLinkifyRow(row, matcher);
|
||||
if (linkElements.length > 0) {
|
||||
// Fire validation callback
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Only allow a single LinkMatcher to trigger on any given row.
|
||||
return;
|
||||
}
|
||||
this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies a row given a specific handler.
|
||||
* @param {HTMLElement} row The row to linkify.
|
||||
* @param {LinkMatcher} matcher The link matcher for this line.
|
||||
* @param rowIndex The row index to linkify.
|
||||
* @param text The text of the row (excludes text in the row that's already
|
||||
* linkified).
|
||||
* @param matcher The link matcher for this line.
|
||||
* @param offset The how much of the row has already been linkified.
|
||||
* @return The link element(s) that were added.
|
||||
*/
|
||||
private _doLinkifyRow(row: HTMLElement, matcher: LinkMatcher): HTMLElement[] {
|
||||
private _doLinkifyRow(rowIndex: number, text: string, matcher: LinkMatcher, offset: number = 0): void {
|
||||
// Iterate over nodes as we want to consider text nodes
|
||||
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);
|
||||
let match = text.match(matcher.regex);
|
||||
if (!match || match.length === 0) {
|
||||
return result;
|
||||
return;
|
||||
}
|
||||
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++) {
|
||||
const node = nodes[i];
|
||||
const searchIndex = node.textContent.indexOf(uri);
|
||||
if (searchIndex >= 0) {
|
||||
const linkElement = this._createAnchorElement(uri, matcher.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 result;
|
||||
}
|
||||
element.innerHTML = '';
|
||||
element.appendChild(linkElement);
|
||||
}
|
||||
} else if (node.childNodes.length > 1) {
|
||||
// Matches part of string in an element with multiple child nodes
|
||||
for (let j = 0; j < node.childNodes.length; j++) {
|
||||
const childNode = node.childNodes[j];
|
||||
const childSearchIndex = childNode.textContent.indexOf(uri);
|
||||
if (childSearchIndex !== -1) {
|
||||
// Match found in currentNode
|
||||
this._replaceNodeSubstringWithNode(childNode, linkElement, uri, childSearchIndex);
|
||||
// Don't need to count nodesAdded by replacing the node as this
|
||||
// is a child node, not a top-level node.
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Matches part of string in a single text node
|
||||
const nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex);
|
||||
// No need to consider the new nodes
|
||||
i += nodesAdded;
|
||||
// Get index, match.index is for the outer match which includes negated chars
|
||||
const index = text.indexOf(uri);
|
||||
|
||||
// Ensure the link is valid before registering
|
||||
if (matcher.validationCallback) {
|
||||
matcher.validationCallback(text, isValid => {
|
||||
// Discard link if the line has already changed
|
||||
if (this._rowsTimeoutId) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
element.draggable = false;
|
||||
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);
|
||||
if (isValid) {
|
||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
||||
}
|
||||
});
|
||||
} 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;
|
||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
||||
}
|
||||
|
||||
// Recursively check for links in the rest of the text
|
||||
const remainingStartIndex = index + uri.length;
|
||||
const remainingText = text.substr(remainingStartIndex);
|
||||
if (remainingText.length > 0) {
|
||||
this._doLinkifyRow(rowIndex, remainingText, matcher, offset + remainingStartIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a link to the mouse zone manager.
|
||||
* @param x The column the link starts.
|
||||
* @param y The row the link is on.
|
||||
* @param uri The URI of the link.
|
||||
* @param matcher The link matcher for the link.
|
||||
*/
|
||||
private _addLink(x: number, y: number, uri: string, matcher: LinkMatcher): void {
|
||||
this._mouseZoneManager.add(new MouseZone(
|
||||
x + 1,
|
||||
x + 1 + uri.length,
|
||||
y + 1,
|
||||
e => {
|
||||
if (matcher.handler) {
|
||||
return matcher.handler(e, uri);
|
||||
}
|
||||
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.
|
||||
* @return The number of nodes to skip when searching for the next uri.
|
||||
*/
|
||||
private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): number {
|
||||
// If the targetNode is a non-text node with a single child, make the child
|
||||
// the new targetNode.
|
||||
if (targetNode.childNodes.length === 1) {
|
||||
targetNode = targetNode.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 (targetNode.nodeType !== 3/*Node.TEXT_NODE*/) {
|
||||
throw new Error('targetNode must be a text node or only contain a single text node');
|
||||
}
|
||||
|
||||
const fullText = targetNode.textContent;
|
||||
|
||||
if (substringIndex === 0) {
|
||||
// Replace with <newNode><textnode>
|
||||
const rightText = fullText.substring(substring.length);
|
||||
const rightTextNode = this._document.createTextNode(rightText);
|
||||
this._replaceNode(targetNode, newNode, rightTextNode);
|
||||
return 0;
|
||||
}
|
||||
|
||||
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(targetNode, leftTextNode, newNode);
|
||||
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(targetNode, leftTextNode, newNode, rightTextNode);
|
||||
return 1;
|
||||
window.open(uri, '_blank');
|
||||
},
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.HOVER, <LinkHoverEvent>{ x, y, length: uri.length});
|
||||
this._terminal.element.style.cursor = 'pointer';
|
||||
},
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.TOOLTIP, <LinkHoverEvent>{ x, y, length: uri.length});
|
||||
if (matcher.hoverTooltipCallback) {
|
||||
matcher.hoverTooltipCallback(e, uri);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.emit(LinkHoverEventTypes.LEAVE, <LinkHoverEvent>{ x, y, length: uri.length});
|
||||
this._terminal.element.style.cursor = '';
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user