diff --git a/test/client/js/libs/handlebars/ircmessageparser/anyIntersection.js b/test/client/js/libs/handlebars/ircmessageparser/anyIntersection.js
new file mode 100644
index 00000000..b80a44ed
--- /dev/null
+++ b/test/client/js/libs/handlebars/ircmessageparser/anyIntersection.js
@@ -0,0 +1,30 @@
+"use strict";
+
+const expect = require("chai").expect;
+const anyIntersection = require("../../../../../../client/js/libs/handlebars/ircmessageparser/anyIntersection");
+
+describe("anyIntersection", () => {
+ it("should not intersect on edges", () => {
+ const a = {start: 1, end: 2};
+ const b = {start: 2, end: 3};
+
+ expect(anyIntersection(a, b)).to.equal(false);
+ expect(anyIntersection(b, a)).to.equal(false);
+ });
+
+ it("should intersect on overlapping", () => {
+ const a = {start: 0, end: 3};
+ const b = {start: 1, end: 2};
+
+ expect(anyIntersection(a, b)).to.equal(true);
+ expect(anyIntersection(b, a)).to.equal(true);
+ });
+
+ it("should not intersect", () => {
+ const a = {start: 0, end: 1};
+ const b = {start: 2, end: 3};
+
+ expect(anyIntersection(a, b)).to.equal(false);
+ expect(anyIntersection(b, a)).to.equal(false);
+ });
+});
diff --git a/test/client/js/libs/handlebars/ircmessageparser/findChannels.js b/test/client/js/libs/handlebars/ircmessageparser/findChannels.js
new file mode 100644
index 00000000..93c119ee
--- /dev/null
+++ b/test/client/js/libs/handlebars/ircmessageparser/findChannels.js
@@ -0,0 +1,123 @@
+"use strict";
+
+const expect = require("chai").expect;
+const analyseText = require("../../../../../../client/js/libs/handlebars/ircmessageparser/findChannels");
+
+describe("findChannels", () => {
+ it("should find single letter channel", () => {
+ const input = "#a";
+ const expected = [{
+ channel: "#a",
+ start: 0,
+ end: 2
+ }];
+
+ const actual = analyseText(input, ["#"], ["@", "+"]);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should find utf8 channels", () => {
+ const input = "#äöü";
+ const expected = [{
+ channel: "#äöü",
+ start: 0,
+ end: 4
+ }];
+
+ const actual = analyseText(input, ["#"], ["@", "+"]);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should find inline channel", () => {
+ const input = "inline #channel text";
+ const expected = [{
+ channel: "#channel",
+ start: 7,
+ end: 15
+ }];
+
+ const actual = analyseText(input, ["#"], ["@", "+"]);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should stop at \\0x07", () => {
+ const input = "#chan\x07nel";
+ const expected = [{
+ channel: "#chan",
+ start: 0,
+ end: 5
+ }];
+
+ const actual = analyseText(input, ["#"], ["@", "+"]);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should allow classics pranks", () => {
+ const input = "#1,000";
+ const expected = [{
+ channel: "#1,000",
+ start: 0,
+ end: 6
+ }];
+
+ const actual = analyseText(input, ["#"], ["@", "+"]);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should work with whois reponses", () => {
+ const input = "@#a";
+ const expected = [{
+ channel: "#a",
+ start: 1,
+ end: 3
+ }];
+
+ const actual = analyseText(input, ["#"], ["@", "+"]);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should work with IRCv3.1 multi-prefix", () => {
+ const input = "!@%+#a";
+ const expected = [{
+ channel: "#a",
+ start: 4,
+ end: 6
+ }];
+
+ const actual = analyseText(input, ["#"], ["!", "@", "%", "+"]);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should work with custom channelPrefixes", () => {
+ const input = "@a";
+ const expected = [{
+ channel: "@a",
+ start: 0,
+ end: 2
+ }];
+
+ const actual = analyseText(input, ["@"], ["#", "+"]);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should handle multiple channelPrefix correctly", () => {
+ const input = "##test";
+ const expected = [{
+ channel: "##test",
+ start: 0,
+ end: 6
+ }];
+
+ const actual = analyseText(input, ["#"], ["@", "+"]);
+
+ expect(actual).to.deep.equal(expected);
+ });
+});
diff --git a/test/client/js/libs/handlebars/ircmessageparser/merge.js b/test/client/js/libs/handlebars/ircmessageparser/merge.js
new file mode 100644
index 00000000..d55ac1a2
--- /dev/null
+++ b/test/client/js/libs/handlebars/ircmessageparser/merge.js
@@ -0,0 +1,63 @@
+"use strict";
+
+const expect = require("chai").expect;
+const merge = require("../../../../../../client/js/libs/handlebars/ircmessageparser/merge");
+
+describe("merge", () => {
+ it("should split style information", () => {
+ const textParts = [{
+ start: 0,
+ end: 10,
+ flag1: true
+ }, {
+ start: 10,
+ end: 20,
+ flag2: true
+ }];
+ const styleFragments = [{
+ start: 0,
+ end: 5,
+ text: "01234"
+ }, {
+ start: 5,
+ end: 15,
+ text: "5678901234"
+ }, {
+ start: 15,
+ end: 20,
+ text: "56789"
+ }];
+
+ const expected = [{
+ start: 0,
+ end: 10,
+ flag1: true,
+ fragments: [{
+ start: 0,
+ end: 5,
+ text: "01234"
+ }, {
+ start: 5,
+ end: 10,
+ text: "56789"
+ }]
+ }, {
+ start: 10,
+ end: 20,
+ flag2: true,
+ fragments: [{
+ start: 10,
+ end: 15,
+ text: "01234"
+ }, {
+ start: 15,
+ end: 20,
+ text: "56789"
+ }]
+ }];
+
+ const actual = merge(textParts, styleFragments);
+
+ expect(actual).to.deep.equal(expected);
+ });
+});
diff --git a/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js b/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js
new file mode 100644
index 00000000..6af289c4
--- /dev/null
+++ b/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js
@@ -0,0 +1,274 @@
+"use strict";
+
+const expect = require("chai").expect;
+const parseStyle = require("../../../../../../client/js/libs/handlebars/ircmessageparser/parseStyle");
+
+describe("parseStyle", () => {
+ it("should skip control codes", () => {
+ const input = "text\x01with\x04control\x05codes";
+ const expected = [{
+ bold: false,
+ textColor: undefined,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "textwithcontrolcodes",
+
+ start: 0,
+ end: 20
+ }];
+
+ const actual = parseStyle(input);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should parse bold", () => {
+ const input = "\x02bold";
+ const expected = [{
+ bold: true,
+ textColor: undefined,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "bold",
+
+ start: 0,
+ end: 4
+ }];
+
+ const actual = parseStyle(input);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should parse textColor", () => {
+ const input = "\x038yellowText";
+ const expected = [{
+ bold: false,
+ textColor: 8,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "yellowText",
+
+ start: 0,
+ end: 10
+ }];
+
+ const actual = parseStyle(input);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should parse textColor and background", () => {
+ const input = "\x034,8yellowBG redText";
+ const expected = [{
+ textColor: 4,
+ bgColor: 8,
+ bold: false,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "yellowBG redText",
+
+ start: 0,
+ end: 16
+ }];
+
+ const actual = parseStyle(input);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should parse italic", () => {
+ const input = "\x1ditalic";
+ const expected = [{
+ bold: false,
+ textColor: undefined,
+ bgColor: undefined,
+ reverse: false,
+ italic: true,
+ underline: false,
+ text: "italic",
+
+ start: 0,
+ end: 6
+ }];
+
+ const actual = parseStyle(input);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should carry state corretly forward", () => {
+ const input = "\x02bold\x038yellow\x02nonBold\x03default";
+ const expected = [{
+ bold: true,
+ textColor: undefined,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "bold",
+
+ start: 0,
+ end: 4
+ }, {
+ bold: true,
+ textColor: 8,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "yellow",
+
+ start: 4,
+ end: 10
+ }, {
+ bold: false,
+ textColor: 8,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "nonBold",
+
+ start: 10,
+ end: 17
+ }, {
+ bold: false,
+ textColor: undefined,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "default",
+
+ start: 17,
+ end: 24
+ }];
+
+ const actual = parseStyle(input);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should toggle bold correctly", () => {
+ const input = "\x02bold\x02 \x02bold\x02";
+ const expected = [{
+ bold: true,
+ textColor: undefined,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "bold",
+
+ start: 0,
+ end: 4
+ }, {
+ bold: false,
+ textColor: undefined,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: " ",
+
+ start: 4,
+ end: 5
+ }, {
+ bold: true,
+ textColor: undefined,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "bold",
+
+ start: 5,
+ end: 9
+ }];
+
+ const actual = parseStyle(input);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should reset all styles", () => {
+ const input = "\x02\x034\x16\x1d\x1ffull\x0fnone";
+ const expected = [{
+ bold: true,
+ textColor: 4,
+ bgColor: undefined,
+ reverse: true,
+ italic: true,
+ underline: true,
+ text: "full",
+
+ start: 0,
+ end: 4
+ }, {
+ bold: false,
+ textColor: undefined,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "none",
+
+ start: 4,
+ end: 8
+ }];
+
+ const actual = parseStyle(input);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should not emit empty fragments", () => {
+ const input = "\x031\x031,2\x031\x031,2\x031\x031,2\x03a";
+ const expected = [{
+ bold: false,
+ textColor: undefined,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: "a",
+
+ start: 0,
+ end: 1
+ }];
+
+ const actual = parseStyle(input);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should optimize fragments", () => {
+ const rawString = "oh hi test text";
+ const colorCode = "\x0312";
+ const input = colorCode + rawString.split("").join(colorCode);
+ const expected = [{
+ bold: false,
+ textColor: 12,
+ bgColor: undefined,
+ reverse: false,
+ italic: false,
+ underline: false,
+ text: rawString,
+
+ start: 0,
+ end: rawString.length
+ }];
+
+ const actual = parseStyle(input);
+
+ expect(actual).to.deep.equal(expected);
+ });
+});
diff --git a/test/client/js/libs/handlebars/parse.js b/test/client/js/libs/handlebars/parse.js
new file mode 100644
index 00000000..7d1e025a
--- /dev/null
+++ b/test/client/js/libs/handlebars/parse.js
@@ -0,0 +1,336 @@
+"use strict";
+
+const expect = require("chai").expect;
+const parse = require("../../../../../client/js/libs/handlebars/parse");
+
+describe("parse Handlebars helper", () => {
+ it("should not introduce xss", () => {
+ const testCases = [{
+ input: "",
+ expected: "<img onerror='location.href="//youtube.com"'>"
+ }, {
+ input: "#&\">bug",
+ expected: "#&">bug"
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should skip control codes", () => {
+ const testCases = [{
+ input: "text\x01with\x04control\x05codes",
+ expected: "textwithcontrolcodes"
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should find urls", () => {
+ const testCases = [{
+ input: "irc://freenode.net/thelounge",
+ expected:
+ "" +
+ "irc://freenode.net/thelounge" +
+ ""
+ }, {
+ input: "www.nooooooooooooooo.com",
+ expected:
+ "" +
+ "www.nooooooooooooooo.com" +
+ ""
+ }, {
+ input: "look at https://thelounge.github.io/ for more information",
+ expected:
+ "look at " +
+ "" +
+ "https://thelounge.github.io/" +
+ "" +
+ " for more information",
+ }, {
+ input: "use www.duckduckgo.com for privacy reasons",
+ expected:
+ "use " +
+ "" +
+ "www.duckduckgo.com" +
+ "" +
+ " for privacy reasons"
+ }, {
+ input: "svn+ssh://example.org",
+ expected:
+ "" +
+ "svn+ssh://example.org" +
+ ""
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("url with a dot parsed correctly", () => {
+ const input =
+ "bonuspunkt: your URL parser misparses this URL: https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx";
+ const correctResult =
+ "bonuspunkt: your URL parser misparses this URL: " +
+ "" +
+ "https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx" +
+ "";
+
+ const actual = parse(input);
+
+ expect(actual).to.deep.equal(correctResult);
+ });
+
+ it("should balance brackets", () => {
+ const testCases = [{
+ input: "",
+ expected:
+ "<" +
+ "" +
+ "https://theos.kyriasis.com/~kyrias/stats/archlinux.html" +
+ "" +
+ ">"
+ }, {
+ input: "abc (www.example.com)",
+ expected:
+ "abc (" +
+ "" +
+ "www.example.com" +
+ "" +
+ ")"
+ }, {
+ input: "http://example.com/Test_(Page)",
+ expected:
+ "" +
+ "http://example.com/Test_(Page)" +
+ ""
+ }, {
+ input: "www.example.com/Test_(Page)",
+ expected:
+ "" +
+ "www.example.com/Test_(Page)" +
+ ""
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should not find urls", () => {
+ const testCases = [{
+ input: "text www. text",
+ expected: "text www. text"
+ }, {
+ input: "http://.",
+ expected: "http://."
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should find channels", () => {
+ const testCases = [{
+ input: "#a",
+ expected:
+ "" +
+ "#a" +
+ ""
+ }, {
+ input: "#test",
+ expected:
+ "" +
+ "#test" +
+ ""
+ }, {
+ input: "#äöü",
+ expected:
+ "" +
+ "#äöü" +
+ ""
+ }, {
+ input: "inline #channel text",
+ expected:
+ "inline " +
+ "" +
+ "#channel" +
+ "" +
+ " text"
+ }, {
+ input: "#1,000",
+ expected:
+ "" +
+ "#1,000" +
+ ""
+ }, {
+ input: "@#a",
+ expected:
+ "@" +
+ "" +
+ "#a" +
+ ""
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should not find channels", () => {
+ const testCases = [{
+ input: "hi#test",
+ expected: "hi#test"
+ }, {
+ input: "#",
+ expected: "#"
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should style like mirc", () => {
+ const testCases = [{
+ input: "\x02bold",
+ expected: "bold"
+ }, {
+ input: "\x038yellowText",
+ expected: "yellowText"
+ }, {
+ input: "\x030,0white,white",
+ expected: "white,white"
+ }, {
+ input: "\x034,8yellowBGredText",
+ expected: "yellowBGredText"
+ }, {
+ input: "\x1ditalic",
+ expected: "italic"
+ }, {
+ input: "\x1funderline",
+ expected: "underline"
+ }, {
+ input: "\x02bold\x038yellow\x02nonBold\x03default",
+ expected:
+ "bold" +
+ "yellow" +
+ "nonBold" +
+ "default"
+ }, {
+ input: "\x02bold\x02 \x02bold\x02",
+ expected:
+ "bold" +
+ " " +
+ "bold"
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should go bonkers like mirc", () => {
+ const testCases = [{
+ input: "\x02irc\x0f://\x1dfreenode.net\x0f/\x034,8thelounge",
+ expected:
+ "" +
+ "irc" +
+ "://" +
+ "freenode.net" +
+ "/" +
+ "thelounge" +
+ ""
+ }, {
+ input: "\x02#\x038,9thelounge",
+ expected:
+ "" +
+ "#" +
+ "thelounge" +
+ ""
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should optimize generated html", () => {
+ const testCases = [{
+ input: "test \x0312#\x0312\x0312\"te\x0312st\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312a",
+ expected:
+ "test " +
+ "" +
+ "#"testa" +
+ ""
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should trim commom protocols", () => {
+ const testCases = [{
+ input: "like..http://example.com",
+ expected:
+ "like.." +
+ "" +
+ "http://example.com" +
+ ""
+ }, {
+ input: "like..HTTP://example.com",
+ expected:
+ "like.." +
+ "" +
+ "HTTP://example.com" +
+ ""
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should not find channel in fragment", () => {
+ const testCases = [{
+ input: "http://example.com/#hash",
+ expected:
+ "" +
+ "" +
+ "http://example.com/#hash" +
+ ""
+ }];
+
+ const actual = testCases.map(testCase => parse(testCase.input));
+ const expected = testCases.map(testCase => testCase.expected);
+
+ expect(actual).to.deep.equal(expected);
+ });
+
+ it("should not overlap parts", () => {
+ const input = "Url: http://example.com/path Channel: ##channel";
+ const actual = parse(input);
+
+ expect(actual).to.equal(
+ "Url: http://example.com/path " +
+ "Channel: ##channel"
+ );
+ });
+});