glowing-bear/js/irc-utils.js

240 lines
7.5 KiB
JavaScript

/**
* Portable utilities for IRC.
*/
(function() {
'use strict';
var IrcUtils = angular.module('IrcUtils', []);
IrcUtils.service('IrcUtils', [function() {
/**
* Escape a string for usage in a larger regexp
* @param str String to escape
* @return Escaped string
*/
var escapeRegExp = function(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
};
/**
* Get a new version of a nick list, sorted by last speaker
*
* @param nickList Original nick list
* @return Sorted nick list
*/
var _ciNickList = function(nickList) {
var newList = _(nickList).sortBy(function(nickObj) {
return -nickObj.spokeAt;
});
newList = _(newList).pluck('name');
return newList;
};
/**
* Completes a single nick.
*
* @param candidate What to search for
* @param nickList Array of current nicks sorted for case insensitive searching
* @return Completed nick (null if not found)
*/
var _completeSingleNick = function(candidate, nickList) {
var foundNick = null;
nickList.some(function(nick) {
if (nick.toLowerCase().indexOf(candidate.toLowerCase()) === 0) {
// found!
foundNick = nick;
return true;
}
return false;
});
return foundNick;
};
/**
* Get the next nick when iterating nicks.
*
* @param iterCandidate First characters to look at
* @param currentNick Current selected nick
* @param nickList Array of current nicks sorted for case insensitive searching
* @return Next nick (may be the same)
*/
var _nextNick = function(iterCandidate, currentNick, nickList) {
var matchingNicks = [];
var at = null;
var lcIterCandidate = iterCandidate.toLowerCase();
var lcCurrentNick = currentNick.toLowerCase();
// collect matching nicks
for (var i = 0; i < nickList.length; ++i) {
var lcNick = nickList[i].toLowerCase();
if (lcNick.indexOf(lcIterCandidate) === 0) {
matchingNicks.push(nickList[i]);
if (lcCurrentNick === lcNick) {
at = matchingNicks.length - 1;
}
}
/* Since we aren't sorted any more torhve disabled this:
else if (matchingNicks.length > 0) {
// end of group, no need to check after this
//break;
}
*/
}
if (at === null || matchingNicks.length === 0) {
return currentNick;
} else {
++at;
if (at === matchingNicks.length) {
// cycle
at = 0;
}
return matchingNicks[at];
}
};
/**
* Nicks tab completion.
*
* @param text Plain text (no colors)
* @param caretPos Current caret position (0 means before the first character)
* @param iterCandidate Current iteration candidate (null if not iterating)
* @param nickList Array of current nicks
* @param suf Custom suffix (at least one character, escaped for regex)
* @param addSpace Whether to add a space after nick completion in the middle
* @return Object with following properties:
* text: new complete replacement text
* caretPos: new caret position within new text
* foundNick: completed nick (or null if not possible)
* iterCandidate: current iterating candidate
*/
var completeNick = function(text, caretPos, iterCandidate, nickList, suf, addSpace) {
var doIterate = (iterCandidate !== null);
if (suf === undefined) {
suf = ':';
}
// addSpace defaults to true
var addSpaceChar = (addSpace === undefined || addSpace === 'on') ? ' ' : '';
// new nick list to search in
var searchNickList = _ciNickList(nickList);
// text before and after caret
var beforeCaret = text.substring(0, caretPos);
var afterCaret = text.substring(caretPos);
// default: don't change anything
var ret = {
text: text,
caretPos: caretPos,
foundNick: null,
iterCandidate: null
};
// iterating nicks at the beginning?
var m = beforeCaret.match(new RegExp('^([a-zA-Z0-9_\\\\\\[\\]{}^`|-]+)' + suf + ' $'));
var newNick = null;
if (m) {
if (doIterate) {
// try iterating
newNick = _nextNick(iterCandidate, m[1], searchNickList);
if (suf.endsWith(' ')) {
beforeCaret = newNick + suf;
} else {
beforeCaret = newNick + suf + ' ';
}
return {
text: beforeCaret + afterCaret,
caretPos: beforeCaret.length,
foundNick: newNick,
iterCandidate: iterCandidate
};
} else {
// if not iterating, don't do anything
return ret;
}
}
// nick completion in the beginning?
m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/);
if (m) {
// try completing
newNick = _completeSingleNick(m[1], searchNickList);
if (newNick === null) {
// no match
return ret;
}
if (suf.endsWith(' ')) {
beforeCaret = newNick + suf;
} else {
beforeCaret = newNick + suf + ' ';
}
if (afterCaret[0] === ' ') {
// swallow first space after caret if any
afterCaret = afterCaret.substring(1);
}
return {
text: beforeCaret + afterCaret,
caretPos: beforeCaret.length,
foundNick: newNick,
iterCandidate: m[1]
};
}
// iterating nicks in the middle?
m = beforeCaret.match(/^(.* )([a-zA-Z0-9_\\\[\]{}^`|-]+) $/);
if (m) {
if (doIterate) {
// try iterating
newNick = _nextNick(iterCandidate, m[2], searchNickList);
beforeCaret = m[1] + newNick + addSpaceChar;
return {
text: beforeCaret + afterCaret,
caretPos: beforeCaret.length,
foundNick: newNick,
iterCandidate: iterCandidate
};
} else {
// if not iterating, don't do anything
return ret;
}
}
// nick completion elsewhere in the middle?
m = beforeCaret.match(/^(.* )([a-zA-Z0-9_\\\[\]{}^`|-]+)$/);
if (m) {
// try completing
newNick = _completeSingleNick(m[2], searchNickList);
if (newNick === null) {
// no match
return ret;
}
beforeCaret = m[1] + newNick + addSpaceChar;
if (afterCaret[0] === ' ') {
// swallow first space after caret if any
afterCaret = afterCaret.substring(1);
}
return {
text: beforeCaret + afterCaret,
caretPos: beforeCaret.length,
foundNick: newNick,
iterCandidate: m[2]
};
}
// completion not possible
return ret;
};
return {
'completeNick': completeNick
};
}]);
})();