~emersion/gamja

7ce98e0e90442a04a93d8571ee654cea213f7741 — Simon Ser 3 years ago 22e54da case-mapping
wip: implement case-mapping
2 files changed, 133 insertions(+), 2 deletions(-)

M lib/client.js
M lib/irc.js
M lib/client.js => lib/client.js +20 -1
@@ 161,7 161,18 @@ export default class Client extends EventTarget {
			break;
		case irc.RPL_ISUPPORT:
			var tokens = msg.params.slice(1, -1);
			irc.parseISUPPORT(tokens, this.isupport);
			var changed = irc.parseISUPPORT(tokens, this.isupport);
			if (changed.indexOf("CASEMAPPING") >= 0) {
				this.setCaseMapping(this.isupport.get("CASEMAPPING"));
			}
			break;
		case irc.RPL_ENDOFMOTD:
		case irc.ERR_NOMOTD:
			// These messages are used to indicate the end of the ISUPPORT list
			if (!this.isupport.has("CASEMAPPING")) {
				// Server didn't send any CASEMAPPING token, assume RFC 1459
				this.setCaseMapping("rfc1459");
			}
			break;
		case "CAP":
			this.handleCap(msg);


@@ 358,6 369,14 @@ export default class Client extends EventTarget {
		console.log("Sent:", msg);
	}

	setCaseMapping(name) {
		this.cm = irc.CaseMapping.byName(name);
		if (!this.cm) {
			console.error("Unsupported case-mapping '" + name + "', falling back to RFC 1459");
			this.cm = irc.CaseMapping.RFC1459;
		}
	}

	/* Execute a command that expects a response. `done` is called with message
	 * events until it returns a truthy value. */
	roundtrip(msg, done) {

M lib/irc.js => lib/irc.js +113 -1
@@ 11,6 11,7 @@ export const RPL_TOPICWHOTIME = "333";
export const RPL_WHOREPLY = "352";
export const RPL_NAMREPLY = "353";
export const RPL_ENDOFNAMES = "366";
export const RPL_ENDOFMOTD = "376";
export const ERR_NOMOTD = "422";
export const ERR_ERRONEUSNICKNAME = "432";
export const ERR_NICKNAMEINUSE = "433";


@@ 320,6 321,7 @@ export function parseCTCP(msg) {
}

export function parseISUPPORT(tokens, params) {
	var changed = [];
	tokens.forEach((tok) => {
		if (tok.startsWith("-")) {
			var k = tok.slice(1);


@@ 333,6 335,116 @@ export function parseISUPPORT(tokens, params) {
			k = tok.slice(0, i);
			v = tok.slice(i + 1);
		}
		params.set(k.toUpperCase(), v);

		k = k.toUpperCase();

		params.set(k, v);
		changed.push(k);
	});
	return changed;
}

export const CaseMapping = {
	ASCII(str) {
		var out = "";
		for (var i = 0; i < out.length; i++) {
			var ch = str[i];
			if ("A" <= ch && ch <= "Z") {
				ch = ch.toLowerCase();
			}
			out += ch;
		}
		return out;
	}

	RFC1459(str) {
		var out = "";
		for (var i = 0; i < out.length; i++) {
			var ch = str[i];
			if ("A" <= ch && ch <= "Z") {
				ch = ch.toLowerCase();
			} else if (ch == "{") {
				ch = "[";
			} else if (ch == "}") {
				ch = "]";
			} else if (ch == "\\") {
				ch = "|";
			} else if (ch == "~") {
				ch = "^";
			}
			out += ch;
		}
		return out;
	}

	RFC1459Strict(str) {
		var out = "";
		for (var i = 0; i < out.length; i++) {
			var ch = str[i];
			if ("A" <= ch && ch <= "Z") {
				ch = ch.toLowerCase();
			} else if (ch == "{") {
				ch = "[";
			} else if (ch == "}") {
				ch = "]";
			} else if (ch == "\\") {
				ch = "|";
			}
			out += ch;
		}
		return out;
	}

	byName(name) {
		switch (name) {
		case "ascii":
			return CaseMapping.ASCII;
		case "rfc1459":
			return CaseMapping.RFC1459;
		case "rfc1459-strict":
			return CaseMapping.RFC1459Strict;
		}
	}
};

export class CaseMapMap {
	caseMap: null,
	map: new Map(),

	constructor(caseMap, iterable) {
		this.caseMap = caseMap;

		for (var [key, value] of iterable) {
			this.set(key, value);
		}
	}

	has(key) {
		return this.map.has(this.caseMap(key));
	}

	get(key) {
		var value = this.map.get(this.caseMap(key));
		if (value) {
			return value.value;
		}
		return undefined;
	}

	set(key, value) {
		this.map.set(this.caseMap(key), { key, value });
	}

	[Symbol.iterator]() {
		var it = this.map.entries();
		return {
			next: () => {
				var { value, done } = it.next();
				if (done) {
					return { done: true };
				}
				return { value: [value[1].key, value[1].value], done: false };
			},
		};
	}
}