mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #623 from Tyriar/612_multiple_links_in_row
Support multiple link matches in a single row
This commit is contained in:
+65
-7
@@ -33,6 +33,13 @@ describe('Linkifier', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function addRow(html: string) {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = html;
|
||||
container.appendChild(element);
|
||||
rows.push(element);
|
||||
}
|
||||
|
||||
describe('before attachToDom', () => {
|
||||
it('should allow link matcher registration', done => {
|
||||
assert.doesNotThrow(() => {
|
||||
@@ -51,13 +58,6 @@ describe('Linkifier', () => {
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
function addRow(text: string) {
|
||||
const element = document.createElement('div');
|
||||
element.textContent = text;
|
||||
container.appendChild(element);
|
||||
rows.push(element);
|
||||
}
|
||||
|
||||
function clickElement(element: Node) {
|
||||
const event = document.createEvent('MouseEvent');
|
||||
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||
@@ -75,9 +75,52 @@ describe('Linkifier', () => {
|
||||
}
|
||||
|
||||
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('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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validationCallback', () => {
|
||||
it('should enable link if true', done => {
|
||||
addRow('test');
|
||||
@@ -104,6 +147,21 @@ describe('Linkifier', () => {
|
||||
// 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, cb) => {
|
||||
count += 1;
|
||||
if (count === 2) {
|
||||
done();
|
||||
}
|
||||
cb(false);
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRow(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('priority', () => {
|
||||
|
||||
+57
-43
@@ -168,16 +168,17 @@ export class Linkifier {
|
||||
const text = row.textContent;
|
||||
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||
const matcher = this._linkMatchers[i];
|
||||
const uri = this._findLinkMatch(text, matcher.regex, matcher.matchIndex);
|
||||
if (uri) {
|
||||
const linkElement = this._doLinkifyRow(rowIndex, uri, matcher.handler, matcher.id === HYPERTEXT_LINK_MATCHER_ID);
|
||||
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++) {
|
||||
matcher.validationCallback(linkElements[j].textContent, isValid => {
|
||||
if (!isValid) {
|
||||
linkElements[j].classList.add(INVALID_LINK_CLASS);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Only allow a single LinkMatcher to trigger on any given row.
|
||||
return;
|
||||
@@ -187,54 +188,61 @@ export class Linkifier {
|
||||
|
||||
/**
|
||||
* 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++) {
|
||||
const node = nodes[i];
|
||||
const searchIndex = node.textContent.indexOf(uri);
|
||||
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*/) {
|
||||
this._replaceNode(node, linkElement);
|
||||
} else {
|
||||
const element = (<HTMLElement>node);
|
||||
if (element.nodeName === 'A') {
|
||||
// This row has already been linkified
|
||||
return;
|
||||
return result;
|
||||
}
|
||||
element.innerHTML = '';
|
||||
element.appendChild(linkElement);
|
||||
}
|
||||
} else {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,8 +295,9 @@ export class Linkifier {
|
||||
* @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): void {
|
||||
private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): number {
|
||||
let node = targetNode;
|
||||
if (node.nodeType !== 3/*Node.TEXT_NODE*/) {
|
||||
node = node.childNodes[0];
|
||||
@@ -297,7 +306,7 @@ export class Linkifier {
|
||||
// The targetNode will be either a text node or a <span>. The text node
|
||||
// (targetNode or its only-child) needs to be replaced with newNode plus new
|
||||
// text nodes potentially on either side.
|
||||
if (node.childNodes.length === 0 && node.nodeType !== Node.TEXT_NODE) {
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -308,18 +317,23 @@ export class Linkifier {
|
||||
const rightText = fullText.substring(substring.length);
|
||||
const rightTextNode = this._document.createTextNode(rightText);
|
||||
this._replaceNode(node, newNode, rightTextNode);
|
||||
} else if (substringIndex === targetNode.textContent.length - substring.length) {
|
||||
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(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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -21,7 +21,8 @@ describe('xterm.js', function() {
|
||||
};
|
||||
xterm.element = {
|
||||
classList: {
|
||||
toggle: function(){}
|
||||
toggle: function(){},
|
||||
remove: function(){}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user