mirror of
https://github.com/thelounge/thelounge
synced 2024-11-26 22:10:22 +00:00
Merge pull request #972 from thelounge/bonuspunkt/message-parser
New IRC message parser
This commit is contained in:
commit
6c0cf7cfcc
14 changed files with 1414 additions and 121 deletions
|
@ -0,0 +1,12 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Return true if any section of "a" or "b" parts (defined by their start/end
|
||||||
|
// markers) intersect each other, false otherwise.
|
||||||
|
function anyIntersection(a, b) {
|
||||||
|
return a.start <= b.start && b.start < a.end ||
|
||||||
|
a.start < b.end && b.end <= a.end ||
|
||||||
|
b.start <= a.start && a.start < b.end ||
|
||||||
|
b.start < a.end && a.end <= b.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = anyIntersection;
|
34
client/js/libs/handlebars/ircmessageparser/fill.js
Normal file
34
client/js/libs/handlebars/ircmessageparser/fill.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Create plain text entries corresponding to areas of the text that match no
|
||||||
|
// existing entries. Returns an empty array if all parts of the text have been
|
||||||
|
// parsed into recognizable entries already.
|
||||||
|
function fill(existingEntries, text) {
|
||||||
|
let position = 0;
|
||||||
|
|
||||||
|
// Fill inner parts of the text. For example, if text is `foobarbaz` and both
|
||||||
|
// `foo` and `baz` have matched into an entry, this will return a dummy entry
|
||||||
|
// corresponding to `bar`.
|
||||||
|
const result = existingEntries.reduce((acc, textSegment) => {
|
||||||
|
if (textSegment.start > position) {
|
||||||
|
acc.push({
|
||||||
|
start: position,
|
||||||
|
end: textSegment.start
|
||||||
|
});
|
||||||
|
}
|
||||||
|
position = textSegment.end;
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Complete the unmatched end of the text with a dummy entry
|
||||||
|
if (position < text.length) {
|
||||||
|
result.push({
|
||||||
|
start: position,
|
||||||
|
end: text.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = fill;
|
43
client/js/libs/handlebars/ircmessageparser/findChannels.js
Normal file
43
client/js/libs/handlebars/ircmessageparser/findChannels.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Escapes the RegExp special characters "^", "$", "", ".", "*", "+", "?", "(",
|
||||||
|
// ")", "[", "]", "{", "}", and "|" in string.
|
||||||
|
// See https://lodash.com/docs/#escapeRegExp
|
||||||
|
const escapeRegExp = require("lodash/escapeRegExp");
|
||||||
|
|
||||||
|
// Given an array of channel prefixes (such as "#" and "&") and an array of user
|
||||||
|
// modes (such as "@" and "+"), this function extracts channels and nicks from a
|
||||||
|
// text.
|
||||||
|
// It returns an array of objects for each channel found with their start index,
|
||||||
|
// end index and channel name.
|
||||||
|
function findChannels(text, channelPrefixes, userModes) {
|
||||||
|
// `userModePattern` is necessary to ignore user modes in /whois responses.
|
||||||
|
// For example, a voiced user in #thelounge will have a /whois response of:
|
||||||
|
// > foo is on the following channels: +#thelounge
|
||||||
|
// We need to explicitly ignore user modes to parse such channels correctly.
|
||||||
|
const userModePattern = userModes.map(escapeRegExp).join("");
|
||||||
|
const channelPrefixPattern = channelPrefixes.map(escapeRegExp).join("");
|
||||||
|
const channelPattern = `(?:^|\\s)[${userModePattern}]*([${channelPrefixPattern}][^ \u0007]+)`;
|
||||||
|
const channelRegExp = new RegExp(channelPattern, "g");
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
do {
|
||||||
|
// With global ("g") regexes, calling `exec` multiple times will find
|
||||||
|
// successive matches in the same string.
|
||||||
|
match = channelRegExp.exec(text);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
result.push({
|
||||||
|
start: match.index + match[0].length - match[1].length,
|
||||||
|
end: match.index + match[0].length,
|
||||||
|
channel: match[1]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} while (match);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = findChannels;
|
56
client/js/libs/handlebars/ircmessageparser/findLinks.js
Normal file
56
client/js/libs/handlebars/ircmessageparser/findLinks.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const URI = require("urijs");
|
||||||
|
|
||||||
|
// Known schemes to detect in a text. If a text contains `foo...bar://foo.com`,
|
||||||
|
// the parsed scheme should be `foo...bar` but if it contains
|
||||||
|
// `foo...http://foo.com`, we assume the scheme to extract will be `http`.
|
||||||
|
const commonSchemes = [
|
||||||
|
"http", "https",
|
||||||
|
"ftp", "sftp",
|
||||||
|
"smb", "file",
|
||||||
|
"irc", "ircs",
|
||||||
|
"svn", "git",
|
||||||
|
"steam", "mumble", "ts3server",
|
||||||
|
"svn+ssh", "ssh",
|
||||||
|
];
|
||||||
|
|
||||||
|
function findLinks(text) {
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
// URI.withinString() identifies URIs within text, e.g. to translate them to
|
||||||
|
// <a>-Tags.
|
||||||
|
// See https://medialize.github.io/URI.js/docs.html#static-withinString
|
||||||
|
// In our case, we store each URI encountered in a result array.
|
||||||
|
URI.withinString(text, function(url, start, end) {
|
||||||
|
// Extract the scheme of the URL detected, if there is one
|
||||||
|
const parsedScheme = URI(url).scheme().toLowerCase();
|
||||||
|
|
||||||
|
// Check if the scheme of the detected URL matches a common one above.
|
||||||
|
// In a URL like `foo..http://example.com`, the scheme would be `foo..http`,
|
||||||
|
// so we need to clean up the end of the scheme and filter out the rest.
|
||||||
|
const matchedScheme = commonSchemes.find(scheme => parsedScheme.endsWith(scheme));
|
||||||
|
|
||||||
|
// A known scheme was found, extract the unknown part from the URL
|
||||||
|
if (matchedScheme) {
|
||||||
|
const prefix = parsedScheme.length - matchedScheme.length;
|
||||||
|
start += prefix;
|
||||||
|
url = url.slice(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The URL matched but does not start with a scheme (`www.foo.com`), add it
|
||||||
|
if (!parsedScheme.length) {
|
||||||
|
url = "http://" + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
link: url
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = findLinks;
|
60
client/js/libs/handlebars/ircmessageparser/merge.js
Normal file
60
client/js/libs/handlebars/ircmessageparser/merge.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const anyIntersection = require("./anyIntersection");
|
||||||
|
const fill = require("./fill");
|
||||||
|
|
||||||
|
let Object_assign = Object.assign;
|
||||||
|
|
||||||
|
if (typeof Object_assign !== "function") {
|
||||||
|
Object_assign = function(target) {
|
||||||
|
Array.prototype.slice.call(arguments, 1).forEach(function(obj) {
|
||||||
|
Object.keys(obj).forEach(function(key) {
|
||||||
|
target[key] = obj[key];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge text part information within a styling fragment
|
||||||
|
function assign(textPart, fragment) {
|
||||||
|
const fragStart = fragment.start;
|
||||||
|
const start = Math.max(fragment.start, textPart.start);
|
||||||
|
const end = Math.min(fragment.end, textPart.end);
|
||||||
|
|
||||||
|
return Object_assign({}, fragment, {
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
text: fragment.text.slice(start - fragStart, end - fragStart)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the style fragments withing the text parts, taking into account
|
||||||
|
// boundaries and text sections that have not matched to links or channels.
|
||||||
|
// For example, given a string "foobar" where "foo" and "bar" have been
|
||||||
|
// identified as parts (channels, links, etc.) and "fo", "ob" and "ar" have 3
|
||||||
|
// different styles, the first resulting part will contain fragments "fo" and
|
||||||
|
// "o", and the second resulting part will contain "b" and "ar". "o" and "b"
|
||||||
|
// fragments will contain duplicate styling attributes.
|
||||||
|
function merge(textParts, styleFragments) {
|
||||||
|
// Re-build the overall text (without control codes) from the style fragments
|
||||||
|
const cleanText = styleFragments.reduce((acc, frag) => acc + frag.text, "");
|
||||||
|
|
||||||
|
// Every section of the original text that has not been captured in a "part"
|
||||||
|
// is filled with "text" parts, dummy objects with start/end but no extra
|
||||||
|
// metadata.
|
||||||
|
const allParts = textParts
|
||||||
|
.concat(fill(textParts, cleanText))
|
||||||
|
.sort((a, b) => a.start - b.start);
|
||||||
|
|
||||||
|
// Distribute the style fragments within the text parts
|
||||||
|
return allParts.map(textPart => {
|
||||||
|
textPart.fragments = styleFragments
|
||||||
|
.filter(fragment => anyIntersection(textPart, fragment))
|
||||||
|
.map(fragment => assign(textPart, fragment));
|
||||||
|
|
||||||
|
return textPart;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = merge;
|
161
client/js/libs/handlebars/ircmessageparser/parseStyle.js
Normal file
161
client/js/libs/handlebars/ircmessageparser/parseStyle.js
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Styling control codes
|
||||||
|
const BOLD = "\x02";
|
||||||
|
const COLOR = "\x03";
|
||||||
|
const RESET = "\x0f";
|
||||||
|
const REVERSE = "\x16";
|
||||||
|
const ITALIC = "\x1d";
|
||||||
|
const UNDERLINE = "\x1f";
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
const colorRx = /^(\d{1,2})(?:,(\d{1,2}))?/;
|
||||||
|
|
||||||
|
// Represents all other control codes that to be ignored/filtered from the text
|
||||||
|
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.
|
||||||
|
function parseStyle(text) {
|
||||||
|
const result = [];
|
||||||
|
let start = 0;
|
||||||
|
let position = 0;
|
||||||
|
|
||||||
|
// At any given time, these carry style information since last time a styling
|
||||||
|
// control code was met.
|
||||||
|
let colorCodes, bold, textColor, bgColor, reverse, italic, underline;
|
||||||
|
|
||||||
|
const resetStyle = () => {
|
||||||
|
bold = false;
|
||||||
|
textColor = undefined;
|
||||||
|
bgColor = undefined;
|
||||||
|
reverse = false;
|
||||||
|
italic = false;
|
||||||
|
underline = false;
|
||||||
|
};
|
||||||
|
resetStyle();
|
||||||
|
|
||||||
|
// When called, this "closes" the current fragment by adding an entry to the
|
||||||
|
// `result` array using the styling information set last time a control code
|
||||||
|
// was met.
|
||||||
|
const emitFragment = () => {
|
||||||
|
// Uses the text fragment starting from the last control code position up to
|
||||||
|
// the current position
|
||||||
|
const textPart = text.slice(start, position);
|
||||||
|
|
||||||
|
// Filters out all non-style related control codes present in this text
|
||||||
|
const processedText = textPart.replace(controlCodesRx, "");
|
||||||
|
|
||||||
|
if (processedText.length) {
|
||||||
|
// Current fragment starts where the previous one ends, or at 0 if none
|
||||||
|
const fragmentStart = result.length ? result[result.length - 1].end : 0;
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
bold,
|
||||||
|
textColor,
|
||||||
|
bgColor,
|
||||||
|
reverse,
|
||||||
|
italic,
|
||||||
|
underline,
|
||||||
|
text: processedText,
|
||||||
|
start: fragmentStart,
|
||||||
|
end: fragmentStart + processedText.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that a fragment has been "closed", the next one will start after that
|
||||||
|
start = position + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// This loop goes through each character of the given text one by one by
|
||||||
|
// bumping the `position` cursor. Every time a new special "styling" character
|
||||||
|
// is met, an object gets created (with `emitFragment()`)information on text
|
||||||
|
// encountered since the previous styling character.
|
||||||
|
while (position < text.length) {
|
||||||
|
switch (text[position]) {
|
||||||
|
|
||||||
|
case RESET:
|
||||||
|
emitFragment();
|
||||||
|
resetStyle();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Meeting a BOLD character means that the ongoing text is either going to
|
||||||
|
// be in bold or that the previous one was in bold and the following one
|
||||||
|
// must be reset.
|
||||||
|
// This same behavior applies to COLOR, REVERSE, ITALIC, and UNDERLINE.
|
||||||
|
case BOLD:
|
||||||
|
emitFragment();
|
||||||
|
bold = !bold;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case COLOR:
|
||||||
|
emitFragment();
|
||||||
|
|
||||||
|
// Go one step further to find the corresponding color
|
||||||
|
colorCodes = text.slice(position + 1).match(colorRx);
|
||||||
|
|
||||||
|
if (colorCodes) {
|
||||||
|
textColor = Number(colorCodes[1]);
|
||||||
|
if (colorCodes[2]) {
|
||||||
|
bgColor = Number(colorCodes[2]);
|
||||||
|
}
|
||||||
|
// Color code length is > 1, so bump the current position cursor by as
|
||||||
|
// much (and reset the start cursor for the current text block as well)
|
||||||
|
position += colorCodes[0].length;
|
||||||
|
start = position + 1;
|
||||||
|
} else {
|
||||||
|
// If no color codes were found, toggles back to no colors (like BOLD).
|
||||||
|
textColor = undefined;
|
||||||
|
bgColor = undefined;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case REVERSE:
|
||||||
|
emitFragment();
|
||||||
|
reverse = !reverse;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ITALIC:
|
||||||
|
emitFragment();
|
||||||
|
italic = !italic;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UNDERLINE:
|
||||||
|
emitFragment();
|
||||||
|
underline = !underline;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate the next character at the next iteration
|
||||||
|
position += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The entire text has been parsed, so we finalize the current text fragment.
|
||||||
|
emitFragment();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const properties = ["bold", "textColor", "bgColor", "italic", "underline", "reverse"];
|
||||||
|
|
||||||
|
function prepare(text) {
|
||||||
|
return parseStyle(text)
|
||||||
|
// This optimizes fragments by combining them together when all their values
|
||||||
|
// for the properties defined above are equal.
|
||||||
|
.reduce((prev, curr) => {
|
||||||
|
if (prev.length) {
|
||||||
|
const lastEntry = prev[prev.length - 1];
|
||||||
|
if (properties.every(key => curr[key] === lastEntry[key])) {
|
||||||
|
lastEntry.text += curr.text;
|
||||||
|
lastEntry.end += curr.text.length;
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prev.concat([curr]);
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = prepare;
|
|
@ -1,126 +1,71 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const Handlebars = require("handlebars/runtime");
|
const Handlebars = require("handlebars/runtime");
|
||||||
const URI = require("urijs");
|
const parseStyle = require("./ircmessageparser/parseStyle");
|
||||||
|
const findChannels = require("./ircmessageparser/findChannels");
|
||||||
|
const findLinks = require("./ircmessageparser/findLinks");
|
||||||
|
const merge = require("./ircmessageparser/merge");
|
||||||
|
|
||||||
module.exports = function(text) {
|
// Create an HTML `span` with styling information for a given fragment
|
||||||
text = Handlebars.Utils.escapeExpression(text);
|
function createFragment(fragment) {
|
||||||
text = colors(text);
|
let classes = [];
|
||||||
text = channels(text);
|
if (fragment.bold) {
|
||||||
text = uri(text);
|
classes.push("irc-bold");
|
||||||
return text;
|
}
|
||||||
|
if (fragment.textColor !== undefined) {
|
||||||
|
classes.push("irc-fg" + fragment.textColor);
|
||||||
|
}
|
||||||
|
if (fragment.bgColor !== undefined) {
|
||||||
|
classes.push("irc-bg" + fragment.bgColor);
|
||||||
|
}
|
||||||
|
if (fragment.italic) {
|
||||||
|
classes.push("irc-italic");
|
||||||
|
}
|
||||||
|
if (fragment.underline) {
|
||||||
|
classes.push("irc-underline");
|
||||||
|
}
|
||||||
|
const escapedText = Handlebars.Utils.escapeExpression(fragment.text);
|
||||||
|
if (classes.length) {
|
||||||
|
return `<span class="${classes.join(" ")}">${escapedText}</span>`;
|
||||||
|
}
|
||||||
|
return escapedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform an IRC message potentially filled with styling control codes, URLs
|
||||||
|
// and channels into a string of HTML elements to display on the client.
|
||||||
|
module.exports = function parse(text) {
|
||||||
|
// Extract the styling information and get the plain text version from it
|
||||||
|
const styleFragments = parseStyle(text);
|
||||||
|
const cleanText = styleFragments.map(fragment => fragment.text).join("");
|
||||||
|
|
||||||
|
// On the plain text, find channels and URLs, returned as "parts". Parts are
|
||||||
|
// arrays of objects containing start and end markers, as well as metadata
|
||||||
|
// depending on what was found (channel or link).
|
||||||
|
const channelPrefixes = ["#", "&"]; // TODO Channel prefixes should be RPL_ISUPPORT.CHANTYPES
|
||||||
|
const userModes = ["!", "@", "%", "+"]; // TODO User modes should be RPL_ISUPPORT.PREFIX
|
||||||
|
const channelParts = findChannels(cleanText, channelPrefixes, userModes);
|
||||||
|
const linkParts = findLinks(cleanText);
|
||||||
|
|
||||||
|
// Sort all parts identified based on their position in the original text
|
||||||
|
const parts = channelParts
|
||||||
|
.concat(linkParts)
|
||||||
|
.sort((a, b) => a.start - b.start);
|
||||||
|
|
||||||
|
// Merge the styling information with the channels / URLs / text objects and
|
||||||
|
// generate HTML strings with the resulting fragments
|
||||||
|
return merge(parts, styleFragments).map(textPart => {
|
||||||
|
// Create HTML strings with styling information
|
||||||
|
const fragments = textPart.fragments.map(createFragment).join("");
|
||||||
|
|
||||||
|
// Wrap these potentially styled fragments with links and channel buttons
|
||||||
|
if (textPart.link) {
|
||||||
|
const escapedLink = Handlebars.Utils.escapeExpression(textPart.link);
|
||||||
|
return `<a href="${escapedLink}" target="_blank" rel="noopener">${fragments}</a>`;
|
||||||
|
} else if (textPart.channel) {
|
||||||
|
const escapedChannel = Handlebars.Utils.escapeExpression(textPart.channel);
|
||||||
|
return `<span class="inline-channel" role="button" tabindex="0" data-chan="${escapedChannel}">${fragments}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fragments;
|
||||||
|
}).join("");
|
||||||
};
|
};
|
||||||
|
|
||||||
function uri(text) {
|
|
||||||
return URI.withinString(text, function(url) {
|
|
||||||
if (url.indexOf("javascript:") === 0) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
var split = url.split("<");
|
|
||||||
url = "<a href='" + split[0].replace(/^www/, "http://www") + "' target='_blank' rel='noopener'>" + split[0] + "</a>";
|
|
||||||
if (split.length > 1) {
|
|
||||||
url += "<" + split.slice(1).join("<");
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Channels names are strings of length up to fifty (50) characters.
|
|
||||||
* The only restriction on a channel name is that it SHALL NOT contain
|
|
||||||
* any spaces (' '), a control G (^G or ASCII 7), a comma (',').
|
|
||||||
* Channel prefix '&' is handled as '&' because this parser is executed
|
|
||||||
* after entities in the message have been escaped. This prevents a couple of bugs.
|
|
||||||
*/
|
|
||||||
function channels(text) {
|
|
||||||
return text.replace(
|
|
||||||
/(^|\s|\x07|,)((?:#|&)[^\x07\s,]{1,49})/g,
|
|
||||||
'$1<span class="inline-channel" role="button" tabindex="0" data-chan="$2">$2</span>'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIRC compliant colour and style parser
|
|
||||||
* Unfortuanately this is a non trivial operation
|
|
||||||
* See this branch for source and tests
|
|
||||||
* https://github.com/megawac/irc-style-parser/tree/shout
|
|
||||||
*/
|
|
||||||
var styleCheck_Re = /[\x00-\x1F]/,
|
|
||||||
back_re = /^([0-9]{1,2})(,([0-9]{1,2}))?/,
|
|
||||||
colourKey = "\x03",
|
|
||||||
// breaks all open styles ^O (\x0F)
|
|
||||||
styleBreak = "\x0F";
|
|
||||||
|
|
||||||
function styleTemplate(settings) {
|
|
||||||
return "<span class='" + settings.style + "'>" + settings.text + "</span>";
|
|
||||||
}
|
|
||||||
|
|
||||||
var styles = [
|
|
||||||
["normal", "\x00", ""], ["underline", "\x1F"],
|
|
||||||
["bold", "\x02"], ["italic", "\x1D"]
|
|
||||||
].map(function(style) {
|
|
||||||
var escaped = encodeURI(style[1]).replace("%", "\\x");
|
|
||||||
return {
|
|
||||||
name: style[0],
|
|
||||||
style: style[2] ? style[2] : "irc-" + style[0],
|
|
||||||
key: style[1],
|
|
||||||
keyregex: new RegExp(escaped + "(.*?)(" + escaped + "|$)")
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function colors(line) {
|
|
||||||
// http://www.mirc.com/colors.html
|
|
||||||
// http://www.aviran.org/stripremove-irc-client-control-characters/
|
|
||||||
// https://github.com/perl6/mu/blob/master/examples/rules/Grammar-IRC.pm
|
|
||||||
// regexs are cruel to parse this thing
|
|
||||||
|
|
||||||
// already done?
|
|
||||||
if (!styleCheck_Re.test(line)) {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
// split up by the irc style break character ^O
|
|
||||||
if (line.indexOf(styleBreak) >= 0) {
|
|
||||||
return line.split(styleBreak).map(colors).join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = line;
|
|
||||||
var parseArr = result.split(colourKey);
|
|
||||||
var text, match, colour, background = "";
|
|
||||||
for (var i = 0; i < parseArr.length; i++) {
|
|
||||||
text = parseArr[i];
|
|
||||||
match = text.match(back_re);
|
|
||||||
if (!match) {
|
|
||||||
// ^C (no colour) ending. Escape current colour and carry on
|
|
||||||
background = "";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
colour = "irc-fg" + +match[1];
|
|
||||||
// set the background colour
|
|
||||||
if (match[3]) {
|
|
||||||
background = " irc-bg" + +match[3];
|
|
||||||
}
|
|
||||||
// update the parsed text result
|
|
||||||
result = result.replace(colourKey + text, styleTemplate({
|
|
||||||
style: colour + background,
|
|
||||||
text: text.slice(match[0].length)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matching styles (italics/bold/underline)
|
|
||||||
// if only colours were this easy...
|
|
||||||
styles.forEach(function(style) {
|
|
||||||
if (result.indexOf(style.key) < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = result.replace(style.keyregex, function(matchedTrash, matchedText) {
|
|
||||||
return styleTemplate({
|
|
||||||
style: style.style,
|
|
||||||
text: matchedText
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
50
test/client/js/libs/handlebars/ircmessageparser/fill.js
Normal file
50
test/client/js/libs/handlebars/ircmessageparser/fill.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const expect = require("chai").expect;
|
||||||
|
const fill = require("../../../../../../client/js/libs/handlebars/ircmessageparser/fill");
|
||||||
|
|
||||||
|
describe("fill", () => {
|
||||||
|
const text = "01234567890123456789";
|
||||||
|
|
||||||
|
it("should return an entry for the unmatched end of string", () => {
|
||||||
|
const existingEntries = [
|
||||||
|
{start: 0, end: 10},
|
||||||
|
{start: 5, end: 15},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{start: 15, end: 20},
|
||||||
|
];
|
||||||
|
|
||||||
|
const actual = fill(existingEntries, text);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an entry per unmatched areas of the text", () => {
|
||||||
|
const existingEntries = [
|
||||||
|
{start: 0, end: 5},
|
||||||
|
{start: 10, end: 15},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{start: 5, end: 10},
|
||||||
|
{start: 15, end: 20},
|
||||||
|
];
|
||||||
|
|
||||||
|
const actual = fill(existingEntries, text);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not return anything when entries match all text", () => {
|
||||||
|
const existingEntries = [
|
||||||
|
{start: 0, end: 10},
|
||||||
|
{start: 10, end: 20},
|
||||||
|
];
|
||||||
|
|
||||||
|
const actual = fill(existingEntries, text);
|
||||||
|
|
||||||
|
expect(actual).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
123
test/client/js/libs/handlebars/ircmessageparser/findChannels.js
Normal file
123
test/client/js/libs/handlebars/ircmessageparser/findChannels.js
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const expect = require("chai").expect;
|
||||||
|
const findChannels = 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 = findChannels(input, ["#"], ["@", "+"]);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find utf8 channels", () => {
|
||||||
|
const input = "#äöü";
|
||||||
|
const expected = [{
|
||||||
|
channel: "#äöü",
|
||||||
|
start: 0,
|
||||||
|
end: 4
|
||||||
|
}];
|
||||||
|
|
||||||
|
const actual = findChannels(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 = findChannels(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 = findChannels(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 = findChannels(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 = findChannels(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 = findChannels(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 = findChannels(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 = findChannels(input, ["#"], ["@", "+"]);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
106
test/client/js/libs/handlebars/ircmessageparser/findLinks.js
Normal file
106
test/client/js/libs/handlebars/ircmessageparser/findLinks.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const expect = require("chai").expect;
|
||||||
|
const findLinks = require("../../../../../../client/js/libs/handlebars/ircmessageparser/findLinks");
|
||||||
|
|
||||||
|
describe("findLinks", () => {
|
||||||
|
it("should find url", () => {
|
||||||
|
const input = "irc://freenode.net/thelounge";
|
||||||
|
const expected = [{
|
||||||
|
start: 0,
|
||||||
|
end: 28,
|
||||||
|
link: "irc://freenode.net/thelounge",
|
||||||
|
}];
|
||||||
|
|
||||||
|
const actual = findLinks(input);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find urls with www", () => {
|
||||||
|
const input = "www.nooooooooooooooo.com";
|
||||||
|
const expected = [{
|
||||||
|
start: 0,
|
||||||
|
end: 24,
|
||||||
|
link: "http://www.nooooooooooooooo.com"
|
||||||
|
}];
|
||||||
|
|
||||||
|
const actual = findLinks(input);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find urls in strings", () => {
|
||||||
|
const input = "look at https://thelounge.github.io/ for more information";
|
||||||
|
const expected = [{
|
||||||
|
link: "https://thelounge.github.io/",
|
||||||
|
start: 8,
|
||||||
|
end: 36
|
||||||
|
}];
|
||||||
|
|
||||||
|
const actual = findLinks(input);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find urls in strings starting with www", () => {
|
||||||
|
const input = "use www.duckduckgo.com for privacy reasons";
|
||||||
|
const expected = [{
|
||||||
|
link: "http://www.duckduckgo.com",
|
||||||
|
start: 4,
|
||||||
|
end: 22
|
||||||
|
}];
|
||||||
|
|
||||||
|
const actual = findLinks(input);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find urls with odd surroundings", () => {
|
||||||
|
const input = "<https://theos.kyriasis.com/~kyrias/stats/archlinux.html>";
|
||||||
|
const expected = [{
|
||||||
|
link: "https://theos.kyriasis.com/~kyrias/stats/archlinux.html",
|
||||||
|
start: 1,
|
||||||
|
end: 56
|
||||||
|
}];
|
||||||
|
|
||||||
|
const actual = findLinks(input);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find urls with starting with www. and odd surroundings", () => {
|
||||||
|
const input = ".:www.github.com:.";
|
||||||
|
const expected = [{
|
||||||
|
link: "http://www.github.com",
|
||||||
|
start: 2,
|
||||||
|
end: 16
|
||||||
|
}];
|
||||||
|
|
||||||
|
const actual = findLinks(input);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not find urls", () => {
|
||||||
|
const input = "text www. text";
|
||||||
|
const expected = [];
|
||||||
|
|
||||||
|
const actual = findLinks(input);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple www. correctly", () => {
|
||||||
|
const input = "www.www.test.com";
|
||||||
|
const expected = [{
|
||||||
|
link: "http://www.www.test.com",
|
||||||
|
start: 0,
|
||||||
|
end: 16
|
||||||
|
}];
|
||||||
|
|
||||||
|
const actual = findLinks(input);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
63
test/client/js/libs/handlebars/ircmessageparser/merge.js
Normal file
63
test/client/js/libs/handlebars/ircmessageparser/merge.js
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
274
test/client/js/libs/handlebars/ircmessageparser/parseStyle.js
Normal file
274
test/client/js/libs/handlebars/ircmessageparser/parseStyle.js
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
336
test/client/js/libs/handlebars/parse.js
Normal file
336
test/client/js/libs/handlebars/parse.js
Normal file
|
@ -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: "<img onerror='location.href=\"//youtube.com\"'>",
|
||||||
|
expected: "<img onerror='location.href="//youtube.com"'>"
|
||||||
|
}, {
|
||||||
|
input: "#&\">bug",
|
||||||
|
expected: "<span class=\"inline-channel\" role=\"button\" tabindex=\"0\" data-chan=\"#&">bug\">#&">bug</span>"
|
||||||
|
}];
|
||||||
|
|
||||||
|
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:
|
||||||
|
"<a href=\"irc://freenode.net/thelounge\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"irc://freenode.net/thelounge" +
|
||||||
|
"</a>"
|
||||||
|
}, {
|
||||||
|
input: "www.nooooooooooooooo.com",
|
||||||
|
expected:
|
||||||
|
"<a href=\"http://www.nooooooooooooooo.com\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"www.nooooooooooooooo.com" +
|
||||||
|
"</a>"
|
||||||
|
}, {
|
||||||
|
input: "look at https://thelounge.github.io/ for more information",
|
||||||
|
expected:
|
||||||
|
"look at " +
|
||||||
|
"<a href=\"https://thelounge.github.io/\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"https://thelounge.github.io/" +
|
||||||
|
"</a>" +
|
||||||
|
" for more information",
|
||||||
|
}, {
|
||||||
|
input: "use www.duckduckgo.com for privacy reasons",
|
||||||
|
expected:
|
||||||
|
"use " +
|
||||||
|
"<a href=\"http://www.duckduckgo.com\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"www.duckduckgo.com" +
|
||||||
|
"</a>" +
|
||||||
|
" for privacy reasons"
|
||||||
|
}, {
|
||||||
|
input: "svn+ssh://example.org",
|
||||||
|
expected:
|
||||||
|
"<a href=\"svn+ssh://example.org\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"svn+ssh://example.org" +
|
||||||
|
"</a>"
|
||||||
|
}];
|
||||||
|
|
||||||
|
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: " +
|
||||||
|
"<a href=\"https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx" +
|
||||||
|
"</a>";
|
||||||
|
|
||||||
|
const actual = parse(input);
|
||||||
|
|
||||||
|
expect(actual).to.deep.equal(correctResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should balance brackets", () => {
|
||||||
|
const testCases = [{
|
||||||
|
input: "<https://theos.kyriasis.com/~kyrias/stats/archlinux.html>",
|
||||||
|
expected:
|
||||||
|
"<" +
|
||||||
|
"<a href=\"https://theos.kyriasis.com/~kyrias/stats/archlinux.html\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"https://theos.kyriasis.com/~kyrias/stats/archlinux.html" +
|
||||||
|
"</a>" +
|
||||||
|
">"
|
||||||
|
}, {
|
||||||
|
input: "abc (www.example.com)",
|
||||||
|
expected:
|
||||||
|
"abc (" +
|
||||||
|
"<a href=\"http://www.example.com\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"www.example.com" +
|
||||||
|
"</a>" +
|
||||||
|
")"
|
||||||
|
}, {
|
||||||
|
input: "http://example.com/Test_(Page)",
|
||||||
|
expected:
|
||||||
|
"<a href=\"http://example.com/Test_(Page)\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"http://example.com/Test_(Page)" +
|
||||||
|
"</a>"
|
||||||
|
}, {
|
||||||
|
input: "www.example.com/Test_(Page)",
|
||||||
|
expected:
|
||||||
|
"<a href=\"http://www.example.com/Test_(Page)\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"www.example.com/Test_(Page)" +
|
||||||
|
"</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 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:
|
||||||
|
"<span class=\"inline-channel\" role=\"button\" tabindex=\"0\" data-chan=\"#a\">" +
|
||||||
|
"#a" +
|
||||||
|
"</span>"
|
||||||
|
}, {
|
||||||
|
input: "#test",
|
||||||
|
expected:
|
||||||
|
"<span class=\"inline-channel\" role=\"button\" tabindex=\"0\" data-chan=\"#test\">" +
|
||||||
|
"#test" +
|
||||||
|
"</span>"
|
||||||
|
}, {
|
||||||
|
input: "#äöü",
|
||||||
|
expected:
|
||||||
|
"<span class=\"inline-channel\" role=\"button\" tabindex=\"0\" data-chan=\"#äöü\">" +
|
||||||
|
"#äöü" +
|
||||||
|
"</span>"
|
||||||
|
}, {
|
||||||
|
input: "inline #channel text",
|
||||||
|
expected:
|
||||||
|
"inline " +
|
||||||
|
"<span class=\"inline-channel\" role=\"button\" tabindex=\"0\" data-chan=\"#channel\">" +
|
||||||
|
"#channel" +
|
||||||
|
"</span>" +
|
||||||
|
" text"
|
||||||
|
}, {
|
||||||
|
input: "#1,000",
|
||||||
|
expected:
|
||||||
|
"<span class=\"inline-channel\" role=\"button\" tabindex=\"0\" data-chan=\"#1,000\">" +
|
||||||
|
"#1,000" +
|
||||||
|
"</span>"
|
||||||
|
}, {
|
||||||
|
input: "@#a",
|
||||||
|
expected:
|
||||||
|
"@" +
|
||||||
|
"<span class=\"inline-channel\" role=\"button\" tabindex=\"0\" data-chan=\"#a\">" +
|
||||||
|
"#a" +
|
||||||
|
"</span>"
|
||||||
|
}];
|
||||||
|
|
||||||
|
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: "<span class=\"irc-bold\">bold</span>"
|
||||||
|
}, {
|
||||||
|
input: "\x038yellowText",
|
||||||
|
expected: "<span class=\"irc-fg8\">yellowText</span>"
|
||||||
|
}, {
|
||||||
|
input: "\x030,0white,white",
|
||||||
|
expected: "<span class=\"irc-fg0 irc-bg0\">white,white</span>"
|
||||||
|
}, {
|
||||||
|
input: "\x034,8yellowBGredText",
|
||||||
|
expected: "<span class=\"irc-fg4 irc-bg8\">yellowBGredText</span>"
|
||||||
|
}, {
|
||||||
|
input: "\x1ditalic",
|
||||||
|
expected: "<span class=\"irc-italic\">italic</span>"
|
||||||
|
}, {
|
||||||
|
input: "\x1funderline",
|
||||||
|
expected: "<span class=\"irc-underline\">underline</span>"
|
||||||
|
}, {
|
||||||
|
input: "\x02bold\x038yellow\x02nonBold\x03default",
|
||||||
|
expected:
|
||||||
|
"<span class=\"irc-bold\">bold</span>" +
|
||||||
|
"<span class=\"irc-bold irc-fg8\">yellow</span>" +
|
||||||
|
"<span class=\"irc-fg8\">nonBold</span>" +
|
||||||
|
"default"
|
||||||
|
}, {
|
||||||
|
input: "\x02bold\x02 \x02bold\x02",
|
||||||
|
expected:
|
||||||
|
"<span class=\"irc-bold\">bold</span>" +
|
||||||
|
" " +
|
||||||
|
"<span class=\"irc-bold\">bold</span>"
|
||||||
|
}];
|
||||||
|
|
||||||
|
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:
|
||||||
|
"<a href=\"irc://freenode.net/thelounge\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"<span class=\"irc-bold\">irc</span>" +
|
||||||
|
"://" +
|
||||||
|
"<span class=\"irc-italic\">freenode.net</span>" +
|
||||||
|
"/" +
|
||||||
|
"<span class=\"irc-fg4 irc-bg8\">thelounge</span>" +
|
||||||
|
"</a>"
|
||||||
|
}, {
|
||||||
|
input: "\x02#\x038,9thelounge",
|
||||||
|
expected:
|
||||||
|
"<span class=\"inline-channel\" role=\"button\" tabindex=\"0\" data-chan=\"#thelounge\">" +
|
||||||
|
"<span class=\"irc-bold\">#</span>" +
|
||||||
|
"<span class=\"irc-bold irc-fg8 irc-bg9\">thelounge</span>" +
|
||||||
|
"</span>"
|
||||||
|
}];
|
||||||
|
|
||||||
|
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 " +
|
||||||
|
"<span class=\"inline-channel\" role=\"button\" tabindex=\"0\" data-chan=\"#"testa\">" +
|
||||||
|
"<span class=\"irc-fg12\">#"testa</span>" +
|
||||||
|
"</span>"
|
||||||
|
}];
|
||||||
|
|
||||||
|
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.." +
|
||||||
|
"<a href=\"http://example.com\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"http://example.com" +
|
||||||
|
"</a>"
|
||||||
|
}, {
|
||||||
|
input: "like..HTTP://example.com",
|
||||||
|
expected:
|
||||||
|
"like.." +
|
||||||
|
"<a href=\"HTTP://example.com\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"HTTP://example.com" +
|
||||||
|
"</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 channel in fragment", () => {
|
||||||
|
const testCases = [{
|
||||||
|
input: "http://example.com/#hash",
|
||||||
|
expected:
|
||||||
|
"" +
|
||||||
|
"<a href=\"http://example.com/#hash\" target=\"_blank\" rel=\"noopener\">" +
|
||||||
|
"http://example.com/#hash" +
|
||||||
|
"</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 overlap parts", () => {
|
||||||
|
const input = "Url: http://example.com/path Channel: ##channel";
|
||||||
|
const actual = parse(input);
|
||||||
|
|
||||||
|
expect(actual).to.equal(
|
||||||
|
"Url: <a href=\"http://example.com/path\" target=\"_blank\" rel=\"noopener\">http://example.com/path</a> " +
|
||||||
|
"Channel: <span class=\"inline-channel\" role=\"button\" tabindex=\"0\" data-chan=\"##channel\">##channel</span>"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue