Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
/* This file contains the munger functions and rules used by ChatZilla.
* It's generally a bad idea to call munger functions inside ChatZilla for
* anything but munging (chat) output.
*/
/* Constructs a new munger entry, using a regexp or lambda match function, and
* a class name (to be applied by the munger itself) or lambda replace
* function, and the default enabled state and a start priority (used if two
* rules match at the same index), as well as a default tag (when the munger
* adds it based on the class name) name.
*
* Regular Expressions for matching should ensure that the first capturing
* group is the one that contains the matched text. Non-capturing groups, of
* zero-width or otherwise can be used before and after, to ensure the right
* things are matched (e.g. to ensure whitespace before something).
*
* Note that for RegExp matching, the munger will search for the matched text
* (from the first capturing group) from the leftmost point of the entire
* match. This means that if the text that matched the first group occurs in
* any part of the match before the group, the munger will apply to the wrong
* bit. This is not usually a problem, but if it is you should use a
* lambdaMatch function and be sure to return the new style return value,
* which specifically indicates the start.
*
* The lambda match and lambda replace functions have this signature:
* lambdaMatch(text, containerTag, data, mungerEntry)
* lambdaReplace(text, containerTag, data, mungerEntry)
* - text is the entire text to find a match in/that has matched
* - containerTag is the element containing the text (not useful?)
* - data is a generic object containing properties kept throughout
* - mungerEntry is the CMungerEntry object for the munger itself
*
* The lambdaReplace function is expected to do everything needed to put
* |text| into |containerTab| ready for display.
*
* The return value for lambda match functions should be either:
* - (old style) just the text that matched
* (the munger will search for this text, and uses the first match)
* - (new style) an object with properties:
* - start (start index, 0 = first character)
* - text (matched text)
* (note that |text| must start at index |start|)
*
* The return value for lambda replace functions are not used.
*
*/
function CMungerEntry(name, regex, className, priority, startPriority, enable) {
this.name = name;
if (name[0] != ".") {
this.description = getMsg("munger." + name, null, null);
}
this.enabled = typeof enable == "undefined" ? true : enable;
this.enabledDefault = this.enabled;
this.startPriority = startPriority ? startPriority : 0;
this.priority = priority;
if (isinstance(regex, RegExp)) {
this.regex = regex;
} else {
this.lambdaMatch = regex;
}
if (typeof className == "function") {
this.lambdaReplace = className;
} else {
this.className = className;
}
}
function CMunger(textMunger) {
this.entries = [];
this.enabled = true;
if (textMunger) {
this.insertPlainText = textMunger;
}
}
CMunger.prototype = {
insertPlainText(text, containerTag, data) {
let textNode = document.createTextNode(text);
containerTag.appendChild(textNode);
},
getRule(name) {
for (let entry of this.entries) {
if (isinstance(entry, Object) && name in entry) {
return entry[name];
}
}
return null;
},
addRule(name, regex, className, priority, startPriority, enable) {
if (typeof this.entries[priority] != "object") {
this.entries[priority] = {};
}
var entry = new CMungerEntry(
name,
regex,
className,
priority,
startPriority,
enable
);
this.entries[priority][name] = entry;
},
delRule(name) {
for (let entry of this.entries) {
if (isinstance(entry, Object) && name in entry) {
delete entry[name];
}
}
},
munge(text, containerTag, data) {
if (!containerTag) {
containerTag = document.createElementNS(XHTML_NS, "html:span");
}
// Starting from the top, for each valid priority, check all the rules,
// return as soon as something matches.
if (this.enabled) {
for (let i = this.entries.length - 1; i >= 0; i--) {
if (i in this.entries) {
if (this.mungePriority(i, text, containerTag, data)) {
return containerTag;
}
}
}
}
// If nothing matched, we don't have to do anything,
// just insert text (if any).
if (text) {
this.insertPlainText(text, containerTag, data);
}
return containerTag;
},
mungePriority(priority, text, containerTag, data) {
let matches = {};
// Find all the matches in this priority
for (let entry in this.entries[priority]) {
let munger = this.entries[priority][entry];
if (!munger.enabled) {
continue;
}
let match;
if (typeof munger.lambdaMatch == "function") {
let rval = munger.lambdaMatch(text, containerTag, data, munger);
if (typeof rval == "string") {
match = { start: text.indexOf(rval), text: rval };
} else if (typeof rval == "object") {
match = rval;
}
} else {
let ary = text.match(munger.regex);
if (ary != null && ary[1]) {
match = { start: text.indexOf(ary[1]), text: ary[1] };
}
}
if (match && match.start >= 0) {
match.munger = munger;
matches[entry] = match;
}
}
// Find the first matching entry...
let firstMatch = { start: text.length, munger: null };
let firstPriority = 0;
for (let entry in matches) {
// If it matches before the existing first, or at the same spot but
// with a higher start-priority, this is a better match.
if (
matches[entry].start < firstMatch.start ||
(matches[entry].start == firstMatch.start &&
this.entries[priority][entry].startPriority > firstPriority)
) {
firstMatch = matches[entry];
firstPriority = this.entries[priority][entry].startPriority;
}
}
// Replace it.
if (firstMatch.munger) {
let munger = firstMatch.munger;
firstMatch.end = firstMatch.start + firstMatch.text.length;
// Need to deal with the text before the match, if there is any.
if (firstMatch.start > 0) {
let beforeText = text.slice(0, firstMatch.start);
this.munge(beforeText, containerTag, data);
}
if (typeof munger.lambdaReplace == "function") {
// The munger rule itself should take care of munging the 'inside'
// of the match.
munger.lambdaReplace(firstMatch.text, containerTag, data, munger);
} else {
let tag = document.createElementNS(XHTML_NS, "html:span");
tag.setAttribute("class", munger.className + calcClass(data));
// Don't let this rule match again when we recurse.
munger.enabled = false;
this.munge(firstMatch.text, tag, data);
munger.enabled = true;
containerTag.appendChild(tag);
}
this.munge(text.slice(firstMatch.end), containerTag, data);
return containerTag;
}
return null;
},
};
function initMunger() {
/* linkRE: the general URL linkifier regular expression:
*
* - start with whitespace, non-word, or begining-of-line
* - then match:
* - EITHER scheme (word + hyphen), colon, then lots of non-whitespace
* - OR "www" followed by at least 2 sets of:
* - "." plus some non-whitespace, non-"." characters
* - must end match with a word-break
* - include a "/" or "=" beyond break if present
* - end with whitespace, non-word, or end-of-line
*/
client.linkRE =
/(?:\W|^)((?:(\w[\w-]+):[^\s]+|www(\.[^.\s]+){2,})\b[\/=\)]?)(?=\s|\W|$)/;
// Colours: \x03, with optional foreground and background colours
client.colorRE = /(\x03((\d{1,2})(,\d{1,2}|)|))/;
client.whitespaceRE = new RegExp("(\\S{" + client.MAX_WORD_DISPLAY + ",})");
const LOW_PRIORITY = 5;
const NORMAL_PRIORITY = 10;
const HIGH_PRIORITY = 15;
const HIGHER_PRIORITY = 20;
var munger = (client.munger = new CMunger(insertText));
// Special internal munger!
munger.addRule(
".inline-buttons",
/(\[\[.*?\]\])/,
insertInlineButton,
HIGH_PRIORITY,
LOW_PRIORITY,
false
);
munger.addRule(
"quote",
/(``|'')/,
insertQuote,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
"bold",
/(?:[\s(\[]|^)(\*[^*()]*\*)(?:[\s\]).,;!\?]|$)/,
"chatzilla-bold",
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
"underline",
/(?:[\s(\[]|^)(\_[^_()]*\_)(?:[\s\]).,;!\?]|$)/,
"chatzilla-underline",
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
"italic",
/(?:\s|^)(\/[^\/()]*\/)(?:[\s.,]|$)/,
"chatzilla-italic",
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
/* allow () chars inside |code()| blocks */
munger.addRule(
"teletype",
/(?:\s|^)(\|[^|]*\|)(?:[\s.,]|$)/,
"chatzilla-teletype",
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
".mirc-colors",
client.colorRE,
mircChangeColor,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
".mirc-bold",
/(\x02)/,
mircToggleBold,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
".mirc-underline",
/(\x1f)/,
mircToggleUnder,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
".mirc-color-reset",
/(\x0f)/,
mircResetColor,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
".mirc-reverse",
/(\x16)/,
mircReverseColor,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
".ansi-escape-sgr",
/(\x1b\[([\d;]*)m)/,
ansiEscapeSGR,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
"ctrl-char",
/([\x01-\x1f])/,
showCtrlChar,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
"link",
client.linkRE,
insertLink,
NORMAL_PRIORITY,
HIGH_PRIORITY
);
// This has a higher starting priority so as to get it to match before the
// normal link, which won't know about mailto and then fail.
munger.addRule(
".mailto",
/(?:\W|^)((mailto:)?[^:;\\<>\[\]()\'\"\s\u201d]+@[^.<>\[\]()\'\"\s\u201d]+\.[^<>\[\]()\'\"\s\u201d]+)/i,
insertMailToLink,
NORMAL_PRIORITY,
HIGHER_PRIORITY,
false
);
addBugzillaLinkMungerRule(
client.prefs.bugKeyword,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
"channel-link",
/(?:[^\w#]|^)[@%+]?(#[^<>,\[\](){}\"\s\u201d]*[^:,.<>\[\](){}\'\"\s\u201d])/i,
insertChannelLink,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
"talkback-link",
/(?:\W|^)(TB\d{8,}[A-Z]?)(?:\W|$)/,
insertTalkbackLink,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule(
"face",
/((^|\s)(?:[>O]?[B8=:;(xX%][~']?[-^v"]?(?:[)|(PpSs0oO#\?\*\[\]\/\\]|D+)|>[-^v]?\)|[oO9][._][oO9])(\s|$))/,
insertSmiley,
NORMAL_PRIORITY,
NORMAL_PRIORITY
);
munger.addRule("rheet", /(?:\W|^)(rhee+t\!*)(?:\s|$)/i, insertRheet, 10, 10);
munger.addRule(
"word-hyphenator",
client.whitespaceRE,
insertHyphenatedWord,
LOW_PRIORITY,
NORMAL_PRIORITY
);
client.enableColors = client.prefs["munger.colorCodes"];
let branch = Services.prefs.getBranch("extensions.irc.munger.");
for (let entry of munger.entries) {
if (!isinstance(entry, Object)) {
continue;
}
for (let rule in entry) {
if (rule[0] == ".") {
continue;
}
entry[rule].enabled = branch.getBoolPref(rule, true);
}
}
}
function addBugzillaLinkMungerRule(keywords, priority, startPriority) {
client.munger.addRule(
"bugzilla-link",
new RegExp(
"(?:\\W|^)((" +
keywords +
")\\s+(?:#?\\d+|#[^\\s,]{1,20})(?:\\s+comment\\s+#?\\d+)?)",
"i"
),
insertBugzillaLink,
priority,
startPriority
);
}
function insertLink(matchText, containerTag, data, mungerEntry) {
var href;
var linkText;
var trailing;
ary = matchText.match(/([.,?\)]+)$/);
if (ary) {
linkText = RegExp.leftContext;
trailing = ary[1];
// We special-case links that end with (something), often found on wikis
// if "trailing" starts with ) and there's an unclosed ( in the
// "linkText"; then we put the final ) back in
if (trailing.startsWith(")") && linkText.match(/\([^\)]*$/)) {
linkText += ")";
trailing = trailing.slice(1);
}
} else {
linkText = matchText;
}
var ary = linkText.match(/^(\w[\w-]+):/);
if (ary) {
if (!client.checkURLScheme(ary[1])) {
mungerEntry.enabled = false;
client.munger.munge(matchText, containerTag, data);
mungerEntry.enabled = true;
return;
}
href = linkText;
} else {
href = "http://" + linkText;
}
/* This gives callers to the munger control over URLs being logged; the
* channel topic munger uses this, as well as the "is important" checker.
* If either of |dontLogURLs| or |noStateChange| is present and true, we
* don't log.
*/
if (
(!("dontLogURLs" in data) || !data.dontLogURLs) &&
(!("noStateChange" in data) || !data.noStateChange) &&
client.urlLogger
) {
client.urlLogger.append(href);
}
var anchor = document.createElementNS(XHTML_NS, "html:a");
var mircRE = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g;
anchor.setAttribute("href", href.replace(mircRE, ""));
// Carry over formatting.
var otherFormatting = calcClass(data);
if (otherFormatting) {
anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
} else {
anchor.setAttribute("class", "chatzilla-link");
}
anchor.setAttribute("target", "_content");
mungerEntry.enabled = false;
data.inLink = true;
client.munger.munge(linkText, anchor, data);
mungerEntry.enabled = true;
delete data.inLink;
containerTag.appendChild(anchor);
if (trailing) {
insertText(trailing, containerTag, data);
}
}
function insertMailToLink(matchText, containerTag, eventData, mungerEntry) {
if ("inLink" in eventData && eventData.inLink) {
mungerEntry.enabled = false;
client.munger.munge(matchText, containerTag, eventData);
mungerEntry.enabled = true;
return;
}
var href;
if (!matchText.toLowerCase().startsWith("mailto:")) {
href = "mailto:" + matchText;
} else {
href = matchText;
}
var anchor = document.createElementNS(XHTML_NS, "html:a");
var mircRE = /\x1f|\x02|\x0f|\x16|\x03([0-9]{1,2}(,[0-9]{1,2})?)?/g;
anchor.setAttribute("href", href.replace(mircRE, ""));
// Carry over formatting.
var otherFormatting = calcClass(eventData);
if (otherFormatting) {
anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
} else {
anchor.setAttribute("class", "chatzilla-link");
}
//anchor.setAttribute ("target", "_content");
mungerEntry.enabled = false;
eventData.inLink = true;
client.munger.munge(matchText, anchor, eventData);
mungerEntry.enabled = true;
delete eventData.inLink;
containerTag.appendChild(anchor);
}
function insertChannelLink(matchText, containerTag, eventData, mungerEntry) {
if ("inLink" in eventData && eventData.inLink) {
mungerEntry.enabled = false;
client.munger.munge(matchText, containerTag, eventData);
mungerEntry.enabled = true;
return;
}
var bogusChannels = /^#(include|error|define|if|ifdef|else|elsif|endif)$/i;
if (
!("network" in eventData) ||
!eventData.network ||
matchText.search(bogusChannels) != -1
) {
containerTag.appendChild(document.createTextNode(matchText));
return;
}
var linkText = removeColorCodes(matchText);
var encodedLinkText = fromUnicode(linkText, eventData.sourceObject);
var anchor = document.createElementNS(XHTML_NS, "html:a");
anchor.setAttribute("href", eventData.network.getURL(encodedLinkText));
// Carry over formatting.
var otherFormatting = calcClass(eventData);
if (otherFormatting) {
anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
} else {
anchor.setAttribute("class", "chatzilla-link");
}
mungerEntry.enabled = false;
eventData.inLink = true;
client.munger.munge(matchText, anchor, eventData);
mungerEntry.enabled = true;
delete eventData.inLink;
containerTag.appendChild(anchor);
}
function insertTalkbackLink(matchText, containerTag, eventData, mungerEntry) {
if ("inLink" in eventData && eventData.inLink) {
mungerEntry.enabled = false;
client.munger.munge(matchText, containerTag, eventData);
mungerEntry.enabled = true;
return;
}
var anchor = document.createElementNS(XHTML_NS, "html:a");
anchor.setAttribute(
"href",
"search/start.jsp?search=2&type=iid&id=" +
matchText
);
// Carry over formatting.
var otherFormatting = calcClass(eventData);
if (otherFormatting) {
anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
} else {
anchor.setAttribute("class", "chatzilla-link");
}
mungerEntry.enabled = false;
client.munger.munge(matchText, anchor, eventData);
mungerEntry.enabled = true;
containerTag.appendChild(anchor);
}
function insertBugzillaLink(matchText, containerTag, eventData, mungerEntry) {
if ("inLink" in eventData && eventData.inLink) {
mungerEntry.enabled = false;
client.munger.munge(matchText, containerTag, eventData);
mungerEntry.enabled = true;
return;
}
var prefs = client.prefs;
if (eventData.channel) {
prefs = eventData.channel.prefs;
} else if (eventData.network) {
prefs = eventData.network.prefs;
}
var bugURL = prefs.bugURL;
var bugURLcomment = prefs["bugURL.comment"];
if (bugURL.length > 0) {
var idOrAlias = matchText.match(
new RegExp(
"(?:" + client.prefs.bugKeyword + ")\\s+#?(\\d+|[^\\s,]{1,20})",
"i"
)
)[1];
bugURL = bugURL.replace("%s", idOrAlias);
var commentNum = matchText.match(/comment\s+#?(\d+)/i);
if (commentNum) {
/* If the comment is a complete URL, use only that, replacing %1$s
* and %2$s with the bug number and comment number, respectively.
* Otherwise, append the comment preference to the main one,
* replacing just %s in each.
*/
if (bugURLcomment.match(/^\w+:/)) {
bugURL = bugURLcomment;
bugURL = bugURL.replace("%1$s", idOrAlias);
bugURL = bugURL.replace("%2$s", commentNum[1]);
} else {
bugURL += bugURLcomment.replace("%s", commentNum[1]);
}
}
var anchor = document.createElementNS(XHTML_NS, "html:a");
anchor.setAttribute("href", bugURL);
// Carry over formatting.
var otherFormatting = calcClass(eventData);
if (otherFormatting) {
anchor.setAttribute("class", "chatzilla-link " + otherFormatting);
} else {
anchor.setAttribute("class", "chatzilla-link");
}
anchor.setAttribute("target", "_content");
mungerEntry.enabled = false;
eventData.inLink = true;
client.munger.munge(matchText, anchor, eventData);
mungerEntry.enabled = true;
delete eventData.inLink;
containerTag.appendChild(anchor);
} else {
mungerEntry.enabled = false;
client.munger.munge(matchText, containerTag, eventData);
mungerEntry.enabled = true;
}
}
function insertRheet(matchText, containerTag, eventData, mungerEntry) {
if ("inLink" in eventData && eventData.inLink) {
mungerEntry.enabled = false;
client.munger.munge(matchText, containerTag, eventData);
mungerEntry.enabled = true;
return;
}
var anchor = document.createElementNS(XHTML_NS, "html:a");
anchor.setAttribute(
"href",
);
anchor.setAttribute("class", "chatzilla-rheet chatzilla-link");
//anchor.setAttribute ("target", "_content");
insertText(matchText, anchor, eventData);
containerTag.appendChild(anchor);
}
function insertQuote(matchText, containerTag) {
if (matchText == "``") {
containerTag.appendChild(document.createTextNode("\u201c"));
} else {
containerTag.appendChild(document.createTextNode("\u201d"));
}
containerTag.appendChild(document.createElementNS(XHTML_NS, "html:wbr"));
}
function insertSmiley(emoticon, containerTag, eventData, mungerEntry) {
let smilies = {
"face-alien": "\uD83D\uDC7D",
"face-lol": "\uD83D\uDE02",
"face-laugh": "\uD83D\uDE04",
"face-sweat_smile": "\uD83D\uDE05",
"face-innocent": "\uD83D\uDE07",
"face-evil": "\uD83D\uDE08",
"face-wink": "\uD83D\uDE09",
"face-smile": "\uD83D\uDE0A",
"face-cool": "\uD83D\uDE0E",
"face-neutral": "\uD83D\uDE10",
"face-thinking": "\uD83D\uDE14",
"face-confused": "\uD83D\uDE15",
"face-kissing": "\uD83D\uDE17",
"face-tongue": "\uD83D\uDE1B",
"face-worried": "\uD83D\uDE1F",
"face-angry": "\uD83D\uDE20",
"face-cry": "\uD83D\uDE22",
"face-surprised": "\uD83D\uDE2D",
"face-eek": "\uD83D\uDE31",
"face-red": "\uD83D\uDE33",
"face-dizzy": "\uD83D\uDE35",
"face-sad": "\uD83D\uDE41",
"face-rolleyes": "\uD83D\uDE44",
"face-zipped": "\uD83E\uDD10",
"face-rofl": "\uD83E\uDD23",
"face-woozy": "\uD83E\uDD74",
};
let type;
if (emoticon.search(/\>[-^v]?\)/) != -1) {
type = "face-alien";
} else if (emoticon.search(/\>[=:;][-^v]?[(|]|[Xx][-^v]?[(\[]/) != -1) {
type = "face-angry";
} else if (emoticon.search(/[=:;][-^v]?[Ss]/) != -1) {
type = "face-confused";
} else if (emoticon.search(/[B8][-^v]?[)\]]/) != -1) {
type = "face-cool";
} else if (emoticon.search(/[=:;][~'][-^v]?\(/) != -1) {
type = "face-cry";
} else if (emoticon.search(/o[._]O|O[._]o/) != -1) {
type = "face-dizzy";
} else if (emoticon.search(/o[._]o|O[._]O/) != -1) {
type = "face-eek";
} else if (emoticon.search(/\>[=:;][-^v]?D/) != -1) {
type = "face-evil";
} else if (emoticon.search(/O[=:][-^v]?[)]/) != -1) {
type = "face-innocent";
} else if (emoticon.search(/[=:;][-^v]?[*]/) != -1) {
type = "face-kissing";
} else if (emoticon.search(/[=:;][-^v]?DD/) != -1) {
type = "face-lol";
} else if (emoticon.search(/[=:;][-^v]?D/) != -1) {
type = "face-laugh";
} else if (emoticon.search(/\([-^v]?D|[xX][-^v]?D/) != -1) {
type = "face-rofl";
} else if (emoticon.search(/[=:;][-^v]?\|/) != -1) {
type = "face-neutral";
} else if (emoticon.search(/[=:;][-^v]?\?/) != -1) {
type = "face-thinking";
} else if (emoticon.search(/[=:;]"[)\]]/) != -1) {
type = "face-red";
} else if (emoticon.search(/9[._]9/) != -1) {
type = "face-rolleyes";
} else if (emoticon.search(/[=:;][-^v]?[(\[]/) != -1) {
type = "face-sad";
} else if (emoticon.search(/[=:][-^v]?[)]/) != -1) {
type = "face-smile";
} else if (emoticon.search(/[=:;][-^v]?[0oO]/) != -1) {
type = "face-surprised";
} else if (emoticon.search(/[=:][-^v]?[\]]/) != -1) {
type = "face-sweat_smile";
} else if (emoticon.search(/[=:;][-^v]?[pP]/) != -1) {
type = "face-tongue";
} else if (emoticon.search(/;[-^v]?[)\]]/) != -1) {
type = "face-wink";
} else if (emoticon.search(/%[-^v][)\]]/) != -1) {
type = "face-woozy";
} else if (emoticon.search(/[=:;][-^v]?[\/\\]/) != -1) {
type = "face-worried";
} else if (emoticon.search(/[=:;][-^v]?[#]/) != -1) {
type = "face-zipped";
}
let glyph = smilies[type];
if (!glyph) {
// We didn't actually match anything, so it'll be a too-generic match
// from the munger RegExp.
mungerEntry.enabled = false;
client.munger.munge(emoticon, containerTag, eventData);
mungerEntry.enabled = true;
return;
}
// Add spaces to beginning / end where appropriate.
if (emoticon.search(/^\s/) != -1) {
glyph = " " + glyph;
}
if (emoticon.search(/\s$/) != -1) {
glyph = glyph + " ";
}
// Create a span to hold the emoticon.
let span = document.createElementNS(XHTML_NS, "html:span");
span.appendChild(document.createTextNode(glyph));
span.setAttribute("class", "chatzilla-emote-txt");
// Add the title attribute (to show the original text in a tooltip) in case
// the replacement was done incorrectly.
span.setAttribute("title", emoticon);
span.setAttribute("type", type);
containerTag.appendChild(span);
}
function mircChangeColor(colorInfo, containerTag, data) {
/* If colors are disabled, the caller doesn't want colors specifically, or
* the caller doesn't want any state-changing effects, we drop out.
*/
if (
!client.enableColors ||
("noMircColors" in data && data.noMircColors) ||
("noStateChange" in data && data.noStateChange)
) {
return;
}
// Entry 0 will contain all colors specified,
// entry 1 will have any specified foreground color or be undefined,
// entry 2 will have any specified background color or be undefined.
// Valid color codes are 0-99 with 99 having special meaning.
let ary = colorInfo.match(/^\x03(?:(\d\d?)(?:,(\d\d?))?)?/);
// If no foreground color specified or somehow the array does not have 3
// entries then it has invalid syntax.
if (ary.length != 3 || !ary[1]) {
delete data.currFgColor;
delete data.currBgColor;
return;
}
let fgColor = Number(ary[1]);
if (fgColor != 99) {
data.currFgColor = (fgColor % 16).toString().padStart(2, "0");
} else {
delete data.currFgColor;
}
// If no background color then default to 99.
let bgColor = Number(ary[2] || "99");
if (bgColor != 99) {
data.currBgColor = (bgColor % 16).toString().padStart(2, "0");
} else {
delete data.currBgColor;
}
// Only set hasColorInfo if we have something set.
if (fgColor != 99 || bgColor != 99) {
data.hasColorInfo = true;
}
}
function mircToggleBold(colorInfo, containerTag, data) {
if (
!client.enableColors ||
("noMircColors" in data && data.noMircColors) ||
("noStateChange" in data && data.noStateChange)
) {
return;
}
if ("isBold" in data) {
delete data.isBold;
} else {
data.isBold = true;
}
data.hasColorInfo = true;
}
function mircToggleUnder(colorInfo, containerTag, data) {
if (
!client.enableColors ||
("noMircColors" in data && data.noMircColors) ||
("noStateChange" in data && data.noStateChange)
) {
return;
}
if ("isUnderline" in data) {
delete data.isUnderline;
} else {
data.isUnderline = true;
}
data.hasColorInfo = true;
}
function mircResetColor(text, containerTag, data) {
if (
!client.enableColors ||
("noMircColors" in data && data.noMircColors) ||
("noStateChange" in data && data.noStateChange) ||
!("hasColorInfo" in data)
) {
return;
}
removeColorInfo(data);
}
function mircReverseColor(text, containerTag, data) {
if (
!client.enableColors ||
("noMircColors" in data && data.noMircColors) ||
("noStateChange" in data && data.noStateChange)
) {
return;
}
var tempColor = "currFgColor" in data ? data.currFgColor : "";
if ("currBgColor" in data) {
data.currFgColor = data.currBgColor;
} else {
delete data.currFgColor;
}
if (tempColor) {
data.currBgColor = tempColor;
} else {
delete data.currBgColor;
}
data.hasColorInfo = true;
}
function ansiEscapeSGR(text, containerTag, data) {
if (
!client.enableColors ||
("noANSIColors" in data && data.noANSIColors) ||
("noStateChange" in data && data.noStateChange)
) {
return;
}
/* ANSI SGR (Select Graphic Rendition) escape support. Matched text may
* have any number of effects, each a number separated by a semicolon. If
* there are no effects listed, it is treated as effect "0" (reset/normal).
*/
text = text.slice(2, text.length - 3) || "0";
const ansiToMircColor = [
"01",
"05",
"03",
"07",
"02",
"06",
"10",
"15",
"14",
"04",
"09",
"08",
"12",
"13",
"11",
"00",
];
var effects = text.split(";");
for (var i = 0; i < effects.length; i++) {
data.hasColorInfo = true;
switch (Number(effects[i])) {
case 0: // Reset/normal.
removeColorInfo(data);
break;
case 1: // Intensity: bold.
data.isBold = true;
break;
case 3: // Italic: on.
data.isItalic = true;
break;
case 4: // Underline: single.
data.isUnderline = true;
break;
case 9: // Strikethrough: on.
data.isStrikethrough = true;
break;
case 22: // Intensity: normal.
delete data.isBold;
break;
case 23: // Italic: off.
delete data.isItalic;
break;
case 24: // Underline: off.
delete data.isUnderline;
break;
case 29: // Strikethrough: off.
delete data.isStrikethrough;
break;
case 53: // Overline: on.
data.isOverline = true;
break;
case 55: // Overline: off.
delete data.isOverline;
break;
case 30: // FG: Black.
case 31: // FG: Red.
case 32: // FG: Green.
case 33: // FG: Yellow.
case 34: // FG: Blue.
case 35: // FG: Magenta.
case 36: // FG: Cyan.
case 37: // FG: While (light grey).
data.currFgColor = ansiToMircColor[effects[i] - 30];
break;
case 39: // FG: default.
delete data.currFgColor;
break;
case 40: // BG: Black.
case 41: // BG: Red.
case 42: // BG: Green.
case 43: // BG: Yellow.
case 44: // BG: Blue.
case 45: // BG: Magenta.
case 46: // BG: Cyan.
case 47: // BG: While (light grey).
data.currBgColor = ansiToMircColor[effects[i] - 40];
break;
case 49: // BG: default.
delete data.currBgColor;
break;
case 90: // FG: Bright Black (dark grey).
case 91: // FG: Bright Red.
case 92: // FG: Bright Green.
case 93: // FG: Bright Yellow.
case 94: // FG: Bright Blue.
case 95: // FG: Bright Magenta.
case 96: // FG: Bright Cyan.
case 97: // FG: Bright While.
data.currFgColor = ansiToMircColor[effects[i] - 90 + 8];
break;
case 100: // BG: Bright Black (dark grey).
case 101: // BG: Bright Red.
case 102: // BG: Bright Green.
case 103: // BG: Bright Yellow.
case 104: // BG: Bright Blue.
case 105: // BG: Bright Magenta.
case 106: // BG: Bright Cyan.
case 107: // BG: Bright While.
data.currBgColor = ansiToMircColor[effects[i] - 100 + 8];
break;
}
}
}
function removeColorInfo(data) {
delete data.currFgColor;
delete data.currBgColor;
delete data.isBold;
delete data.isItalic;
delete data.isOverline;
delete data.isStrikethrough;
delete data.isUnderline;
delete data.hasColorInfo;
}
function showCtrlChar(c, containerTag) {
var span = document.createElementNS(XHTML_NS, "html:span");
span.setAttribute("class", "chatzilla-control-char");
if (c == "\t") {
containerTag.appendChild(document.createTextNode(c));
return;
}
var ctrlStr = c.charCodeAt(0).toString(16);
if (ctrlStr.length < 2) {
ctrlStr = "0" + ctrlStr;
}
span.appendChild(document.createTextNode("0x" + ctrlStr));
containerTag.appendChild(span);
containerTag.appendChild(document.createElementNS(XHTML_NS, "html:wbr"));
}
function insertText(text, containerTag, data) {
var newClass = "";
if (data && "hasColorInfo" in data) {
newClass = calcClass(data);
}
if (!newClass) {
delete data.hasColorInfo;
}
if (newClass) {
var spanTag = document.createElementNS(XHTML_NS, "html:span");
spanTag.setAttribute("class", newClass);
containerTag.appendChild(spanTag);
containerTag = spanTag;
}
var arg;
while ((arg = text.match(client.whitespaceRE))) {
// Find the start of the match so we can insert the preceding text.
var start = text.indexOf(arg[0]);
if (start > 0) {
containerTag.appendChild(document.createTextNode(text.slice(0, start)));
}
// Process the long word itself.
insertHyphenatedWord(arg[1], containerTag, { dontStyleText: true });
// Continue with the rest of the text.
text = text.slice(start + arg[0].length);
}
// Insert any left-over text on the end.
if (text) {
containerTag.appendChild(document.createTextNode(text));
}
}
function insertHyphenatedWord(longWord, containerTag, data) {
/*
* If there are any wordbreaking characters in |str| within -/+5 characters
* of a |pos| then the word is broken up there. Individual chunks of the
* word are returned as elements of an array.
*/
function splitLongWord(str, pos) {
if (str.length <= pos) {
return [str];
}
let ary = [];
let right = str;
while (right.length > pos) {
/* search for a nice place to break the word, fuzzfactor of +/-5,
* centered around |pos| */
let splitPos = right.substring(pos - 5, pos + 5).search(/[^A-Za-z0-9]/);
splitPos = splitPos != -1 ? pos - 4 + splitPos : pos;
ary.push(right.slice(0, splitPos));
right = right.slice(splitPos);
}
ary.push(right);
return ary;
}
var wordParts = splitLongWord(longWord, client.MAX_WORD_DISPLAY);
if (!data || !("dontStyleText" in data)) {
var newClass = "";
if (data && "hasColorInfo" in data) {
newClass = calcClass(data);
}
if (!newClass) {
delete data.hasColorInfo;
}
if (newClass) {
var spanTag = document.createElementNS(XHTML_NS, "html:span");
spanTag.setAttribute("class", newClass);
containerTag.appendChild(spanTag);
containerTag = spanTag;
}
}
var wbr = document.createElementNS(XHTML_NS, "html:wbr");
for (var i = 0; i < wordParts.length; ++i) {
containerTag.appendChild(document.createTextNode(wordParts[i]));
containerTag.appendChild(wbr.cloneNode(true));
}
}
function insertInlineButton(text, containerTag, data) {
var ary = text.match(/\[\[([^\]]+)\]\[([^\]]+)\]\[([^\]]+)\]\]/);
if (!ary) {
containerTag.appendChild(document.createTextNode(text));
return;
}
var label = ary[1];
var title = ary[2];
var command = ary[3];
var link = document.createElementNS(XHTML_NS, "html:a");
link.setAttribute("href", "x-cz-command:" + encodeURI(command));
link.setAttribute("title", title);
link.setAttribute("class", "chatzilla-link");
link.appendChild(document.createTextNode(label));
containerTag.appendChild(document.createTextNode("["));
containerTag.appendChild(link);
containerTag.appendChild(document.createTextNode("]"));
}
function calcClass(data) {
var className = "";
if ("hasColorInfo" in data) {
if ("currFgColor" in data) {
className += " chatzilla-fg" + data.currFgColor;
}
if ("currBgColor" in data) {
className += " chatzilla-bg" + data.currBgColor;
}
if ("isBold" in data) {
className += " chatzilla-bold";
}
if ("isItalic" in data) {
className += " chatzilla-italic";
}
if ("isOverline" in data) {
className += " chatzilla-overline";
}
if ("isStrikethrough" in data) {
className += " chatzilla-strikethrough";
}
if ("isUnderline" in data) {
className += " chatzilla-underline";
}
}
return className;
}