diff --git a/client/css/style.css b/client/css/style.css index 6ab8c4d3..1d619b10 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -80,6 +80,7 @@ button { code, kbd, +.irc-monospace, textarea#user-specified-css-input { font-family: Consolas, Menlo, Monaco, "Lucida Console", "DejaVu Sans Mono", "Courier New", monospace; } @@ -1947,6 +1948,10 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */ text-decoration: underline; } +.irc-strikethrough { + text-decoration: line-through; +} + .irc-italic { font-style: italic; } diff --git a/client/js/keybinds.js b/client/js/keybinds.js index 1c269ac3..eb9f51a4 100644 --- a/client/js/keybinds.js +++ b/client/js/keybinds.js @@ -72,6 +72,8 @@ const colorsHotkeys = { u: "\x1F", i: "\x1D", o: "\x0F", + s: "\x1e", + m: "\x11", }; for (const hotkey in colorsHotkeys) { diff --git a/client/js/libs/handlebars/ircmessageparser/parseStyle.js b/client/js/libs/handlebars/ircmessageparser/parseStyle.js index 5a3ca566..642b4e52 100644 --- a/client/js/libs/handlebars/ircmessageparser/parseStyle.js +++ b/client/js/libs/handlebars/ircmessageparser/parseStyle.js @@ -8,6 +8,8 @@ const RESET = "\x0f"; const REVERSE = "\x16"; const ITALIC = "\x1d"; const UNDERLINE = "\x1f"; +const STRIKETHROUGH = "\x1e"; +const MONOSPACE = "\x11"; // Color code matcher, with format `XX,YY` where both `XX` and `YY` are // integers, `XX` is the text color and `YY` is an optional background color. @@ -22,7 +24,7 @@ const controlCodesRx = /[\u0000-\u001F]/g; // Converts a given text into an array of objects, each of them representing a // similarly styled section of the text. Each object carries the `text`, style // information (`bold`, `textColor`, `bgcolor`, `reverse`, `italic`, -// `underline`), and `start`/`end` cursors. +// `underline`, `strikethrough`, `monospace`), and `start`/`end` cursors. function parseStyle(text) { const result = []; let start = 0; @@ -30,7 +32,7 @@ function parseStyle(text) { // At any given time, these carry style information since last time a styling // control code was met. - let colorCodes, bold, textColor, bgColor, hexColor, hexBgColor, reverse, italic, underline; + let colorCodes, bold, textColor, bgColor, hexColor, hexBgColor, reverse, italic, underline, strikethrough, monospace; const resetStyle = () => { bold = false; @@ -41,6 +43,8 @@ function parseStyle(text) { reverse = false; italic = false; underline = false; + strikethrough = false; + monospace = false; }; resetStyle(); @@ -68,6 +72,8 @@ function parseStyle(text) { reverse, italic, underline, + strikethrough, + monospace, text: processedText, start: fragmentStart, end: fragmentStart + processedText.length, @@ -156,6 +162,16 @@ function parseStyle(text) { emitFragment(); underline = !underline; break; + + case STRIKETHROUGH: + emitFragment(); + strikethrough = !strikethrough; + break; + + case MONOSPACE: + emitFragment(); + monospace = !monospace; + break; } // Evaluate the next character at the next iteration @@ -168,7 +184,7 @@ function parseStyle(text) { return result; } -const properties = ["bold", "textColor", "bgColor", "hexColor", "hexBgColor", "italic", "underline", "reverse"]; +const properties = ["bold", "textColor", "bgColor", "hexColor", "hexBgColor", "italic", "underline", "reverse", "strikethrough", "monospace"]; function prepare(text) { return parseStyle(text) diff --git a/client/js/libs/handlebars/parse.js b/client/js/libs/handlebars/parse.js index ec0a7dc6..71bb3017 100644 --- a/client/js/libs/handlebars/parse.js +++ b/client/js/libs/handlebars/parse.js @@ -28,6 +28,12 @@ function createFragment(fragment) { if (fragment.underline) { classes.push("irc-underline"); } + if (fragment.strikethrough) { + classes.push("irc-strikethrough"); + } + if (fragment.monospace) { + classes.push("irc-monospace"); + } let attributes = classes.length ? ` class="${classes.join(" ")}"` : ""; const escapedText = Handlebars.Utils.escapeExpression(fragment.text); diff --git a/client/views/windows/help.tpl b/client/views/windows/help.tpl index 6db8d46d..35ef3ede 100644 --- a/client/views/windows/help.tpl +++ b/client/views/windows/help.tpl @@ -65,6 +65,24 @@ +
+
+ Ctrl + S +
+
+

Mark all text typed after this shortcut as struck through.

+
+
+ +
+
+ Ctrl + M +
+
+

Mark all text typed after this shortcut as monospaced.

+
+
+
Ctrl + O diff --git a/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js b/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js index 6d3e225d..99f14a80 100644 --- a/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js +++ b/test/client/js/libs/handlebars/ircmessageparser/parseStyle.js @@ -15,6 +15,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "textwithcontrolcodes", start: 0, @@ -37,6 +39,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "bold", start: 0, @@ -48,6 +52,417 @@ describe("parseStyle", () => { expect(actual).to.deep.equal(expected); }); + it("should parse strikethrough", () => { + const input = "\x1estrikethrough text\x1e"; + const expected = [{ + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: true, + monospace: false, + text: "strikethrough text", + + start: 0, + end: 18, + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should parse monospace", () => { + const input = "\x11monospace text\x1e"; + const expected = [{ + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: true, + text: "monospace text", + + start: 0, + end: 14, + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should toggle monospace correctly", () => { + const input = "toggling \x11on and \x11off and \x11on again\x11"; + const expected = [{ + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: false, + text: "toggling ", + + start: 0, + end: 9, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: true, + text: "on and ", + + start: 9, + end: 16, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: false, + text: "off and ", + + start: 16, + end: 24, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: true, + text: "on again", + + start: 24, + end: 32, + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should parse monospace and underline", () => { + const input = "\x1funderline formatting \x11with monospace\x1f no underline \x11 and vanilla"; + const expected = [{ + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: true, + strikethrough: false, + monospace: false, + text: "underline formatting ", + + start: 0, + end: 21, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: true, + strikethrough: false, + monospace: true, + text: "with monospace", + + start: 21, + end: 35, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: true, + text: " no underline ", + + start: 35, + end: 49, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: false, + text: " and vanilla", + + start: 49, + end: 61, + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should parse monospace and text colors", () => { + const input = "\x037,9\x11text with color and monospace\x11\x03"; + const expected = [{ + bold: false, + textColor: 7, + bgColor: 9, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: true, + text: "text with color and monospace", + + start: 0, + end: 29, + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should parse strikethrough and italics", () => { + const input = "\x1ditalic formatting \x1ewith strikethrough\x1d no italic \x1e and vanilla"; + const expected = [{ + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: true, + underline: false, + strikethrough: false, + monospace: false, + text: "italic formatting ", + + start: 0, + end: 18, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: true, + underline: false, + strikethrough: true, + monospace: false, + text: "with strikethrough", + + start: 18, + end: 36, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: true, + monospace: false, + text: " no italic ", + + start: 36, + end: 47, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: false, + text: " and vanilla", + + start: 47, + end: 59, + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should parse strikethrough and text colors", () => { + const input = "\x031,2text with color \x1eand strikethrough\x1e\x03"; + const expected = [{ + bold: false, + textColor: 1, + bgColor: 2, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: false, + text: "text with color ", + + start: 0, + end: 16, + }, { + bold: false, + textColor: 1, + bgColor: 2, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: true, + monospace: false, + text: "and strikethrough", + + start: 16, + end: 33, + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should correctly parse multiple unclosed format tokens", () => { + const input = "\x1e\x02\x1d\x033,4string with multiple unclosed formats"; + const expected = [{ + bold: true, + textColor: 3, + bgColor: 4, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: true, + underline: false, + strikethrough: true, + monospace: false, + text: "string with multiple unclosed formats", + + start: 0, + end: 37, + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + + it("should toggle strikethrough correctly", () => { + const input = "toggling \x1eon and \x1eoff and \x1eon again\x1e"; + const expected = [{ + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: false, + text: "toggling ", + + start: 0, + end: 9, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: true, + monospace: false, + text: "on and ", + + start: 9, + end: 16, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: false, + monospace: false, + text: "off and ", + + start: 16, + end: 24, + }, { + bold: false, + textColor: undefined, + bgColor: undefined, + hexColor: undefined, + hexBgColor: undefined, + reverse: false, + italic: false, + underline: false, + strikethrough: true, + monospace: false, + text: "on again", + + start: 24, + end: 32, + }]; + + const actual = parseStyle(input); + + expect(actual).to.deep.equal(expected); + }); + it("should parse textColor", () => { const input = "\x038yellowText"; const expected = [{ @@ -59,6 +474,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "yellowText", start: 0, @@ -81,6 +498,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "yellowBG redText", start: 0, @@ -103,6 +522,8 @@ describe("parseStyle", () => { reverse: false, italic: true, underline: false, + strikethrough: false, + monospace: false, text: "italic", start: 0, @@ -125,6 +546,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "test ", start: 0, @@ -138,6 +561,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "nice ", start: 5, @@ -151,6 +576,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "RES006 ", start: 10, @@ -164,6 +591,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "colored", start: 17, @@ -177,6 +606,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: " background", start: 24, @@ -190,6 +621,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "?", start: 35, @@ -212,6 +645,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "bold", start: 0, @@ -225,6 +660,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "yellow", start: 4, @@ -238,6 +675,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "nonBold", start: 10, @@ -251,6 +690,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "default", start: 17, @@ -273,6 +714,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "bold", start: 0, @@ -286,6 +729,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: " ", start: 4, @@ -299,6 +744,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "bold", start: 5, @@ -311,7 +758,7 @@ describe("parseStyle", () => { }); it("should reset all styles", () => { - const input = "\x02\x034\x16\x1d\x1ffull\x0fnone"; + const input = "\x11\x1e\x02\x034\x16\x1d\x1ffull\x0fnone"; const expected = [{ bold: true, textColor: 4, @@ -321,6 +768,8 @@ describe("parseStyle", () => { reverse: true, italic: true, underline: true, + strikethrough: true, + monospace: true, text: "full", start: 0, @@ -334,6 +783,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "none", start: 4, @@ -356,6 +807,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: "a", start: 0, @@ -380,6 +833,8 @@ describe("parseStyle", () => { reverse: false, italic: false, underline: false, + strikethrough: false, + monospace: false, text: rawString, start: 0, diff --git a/test/client/js/libs/handlebars/parse.js b/test/client/js/libs/handlebars/parse.js index 2d26976f..bb9e0efe 100644 --- a/test/client/js/libs/handlebars/parse.js +++ b/test/client/js/libs/handlebars/parse.js @@ -235,6 +235,14 @@ describe("parse Handlebars helper", () => { name: "underline", input: "\x1funderline", expected: 'underline', + }, { + name: "strikethrough", + input: "\x1estrikethrough", + expected: 'strikethrough', + }, { + name: "monospace", + input: "\x11monospace", + expected: 'monospace', }, { name: "resets", input: "\x02bold\x038yellow\x02nonBold\x03default",