diff --git a/index.html b/index.html
index 319e62b..df8a198 100644
--- a/index.html
+++ b/index.html
@@ -15,6 +15,7 @@
+
diff --git a/js/irc-utils.js b/js/irc-utils.js
new file mode 100644
index 0000000..7942c21
--- /dev/null
+++ b/js/irc-utils.js
@@ -0,0 +1,174 @@
+/**
+ * Portable utilities for IRC.
+ */
+
+var IrcUtils = {
+ /**
+ * Completes a single nick.
+ *
+ * @param candidate What to search for
+ * @param nickList Array of current nicks sorted alphabetically
+ * @return Completed nick (null if not found)
+ */
+ _completeSingleNick: function(candidate, nickList) {
+ var foundNick = null;
+
+ nickList.some(function(nick) {
+ if (nick.search(candidate) == 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 alphabetically
+ * @return Next nick (may be the same)
+ */
+ _nextNick: function(iterCandidate, currentNick, nickList) {
+ var firstInGroup = null;
+ var matchingNicks = [];
+ var at = null;
+
+ // collect matching nicks
+ for (var i = 0; i < nickList.length; ++i) {
+ if (nickList[i].search(iterCandidate) == 0) {
+ matchingNicks.push(nickList[i]);
+ if (currentNick == nickList[i]) {
+ at = matchingNicks.length - 1;
+ }
+ } 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 doIterate True to iterate the nicks if possible
+ * @param iterCandidate Current iteration candidate (null if not iterating)
+ * @param nickList Array of current nicks sorted alphabetically
+ * @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
+ */
+ completeNick: function(text, caretPos, doIterate, iterCandidate, 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(/^([a-zA-Z0-9_\\\[\]{}^`|-]+): $/);
+ if (m) {
+ if (doIterate) {
+ // try iterating
+ var newNick = IrcUtils._nextNick(iterCandidate, m[1], nickList);
+ beforeCaret = newNick + ': ';
+ 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
+ var newNick = IrcUtils._completeSingleNick(m[1], nickList);
+ if (newNick === null) {
+ // no match
+ return ret;
+ }
+ beforeCaret = newNick + ': ';
+ 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
+ var newNick = IrcUtils._nextNick(iterCandidate, m[2], nickList);
+ beforeCaret = m[1] + newNick + ' ';
+ 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
+ var newNick = IrcUtils._completeSingleNick(m[2], nickList);
+ if (newNick === null) {
+ // no match
+ return ret;
+ }
+ beforeCaret = m[1] + newNick + ' ';
+ return {
+ text: beforeCaret + afterCaret,
+ caretPos: beforeCaret.length,
+ foundNick: newNick,
+ iterCandidate: m[2]
+ };
+ }
+
+ // completion not possible
+ return ret;
+ }
+};