Skip to content
Snippets Groups Projects
DDMsgReader.js 714 KiB
Newer Older
					writeTxtLines = true;
				}
				break;
			case KEY_PAGE_DOWN: // Next page
				if (retObj.topLineIdx < topLineIdxForLastPage)
				{
					retObj.topLineIdx += pHeight;
					if (retObj.topLineIdx > topLineIdxForLastPage)
						retObj.topLineIdx = topLineIdxForLastPage;
					writeTxtLines = true;
				}
				break;
			case KEY_PAGE_UP: // Previous page
				if (retObj.topLineIdx > 0)
				{
					retObj.topLineIdx -= pHeight;
					if (retObj.topLineIdx < 0)
						retObj.topLineIdx = 0;
					writeTxtLines = true;
				}
				break;
			case KEY_HOME: // First page
				if (retObj.topLineIdx > 0)
				{
					retObj.topLineIdx = 0;
					writeTxtLines = true;
				}
				break;
			case KEY_END: // Last page
				if (retObj.topLineIdx < topLineIdxForLastPage)
				{
					retObj.topLineIdx = topLineIdxForLastPage;
					writeTxtLines = true;
				}
				break;
			default:
				continueOn = false;
				break;
		}
	}
	return retObj;
}

// Displays a Frame on the screen and allows scrolling through it with the up &
// down arrow keys, PageUp, PageDown, HOME, and END.
//
// Parameters:
//  pFrame: A Frame object to display & scroll through
//  pScrollbar: A ScrollBar object associated with the Frame object
//  pTopLineIdx: The index of the text line to display at the top
//  pTxtAttrib: The attribute(s) to apply to the text lines
//  pWriteTxtLines: Boolean - Whether or not to write the text lines (in addition
//                  to doing the message loop).  If false, this will only do the
//                  the message loop.  This parameter is intended as a screen
//                  refresh optimization.
//  pPostWriteCurX: The X location for the cursor after writing the message
//                  lines
//  pPostWriteCurY: The Y location for the cursor after writing the message
//                  lines
//  pScrollUpdateFn: A function that the caller can provide for updating the
//                   scroll position.  This function has one parameter:
//                   - fractionToLastPage: The fraction of the top index divided
//                     by the top index for the last page (basically, the progress
//                     to the last page).
//
// Return value: An object with the following properties:
//               lastKeypress: The last key pressed by the user (a string)
//               topLineIdx: The new top line index of the text lines, in case of scrolling
function scrollFrame(pFrame, pScrollbar, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pPostWriteCurX,
                     pPostWriteCurY, pScrollUpdateFn)
{
	// Variables for the top line index for the last page, scrolling, etc.
	var topLineIdxForLastPage = pFrame.data_height - pFrame.height;
	if (topLineIdxForLastPage < 0)
		topLineIdxForLastPage = 0;

	var retObj = new Object();
	retObj.lastKeypress = "";
	retObj.topLineIdx = pTopLineIdx;

	if (pTopLineIdx > 0)
		pFrame.scrollTo(0, pTopLineIdx);

	var writeTxtLines = pWriteTxtLines;
	if (writeTxtLines)
	{
		pFrame.invalidate(); // Force drawing on the next call to draw() or cycle()
		pFrame.cycle();
		//pFrame.draw();
	}

	var cycleFrame = true;
	var continueOn = true;
	while (continueOn)
	{
		// If we are to write the text lines, then draw the frame.
		// TODO: Do we really need this?  Will this be different from
		// scrollTextLines()?
		//if (writeTxtLines)
		//	pFrame.draw();

		if (cycleFrame)
		{
			// Invalidate the frame to force it to redraw everything, as a
			// workaround to clear the background before writing again
			// TODO: I might want to remove this invalidate() later when
			// Frame is fixed to redraw better on scrolling.
			pFrame.invalidate();
			// Cycle the scrollbar & frame to get them to scroll
		}

		writeTxtLines = false;
		cycleFrame = false;

		// Get a keypress from the user and take action based on it
		console.gotoxy(pPostWriteCurX, pPostWriteCurY);
		retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN);
		switch (retObj.lastKeypress)
		{
			case KEY_UP:
				if (retObj.topLineIdx > 0)
				{
					pFrame.scroll(0, -1);
					--retObj.topLineIdx;
					cycleFrame = true;
					writeTxtLines = true;
				}
				break;
			case KEY_DOWN:
				if (retObj.topLineIdx < topLineIdxForLastPage)
				{
					pFrame.scroll(0, 1);
					cycleFrame = true;
					++retObj.topLineIdx;
					writeTxtLines = true;
				}
				break;
			case KEY_PAGE_DOWN: // Next page
				if (retObj.topLineIdx < topLineIdxForLastPage)
				{
					//pFrame.scroll(0, pFrame.height);
					retObj.topLineIdx += pFrame.height;
					if (retObj.topLineIdx > topLineIdxForLastPage)
						retObj.topLineIdx = topLineIdxForLastPage;
					pFrame.scrollTo(1, retObj.topLineIdx+1);
					cycleFrame = true;
					writeTxtLines = true;
				}
				break;
			case KEY_PAGE_UP: // Previous page
				if (retObj.topLineIdx > 0)
				{
					//pFrame.scroll(0, -(pFrame.height));
					retObj.topLineIdx -= pFrame.height;
					if (retObj.topLineIdx < 0)
						retObj.topLineIdx = 0;
					pFrame.scrollTo(1, retObj.topLineIdx+1);
					cycleFrame = true;
					writeTxtLines = true;
				}
				break;
			case KEY_HOME: // First page
				//pFrame.home();
				pFrame.scrollTo(1, 1);
				cycleFrame = true;
				retObj.topLineIdx = 0;
				break;
			case KEY_END: // Last page
				//pFrame.end();
				pFrame.scrollTo(1, topLineIdxForLastPage+1);
				cycleFrame = true;
				retObj.topLineIdx = topLineIdxForLastPage;
				break;
			default:
				continueOn = false;
				break;
		}
	}

	return retObj;
}

// Finds the (1-based) page number of an item by number (1-based).  If no page
// is found, then the return value will be 0.
//
// Parameters:
//  pItemNum: The item number (1-based)
//  pNumPerPage: The number of items per page
//  pTotoalNum: The total number of items in the list
//  pReverseOrder: Boolean - Whether or not the list is in reverse order.  If not specified,
//                 this will default to false.
//
// Return value: The page number (1-based) of the item number.  If no page is found,
//               the return value will be 0.
function findPageNumOfItemNum(pItemNum, pNumPerPage, pTotalNum, pReverseOrder)
{
   if ((typeof(pItemNum) != "number") || (typeof(pNumPerPage) != "number") || (typeof(pTotalNum) != "number"))
      return 0;
   if ((pItemNum < 1) || (pItemNum > pTotalNum))
      return 0;

   var reverseOrder = (typeof(pReverseOrder) == "boolean" ? pReverseOrder : false);
   var itemPageNum = 0;
   if (reverseOrder)
   {
      var pageNum = 1;
      for (var topNum = pTotalNum; ((topNum > 0) && (itemPageNum == 0)); topNum -= pNumPerPage)
      {
         if ((pItemNum <= topNum) && (pItemNum >= topNum-pNumPerPage+1))
            itemPageNum = pageNum;
         ++pageNum;
      }
   }
   else // Forward order
      itemPageNum = Math.ceil(pItemNum / pNumPerPage);

   return itemPageNum;
}

// This function converts a search mode string to one of the defined search value
// constants.  If the passed-in mode string is unknown, then the return value will
// be SEARCH_NONE (-1).
//
// Parameters:
//  pSearchTypeStr: A string describing a search mode ("keyword_search", "from_name_search",
//                  "to_name_search", "to_user_search", "new_msg_scan", "new_msg_scan_cur_sub",
//                  "new_msg_scan_cur_grp", "new_msg_scan_all", "to_user_new_scan",
//                  "to_user_all_scan")
//
// Return value: An integer representing the search value (SEARCH_KEYWORD,
//               SEARCH_FROM_NAME, SEARCH_TO_NAME_CUR_MSG_AREA,
//               SEARCH_TO_USER_CUR_MSG_AREA), or SEARCH_NONE (-1) if the passed-in
//               search type string is unknown.
function searchTypeStrToVal(pSearchTypeStr)
{
	if (typeof(pSearchTypeStr) != "string")
		return SEARCH_NONE;

	var searchTypeInt = SEARCH_NONE;
	var modeStr = pSearchTypeStr.toLowerCase();
	if (modeStr == "keyword_search")
		searchTypeInt = SEARCH_KEYWORD;
	else if (modeStr == "from_name_search")
		searchTypeInt = SEARCH_FROM_NAME;
	else if (modeStr == "to_name_search")
		searchTypeInt = SEARCH_TO_NAME_CUR_MSG_AREA;
	else if (modeStr == "to_user_search")
		searchTypeInt = SEARCH_TO_USER_CUR_MSG_AREA;
	else if (modeStr == "new_msg_scan")
		searchTypeInt = SEARCH_MSG_NEWSCAN;
	else if (modeStr == "new_msg_scan_cur_sub")
		searchTypeInt = SEARCH_MSG_NEWSCAN_CUR_SUB;
	else if (modeStr == "new_msg_scan_cur_grp")
		searchTypeInt = SEARCH_MSG_NEWSCAN_CUR_GRP;
	else if (modeStr == "new_msg_scan_all")
		searchTypeInt = SEARCH_MSG_NEWSCAN_ALL;
	else if (modeStr == "to_user_new_scan")
		searchTypeInt = SEARCH_TO_USER_NEW_SCAN;
	else if (modeStr == "to_user_new_scan_cur_sub")
		searchTypeInt = SEARCH_TO_USER_NEW_SCAN_CUR_SUB;
	else if (modeStr == "to_user_new_scan_cur_grp")
		searchTypeInt = SEARCH_TO_USER_NEW_SCAN_CUR_GRP;
	else if (modeStr == "to_user_new_scan_all")
		searchTypeInt = SEARCH_TO_USER_NEW_SCAN_ALL;
	else if (modeStr == "to_user_all_scan")
		searchTypeInt = SEARCH_ALL_TO_USER_SCAN;
	return searchTypeInt;
}

// This function converts a search type value to a string description.
//
// Parameters:
//  pSearchType: The search type value to convert
//
// Return value: A string describing the search type value
function searchTypeValToStr(pSearchType)
{
	if (typeof(pSearchType) != "number")
		return "Unknown (not a number)";

	var searchTypeStr = "";
	switch (pSearchType)
	{
		case SEARCH_NONE:
			searchTypeStr = "None (SEARCH_NONE)";
			break;
		case SEARCH_KEYWORD:
			searchTypeStr = "Keyword (SEARCH_KEYWORD)";
			break;
		case SEARCH_FROM_NAME:
			searchTypeStr = "'From' name (SEARCH_FROM_NAME)";
			break;
		case SEARCH_TO_NAME_CUR_MSG_AREA:
			searchTypeStr = "'To' name (SEARCH_TO_NAME_CUR_MSG_AREA)";
			break;
		case SEARCH_TO_USER_CUR_MSG_AREA:
			searchTypeStr = "To you (SEARCH_TO_USER_CUR_MSG_AREA)";
			break;
		case SEARCH_MSG_NEWSCAN:
			searchTypeStr = "New message scan (SEARCH_MSG_NEWSCAN)";
			break;
		case SEARCH_MSG_NEWSCAN_CUR_SUB:
			searchTypeStr = "New in current message area (SEARCH_MSG_NEWSCAN_CUR_SUB)";
			break;
		case SEARCH_MSG_NEWSCAN_CUR_GRP:
			searchTypeStr = "New in current message group (SEARCH_MSG_NEWSCAN_CUR_GRP)";
			break;
		case SEARCH_MSG_NEWSCAN_ALL:
			searchTypeStr = "Newscan - All (SEARCH_MSG_NEWSCAN_ALL)";
			break;
		case SEARCH_TO_USER_NEW_SCAN:
			searchTypeStr = "To You new scan (SEARCH_TO_USER_NEW_SCAN)";
			break;
		case SEARCH_TO_USER_NEW_SCAN_CUR_SUB:
			searchTypeStr = "To You new scan, current sub-board (SEARCH_TO_USER_NEW_SCAN_CUR_SUB)";
			break;
		case SEARCH_TO_USER_NEW_SCAN_CUR_GRP:
			searchTypeStr = "To You new scan, current group (SEARCH_TO_USER_NEW_SCAN_CUR_GRP)";
			break;
		case SEARCH_TO_USER_NEW_SCAN_ALL:
			searchTypeStr = "To You new scan, all sub-boards (SEARCH_TO_USER_NEW_SCAN_ALL)";
			break;
		case SEARCH_ALL_TO_USER_SCAN:
			searchTypeStr = "All To You scan (SEARCH_ALL_TO_USER_SCAN)";
			break;
		default:
			searchTypeStr = "Unknown (" + pSearchType + ")";
			break;
	}
	return searchTypeStr;
}

// This function converts a reader mode string to one of the defined reader mode
// value constants.  If the passed-in mode string is unknown, then the return value
// will be -1.
//
// Parameters:
//  pModeStr: A string describing a reader mode ("read", "reader", "list", "lister")
//
// Return value: An integer representing the reader mode value (READER_MODE_READ,
//               READER_MODE_LIST), or -1 if the passed-in mode string is unknown.
function readerModeStrToVal(pModeStr)
{
   if (typeof(pModeStr) != "string")
      return -1;

   var readerModeInt = -1;
   var modeStr = pModeStr.toLowerCase();
   if ((modeStr == "read") || (modeStr == "reader"))
      readerModeInt = READER_MODE_READ;
   else if ((modeStr == "list") || (modeStr == "lister"))
      readerModeInt = READER_MODE_LIST;
   return readerModeInt;
}

// This function returns a boolean to signify whether or not the user's
// terminal supports both high-ASCII characters and ANSI codes.
function canDoHighASCIIAndANSI()
{
	//return (console.term_supports(USER_ANSI) && (user.settings & USER_NO_EXASCII == 0));
	return (console.term_supports(USER_ANSI));
}

// Searches a given range in an open message base and returns an object with arrays
// containing the message headers (0-based indexed and indexed by message number)
// with the message headers of any found messages.
//
// Parameters:
//  pSubCode: The internal code of the message sub-board
//  pMsgbase: A message base object in which to search messages
//  pSearchType: The type of search to do (one of the SEARCH_ values)
//  pSearchString: The string to search for.
//  pListingPersonalEmailFromUser: Optional boolean - Whether or not we're listing
//                                 personal email sent by the user.  This defaults
//                                 to false.
//  pStartIndex: The starting message index (0-based).  Optional; defaults to 0.
//  pEndIndex: One past the last message index.  Optional; defaults to the total number
//             of messages.
//
// Return value: An object with the following arrays:
//               indexed: A 0-based indexed array of message headers
function searchMsgbase(pSubCode, pMsgbase, pSearchType, pSearchString,
                       pListingPersonalEmailFromUser, pStartIndex, pEndIndex)
{
	var msgHeaders = new Object();
	msgHeaders.indexed = new Array();
	if ((pSubCode != "mail") && ((typeof(pSearchString) != "string") || !searchTypePopulatesSearchResults(pSearchType)))
		return msgHeaders;

	var startMsgIndex = 0;
	var endMsgIndex = pMsgbase.total_msgs;
	if (typeof(pStartIndex) == "number")
	{
		if ((pStartIndex >= 0) && (pStartIndex < pMsgbase.total_msgs))
			startMsgIndex = pStartIndex;
	}
	if (typeof(pEndIndex) == "number")
	{
		if ((pEndIndex >= 0) && (pEndIndex > startMsgIndex) && (pEndIndex <= pMsgbase.total_msgs))
			endMsgIndex = pEndIndex;
	}

	// Define a search function for the message field we're going to search
	var readingPersonalEmailFromUser = (typeof(pListingPersonalEmailFromUser) == "boolean" ? pListingPersonalEmailFromUser : false);
	var matchFn = null;
	switch (pSearchType)
	{
		// It might seem odd to have SEARCH_NONE in here, but it's here because
		// when reading personal email, we need to search for messages only to
		// the current user.
		case SEARCH_NONE:
			if (pSubCode == "mail")
			{
				// Set up the match function slightly differently depending on whether
				// we're looking for mail from the current user or to the current user.
				if (readingPersonalEmailFromUser)
				{
					matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
						var msgText = strip_ctrl(pMsgBase.get_msg_body(true, pMsgHdr.offset));
						return gAllPersonalEmailOptSpecified || msgIsFromUser(pMsgHdr);
					}
				}
				else
				{
					matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
						var msgText = strip_ctrl(pMsgBase.get_msg_body(true, pMsgHdr.offset));
						return gAllPersonalEmailOptSpecified || msgIsToUserByNum(pMsgHdr);
					}
				}
			}
			break;
		case SEARCH_KEYWORD:
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				var msgText = strip_ctrl(pMsgBase.get_msg_body(true, pMsgHdr.offset));
				var keywordFound = ((pMsgHdr.subject.toUpperCase().indexOf(pSearchStr) > -1) || (msgText.toUpperCase().indexOf(pSearchStr) > -1));
				if (pSubBoardCode == "mail")
					return keywordFound && msgIsToUserByNum(pMsgHdr);
				else
					return keywordFound;
			}
			break;
		case SEARCH_FROM_NAME:
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				var fromNameFound = (pMsgHdr.from.toUpperCase() == pSearchStr.toUpperCase());
				if (pSubBoardCode == "mail")
					return fromNameFound && (gAllPersonalEmailOptSpecified || msgIsToUserByNum(pMsgHdr));
				else
					return fromNameFound;
			}
			break;
		case SEARCH_TO_NAME_CUR_MSG_AREA:
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				return (pMsgHdr.to.toUpperCase() == pSearchStr);
			}
			break;
		case SEARCH_TO_USER_CUR_MSG_AREA:
		case SEARCH_ALL_TO_USER_SCAN:
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				// See if the message is not marked as deleted and the 'To' name
				// matches the user's handle, alias, and/or username.
				return (((pMsgHdr.attr & MSG_DELETE) == 0) && userNameHandleAliasMatch(pMsgHdr.to));
			}
			break;
		case SEARCH_TO_USER_NEW_SCAN:
		case SEARCH_TO_USER_NEW_SCAN_CUR_SUB:
		case SEARCH_TO_USER_NEW_SCAN_CUR_GRP:
		case SEARCH_TO_USER_NEW_SCAN_ALL:
			if (pSubCode != "mail")
			{
				// If pStartIndex or pEndIndex aren't specified, then set
				// startMsgIndex to the scan pointer and endMsgIndex to one
				// past the index of the last message in the sub-board
				if (typeof(pStartIndex) != "number")
				{
					// First, write some messages to the log if verbose logging is enabled
					if (gCmdLineArgVals.verboselogging)
					{
						writeToSysAndNodeLog("New-to-user scan for " +
						                     subBoardGrpAndName(pSubCode) + " -- Scan pointer: " +
						                     msg_area.sub[pSubCode].scan_ptr);
					}
					//startMsgIndex = absMsgNumToIdx(pMsgbase, msg_area.sub[pSubCode].last_read);
					startMsgIndex = absMsgNumToIdx(pMsgbase, msg_area.sub[pSubCode].scan_ptr);
					if (startMsgIndex == -1)
					{
						msg_area.sub[pSubCode].scan_ptr = 0;
						startMsgIndex = 0;
					}
						// If this message has been read, then start at the next message.
						var startMsgHeader = pMsgbase.get_msg_header(true, startMsgIndex, false);
						if ((startMsgHeader.attr & MSG_READ) == MSG_READ)
							++startMsgIndex;
					}
				}
				if (typeof(pEndIndex) != "number")
					endMsgIndex = (pMsgbase.total_msgs > 0 ? pMsgbase.total_msgs : 0);
			}
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				// Note: This assumes pSubBoardCode is not "mail" (personal mail).
				// See if the message 'To' name matches the user's handle, alias,
				// and/or username and is not marked as deleted and is unread.
				return (((pMsgHdr.attr & MSG_DELETE) == 0) && ((pMsgHdr.attr & MSG_READ) == 0) && userNameHandleAliasMatch(pMsgHdr.to));
			}
			break;
		case SEARCH_MSG_NEWSCAN:
		case SEARCH_MSG_NEWSCAN_CUR_SUB:
		case SEARCH_MSG_NEWSCAN_CUR_GRP:
		case SEARCH_MSG_NEWSCAN_ALL:
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				// Note: This assumes pSubBoardCode is not "mail" (personal mail).
				// Get the offset of the last read message and compare it with the
				// offset of the given message header
				var lastReadMsgHdr = pMsgBase.get_msg_header(false, msg_area.sub[pSubBoardCode].last_read, false);
				var lastReadMsgOffset = (lastReadMsgHdr != null ? lastReadMsgHdr.offset : 0);
				return (pMsgHdr.offset > lastReadMsgOffset);
			}
			break;
	}
	// Search the messages
	if (matchFn != null)
	{
		for (var msgIdx = startMsgIndex; msgIdx < endMsgIndex; ++msgIdx)
		{
			var msgHeader = pMsgbase.get_msg_header(true, msgIdx, false);
			// I've seen situations where the message header object is null for
			// some reason, so check that before running the search function.
			if (msgHeader != null)
			{
				if (matchFn(pSearchString, msgHeader, pMsgbase, pSubCode))
					msgHeaders.indexed.push(msgHeader);
			}
		}
	}
	return msgHeaders;
}

// Returns whether or not a message is to the current user (either the current
// logged-in user or the user specified by the userNum command-line argument)
// and is not deleted.
//
// Parameters:
//  pMsgHdr: A message header object
//
// Return value: Boolean - Whether or not the message is to the user and is not
//               deleted.
function msgIsToUserByNum(pMsgHdr)
	// Return false if  the message is marked as deleted
	if ((pMsgHdr.attr & MSG_DELETE) == MSG_DELETE)
		return false;

	var msgIsToUser = false;
	// If an alternate user number was specified on the command line, then use that
	// user information.  Otherwise, use the current logged-in user.
	if (gCmdLineArgVals.hasOwnProperty("altUserNum"))
		msgIsToUser = (pMsgHdr.to_ext == gCmdLineArgVals.altUserNum);
	else
		msgIsToUser = (pMsgHdr.to_ext == user.number);
	return msgIsToUser;
// Returns whether or not a message is from the current user (either the current
// logged-in user or the user specified by the userNum command-line argument)
// and is not deleted.
//
// Parameters:
//  pMsgHdr: A message header object
//
// Return value: Boolean - Whether or not the message is from the logged-in user
//               and is not deleted.
function msgIsFromUser(pMsgHdr)
{
	if (typeof(pMsgHdr) != "object")
		return false;
	// Return false if  the message is marked as deleted
	if ((pMsgHdr.attr & MSG_DELETE) == MSG_DELETE)
		return false;

	var isFromUser = false;

	// If an alternate user number was specified on the command line, then use that
	// user information.  Otherwise, use the current logged-in user.

	if (pMsgHdr.hasOwnProperty("from_ext"))
	{
		if (gCmdLineArgVals.hasOwnProperty("altUserNum"))
			isFromUser = (pMsgHdr.from_ext == gCmdLineArgVals.altUserNum);
		else
			isFromUser = (pMsgHdr.from_ext == user.number);
	}
	else
	{
		var hdrFromUpper = pMsgHdr.from.toUpperCase();
		if (gCmdLineArgVals.hasOwnProperty("altUserName") && gCmdLineArgVals.hasOwnProperty("altUserAlias"))
			isFromUser = ((hdrFromUpper == gCmdLineArgVals.altUserAlias.toUpperCase()) || (hdrFromUpper == gCmdLineArgVals.altUserName.toUpperCase()));
		else
			isFromUser = ((hdrFromUpper == user.alias.toUpperCase()) || (hdrFromUpper == user.name.toUpperCase()));
	}

	return isFromUser;
}

/////////////////////////////////////////////////////////////////////////
// Functions for converting other BBS color codes to Synchronet attribute codes

// Converts WWIV attribute codes to Synchronet attribute codes.
//
// Parameters:
//  pText: A string containing the text to convert
//
// Return value: The text with the color codes converted
function WWIVAttrsToSyncAttrs(pText)
{
	// First, see if the text has any WWIV-style attribute codes at
	// all.  We'll be performing a bunch of search & replace commands,
	// so we don't want to do all that work for nothing.. :)
	if (/\x03[0-9]/.test(pText))
	{
		var text = pText.replace(/\x030/g, "\1n");        // Normal
		text = text.replace(/\x031/g, "\1n\1c\1h");     // Bright cyan
		text = text.replace(/\x032/g, "\1n\1y\1h");     // Bright yellow
		text = text.replace(/\x033/g, "\1n\1m");         // Magenta
		text = text.replace(/\x034/g, "\1n\1h\1w\1" + "4"); // Bright white on blue
		text = text.replace(/\x035/g, "\1n\1g");         // Green
		text = text.replace(/\x036/g, "\1h\1r\1i");     // Bright red, blinking
		text = text.replace(/\x037/g, "\1n\1h\1b");     // Bright blue
		text = text.replace(/\x038/g, "\1n\1b");         // Blue
		text = text.replace(/\x039/g, "\1n\1c");         // Cyan
		return text;
	}
	else
		return pText; // No WWIV-style color attribute found, so just return the text.
}

// Converts PCBoard attribute codes to Synchronet attribute codes.
//
// Parameters:
//  pText: A string containing the text to convert
//
// Return value: The text with the color codes converted
function PCBoardAttrsToSyncAttrs(pText)
{
	// First, see if the text has any PCBoard-style attribute codes at
	// all.  We'll be performing a bunch of search & replace commands,
	// so we don't want to do all that work for nothing.. :)
	if (/@[xX][0-9A-Fa-f]{2}/.test(pText))
	{
		// Black background
		var text = pText.replace(/@[xX]00/g, "\1n\1k\1" + "0"); // Black on black
		text = text.replace(/@[xX]01/g, "\1n\1b\1" + "0"); // Blue on black
		text = text.replace(/@[xX]02/g, "\1n\1g\1" + "0"); // Green on black
		text = text.replace(/@[xX]03/g, "\1n\1c\1" + "0"); // Cyan on black
		text = text.replace(/@[xX]04/g, "\1n\1r\1" + "0"); // Red on black
		text = text.replace(/@[xX]05/g, "\1n\1m\1" + "0"); // Magenta on black
		text = text.replace(/@[xX]06/g, "\1n\1y\1" + "0"); // Yellow/brown on black
		text = text.replace(/@[xX]07/g, "\1n\1w\1" + "0"); // White on black
		text = text.replace(/@[xX]08/g, "\1n\1w\1" + "0"); // White on black
		text = text.replace(/@[xX]09/g, "\1n\1w\1" + "0"); // White on black
		text = text.replace(/@[xX]08/g, "\1h\1k\1" + "0"); // Bright black on black
		text = text.replace(/@[xX]09/g, "\1h\1b\1" + "0"); // Bright blue on black
		text = text.replace(/@[xX]0[Aa]/g, "\1h\1g\1" + "0"); // Bright green on black
		text = text.replace(/@[xX]0[Bb]/g, "\1h\1c\1" + "0"); // Bright cyan on black
		text = text.replace(/@[xX]0[Cc]/g, "\1h\1r\1" + "0"); // Bright red on black
		text = text.replace(/@[xX]0[Dd]/g, "\1h\1m\1" + "0"); // Bright magenta on black
		text = text.replace(/@[xX]0[Ee]/g, "\1h\1y\1" + "0"); // Bright yellow on black
		text = text.replace(/@[xX]0[Ff]/g, "\1h\1w\1" + "0"); // Bright white on black
		text = text.replace(/@[xX]10/g, "\1n\1k\1" + "4"); // Black on blue
		text = text.replace(/@[xX]11/g, "\1n\1b\1" + "4"); // Blue on blue
		text = text.replace(/@[xX]12/g, "\1n\1g\1" + "4"); // Green on blue
		text = text.replace(/@[xX]13/g, "\1n\1c\1" + "4"); // Cyan on blue
		text = text.replace(/@[xX]14/g, "\1n\1r\1" + "4"); // Red on blue
		text = text.replace(/@[xX]15/g, "\1n\1m\1" + "4"); // Magenta on blue
		text = text.replace(/@[xX]16/g, "\1n\1y\1" + "4"); // Yellow/brown on blue
		text = text.replace(/@[xX]17/g, "\1n\1w\1" + "4"); // White on blue
		text = text.replace(/@[xX]18/g, "\1h\1k\1" + "4"); // Bright black on blue
		text = text.replace(/@[xX]19/g, "\1h\1b\1" + "4"); // Bright blue on blue
		text = text.replace(/@[xX]1[Aa]/g, "\1h\1g\1" + "4"); // Bright green on blue
		text = text.replace(/@[xX]1[Bb]/g, "\1h\1c\1" + "4"); // Bright cyan on blue
		text = text.replace(/@[xX]1[Cc]/g, "\1h\1r\1" + "4"); // Bright red on blue
		text = text.replace(/@[xX]1[Dd]/g, "\1h\1m\1" + "4"); // Bright magenta on blue
		text = text.replace(/@[xX]1[Ee]/g, "\1h\1y\1" + "4"); // Bright yellow on blue
		text = text.replace(/@[xX]1[Ff]/g, "\1h\1w\1" + "4"); // Bright white on blue
		text = text.replace(/@[xX]20/g, "\1n\1k\1" + "2"); // Black on green
		text = text.replace(/@[xX]21/g, "\1n\1b\1" + "2"); // Blue on green
		text = text.replace(/@[xX]22/g, "\1n\1g\1" + "2"); // Green on green
		text = text.replace(/@[xX]23/g, "\1n\1c\1" + "2"); // Cyan on green
		text = text.replace(/@[xX]24/g, "\1n\1r\1" + "2"); // Red on green
		text = text.replace(/@[xX]25/g, "\1n\1m\1" + "2"); // Magenta on green
		text = text.replace(/@[xX]26/g, "\1n\1y\1" + "2"); // Yellow/brown on green
		text = text.replace(/@[xX]27/g, "\1n\1w\1" + "2"); // White on green
		text = text.replace(/@[xX]28/g, "\1h\1k\1" + "2"); // Bright black on green
		text = text.replace(/@[xX]29/g, "\1h\1b\1" + "2"); // Bright blue on green
		text = text.replace(/@[xX]2[Aa]/g, "\1h\1g\1" + "2"); // Bright green on green
		text = text.replace(/@[xX]2[Bb]/g, "\1h\1c\1" + "2"); // Bright cyan on green
		text = text.replace(/@[xX]2[Cc]/g, "\1h\1r\1" + "2"); // Bright red on green
		text = text.replace(/@[xX]2[Dd]/g, "\1h\1m\1" + "2"); // Bright magenta on green
		text = text.replace(/@[xX]2[Ee]/g, "\1h\1y\1" + "2"); // Bright yellow on green
		text = text.replace(/@[xX]2[Ff]/g, "\1h\1w\1" + "2"); // Bright white on green
		text = text.replace(/@[xX]30/g, "\1n\1k\1" + "6"); // Black on cyan
		text = text.replace(/@[xX]31/g, "\1n\1b\1" + "6"); // Blue on cyan
		text = text.replace(/@[xX]32/g, "\1n\1g\1" + "6"); // Green on cyan
		text = text.replace(/@[xX]33/g, "\1n\1c\1" + "6"); // Cyan on cyan
		text = text.replace(/@[xX]34/g, "\1n\1r\1" + "6"); // Red on cyan
		text = text.replace(/@[xX]35/g, "\1n\1m\1" + "6"); // Magenta on cyan
		text = text.replace(/@[xX]36/g, "\1n\1y\1" + "6"); // Yellow/brown on cyan
		text = text.replace(/@[xX]37/g, "\1n\1w\1" + "6"); // White on cyan
		text = text.replace(/@[xX]38/g, "\1h\1k\1" + "6"); // Bright black on cyan
		text = text.replace(/@[xX]39/g, "\1h\1b\1" + "6"); // Bright blue on cyan
		text = text.replace(/@[xX]3[Aa]/g, "\1h\1g\1" + "6"); // Bright green on cyan
		text = text.replace(/@[xX]3[Bb]/g, "\1h\1c\1" + "6"); // Bright cyan on cyan
		text = text.replace(/@[xX]3[Cc]/g, "\1h\1r\1" + "6"); // Bright red on cyan
		text = text.replace(/@[xX]3[Dd]/g, "\1h\1m\1" + "6"); // Bright magenta on cyan
		text = text.replace(/@[xX]3[Ee]/g, "\1h\1y\1" + "6"); // Bright yellow on cyan
		text = text.replace(/@[xX]3[Ff]/g, "\1h\1w\1" + "6"); // Bright white on cyan
		text = text.replace(/@[xX]40/g, "\1n\1k\1" + "1"); // Black on red
		text = text.replace(/@[xX]41/g, "\1n\1b\1" + "1"); // Blue on red
		text = text.replace(/@[xX]42/g, "\1n\1g\1" + "1"); // Green on red
		text = text.replace(/@[xX]43/g, "\1n\1c\1" + "1"); // Cyan on red
		text = text.replace(/@[xX]44/g, "\1n\1r\1" + "1"); // Red on red
		text = text.replace(/@[xX]45/g, "\1n\1m\1" + "1"); // Magenta on red
		text = text.replace(/@[xX]46/g, "\1n\1y\1" + "1"); // Yellow/brown on red
		text = text.replace(/@[xX]47/g, "\1n\1w\1" + "1"); // White on red
		text = text.replace(/@[xX]48/g, "\1h\1k\1" + "1"); // Bright black on red
		text = text.replace(/@[xX]49/g, "\1h\1b\1" + "1"); // Bright blue on red
		text = text.replace(/@[xX]4[Aa]/g, "\1h\1g\1" + "1"); // Bright green on red
		text = text.replace(/@[xX]4[Bb]/g, "\1h\1c\1" + "1"); // Bright cyan on red
		text = text.replace(/@[xX]4[Cc]/g, "\1h\1r\1" + "1"); // Bright red on red
		text = text.replace(/@[xX]4[Dd]/g, "\1h\1m\1" + "1"); // Bright magenta on red
		text = text.replace(/@[xX]4[Ee]/g, "\1h\1y\1" + "1"); // Bright yellow on red
		text = text.replace(/@[xX]4[Ff]/g, "\1h\1w\1" + "1"); // Bright white on red
		text = text.replace(/@[xX]50/g, "\1n\1k\1" + "5"); // Black on magenta
		text = text.replace(/@[xX]51/g, "\1n\1b\1" + "5"); // Blue on magenta
		text = text.replace(/@[xX]52/g, "\1n\1g\1" + "5"); // Green on magenta
		text = text.replace(/@[xX]53/g, "\1n\1c\1" + "5"); // Cyan on magenta
		text = text.replace(/@[xX]54/g, "\1n\1r\1" + "5"); // Red on magenta
		text = text.replace(/@[xX]55/g, "\1n\1m\1" + "5"); // Magenta on magenta
		text = text.replace(/@[xX]56/g, "\1n\1y\1" + "5"); // Yellow/brown on magenta
		text = text.replace(/@[xX]57/g, "\1n\1w\1" + "5"); // White on magenta
		text = text.replace(/@[xX]58/g, "\1h\1k\1" + "5"); // Bright black on magenta
		text = text.replace(/@[xX]59/g, "\1h\1b\1" + "5"); // Bright blue on magenta
		text = text.replace(/@[xX]5[Aa]/g, "\1h\1g\1" + "5"); // Bright green on magenta
		text = text.replace(/@[xX]5[Bb]/g, "\1h\1c\1" + "5"); // Bright cyan on magenta
		text = text.replace(/@[xX]5[Cc]/g, "\1h\1r\1" + "5"); // Bright red on magenta
		text = text.replace(/@[xX]5[Dd]/g, "\1h\1m\1" + "5"); // Bright magenta on magenta
		text = text.replace(/@[xX]5[Ee]/g, "\1h\1y\1" + "5"); // Bright yellow on magenta
		text = text.replace(/@[xX]5[Ff]/g, "\1h\1w\1" + "5"); // Bright white on magenta
		text = text.replace(/@[xX]60/g, "\1n\1k\1" + "3"); // Black on brown
		text = text.replace(/@[xX]61/g, "\1n\1b\1" + "3"); // Blue on brown
		text = text.replace(/@[xX]62/g, "\1n\1g\1" + "3"); // Green on brown
		text = text.replace(/@[xX]63/g, "\1n\1c\1" + "3"); // Cyan on brown
		text = text.replace(/@[xX]64/g, "\1n\1r\1" + "3"); // Red on brown
		text = text.replace(/@[xX]65/g, "\1n\1m\1" + "3"); // Magenta on brown
		text = text.replace(/@[xX]66/g, "\1n\1y\1" + "3"); // Yellow/brown on brown
		text = text.replace(/@[xX]67/g, "\1n\1w\1" + "3"); // White on brown
		text = text.replace(/@[xX]68/g, "\1h\1k\1" + "3"); // Bright black on brown
		text = text.replace(/@[xX]69/g, "\1h\1b\1" + "3"); // Bright blue on brown
		text = text.replace(/@[xX]6[Aa]/g, "\1h\1g\1" + "3"); // Bright breen on brown
		text = text.replace(/@[xX]6[Bb]/g, "\1h\1c\1" + "3"); // Bright cyan on brown
		text = text.replace(/@[xX]6[Cc]/g, "\1h\1r\1" + "3"); // Bright red on brown
		text = text.replace(/@[xX]6[Dd]/g, "\1h\1m\1" + "3"); // Bright magenta on brown
		text = text.replace(/@[xX]6[Ee]/g, "\1h\1y\1" + "3"); // Bright yellow on brown
		text = text.replace(/@[xX]6[Ff]/g, "\1h\1w\1" + "3"); // Bright white on brown
		text = text.replace(/@[xX]70/g, "\1n\1k\1" + "7"); // Black on white
		text = text.replace(/@[xX]71/g, "\1n\1b\1" + "7"); // Blue on white
		text = text.replace(/@[xX]72/g, "\1n\1g\1" + "7"); // Green on white
		text = text.replace(/@[xX]73/g, "\1n\1c\1" + "7"); // Cyan on white
		text = text.replace(/@[xX]74/g, "\1n\1r\1" + "7"); // Red on white
		text = text.replace(/@[xX]75/g, "\1n\1m\1" + "7"); // Magenta on white
		text = text.replace(/@[xX]76/g, "\1n\1y\1" + "7"); // Yellow/brown on white
		text = text.replace(/@[xX]77/g, "\1n\1w\1" + "7"); // White on white
		text = text.replace(/@[xX]78/g, "\1h\1k\1" + "7"); // Bright black on white
		text = text.replace(/@[xX]79/g, "\1h\1b\1" + "7"); // Bright blue on white
		text = text.replace(/@[xX]7[Aa]/g, "\1h\1g\1" + "7"); // Bright green on white
		text = text.replace(/@[xX]7[Bb]/g, "\1h\1c\1" + "7"); // Bright cyan on white
		text = text.replace(/@[xX]7[Cc]/g, "\1h\1r\1" + "7"); // Bright red on white
		text = text.replace(/@[xX]7[Dd]/g, "\1h\1m\1" + "7"); // Bright magenta on white
		text = text.replace(/@[xX]7[Ee]/g, "\1h\1y\1" + "7"); // Bright yellow on white
		text = text.replace(/@[xX]7[Ff]/g, "\1h\1w\1" + "7"); // Bright white on white
		text = text.replace(/@[xX]80/g, "\1n\1k\1" + "0\1i"); // Blinking black on black
		text = text.replace(/@[xX]81/g, "\1n\1b\1" + "0\1i"); // Blinking blue on black
		text = text.replace(/@[xX]82/g, "\1n\1g\1" + "0\1i"); // Blinking green on black
		text = text.replace(/@[xX]83/g, "\1n\1c\1" + "0\1i"); // Blinking cyan on black
		text = text.replace(/@[xX]84/g, "\1n\1r\1" + "0\1i"); // Blinking red on black
		text = text.replace(/@[xX]85/g, "\1n\1m\1" + "0\1i"); // Blinking magenta on black
		text = text.replace(/@[xX]86/g, "\1n\1y\1" + "0\1i"); // Blinking yellow/brown on black
		text = text.replace(/@[xX]87/g, "\1n\1w\1" + "0\1i"); // Blinking white on black
		text = text.replace(/@[xX]88/g, "\1h\1k\1" + "0\1i"); // Blinking bright black on black
		text = text.replace(/@[xX]89/g, "\1h\1b\1" + "0\1i"); // Blinking bright blue on black
		text = text.replace(/@[xX]8[Aa]/g, "\1h\1g\1" + "0\1i"); // Blinking bright green on black
		text = text.replace(/@[xX]8[Bb]/g, "\1h\1c\1" + "0\1i"); // Blinking bright cyan on black
		text = text.replace(/@[xX]8[Cc]/g, "\1h\1r\1" + "0\1i"); // Blinking bright red on black
		text = text.replace(/@[xX]8[Dd]/g, "\1h\1m\1" + "0\1i"); // Blinking bright magenta on black
		text = text.replace(/@[xX]8[Ee]/g, "\1h\1y\1" + "0\1i"); // Blinking bright yellow on black
		text = text.replace(/@[xX]8[Ff]/g, "\1h\1w\1" + "0\1i"); // Blinking bright white on black
		text = text.replace(/@[xX]90/g, "\1n\1k\1" + "4\1i"); // Blinking black on blue
		text = text.replace(/@[xX]91/g, "\1n\1b\1" + "4\1i"); // Blinking blue on blue
		text = text.replace(/@[xX]92/g, "\1n\1g\1" + "4\1i"); // Blinking green on blue
		text = text.replace(/@[xX]93/g, "\1n\1c\1" + "4\1i"); // Blinking cyan on blue
		text = text.replace(/@[xX]94/g, "\1n\1r\1" + "4\1i"); // Blinking red on blue
		text = text.replace(/@[xX]95/g, "\1n\1m\1" + "4\1i"); // Blinking magenta on blue
		text = text.replace(/@[xX]96/g, "\1n\1y\1" + "4\1i"); // Blinking yellow/brown on blue
		text = text.replace(/@[xX]97/g, "\1n\1w\1" + "4\1i"); // Blinking white on blue
		text = text.replace(/@[xX]98/g, "\1h\1k\1" + "4\1i"); // Blinking bright black on blue
		text = text.replace(/@[xX]99/g, "\1h\1b\1" + "4\1i"); // Blinking bright blue on blue
		text = text.replace(/@[xX]9[Aa]/g, "\1h\1g\1" + "4\1i"); // Blinking bright green on blue
		text = text.replace(/@[xX]9[Bb]/g, "\1h\1c\1" + "4\1i"); // Blinking bright cyan on blue
		text = text.replace(/@[xX]9[Cc]/g, "\1h\1r\1" + "4\1i"); // Blinking bright red on blue
		text = text.replace(/@[xX]9[Dd]/g, "\1h\1m\1" + "4\1i"); // Blinking bright magenta on blue
		text = text.replace(/@[xX]9[Ee]/g, "\1h\1y\1" + "4\1i"); // Blinking bright yellow on blue
		text = text.replace(/@[xX]9[Ff]/g, "\1h\1w\1" + "4\1i"); // Blinking bright white on blue
		text = text.replace(/@[xX][Aa]0/g, "\1n\1k\1" + "2\1i"); // Blinking black on green
		text = text.replace(/@[xX][Aa]1/g, "\1n\1b\1" + "2\1i"); // Blinking blue on green
		text = text.replace(/@[xX][Aa]2/g, "\1n\1g\1" + "2\1i"); // Blinking green on green
		text = text.replace(/@[xX][Aa]3/g, "\1n\1c\1" + "2\1i"); // Blinking cyan on green
		text = text.replace(/@[xX][Aa]4/g, "\1n\1r\1" + "2\1i"); // Blinking red on green
		text = text.replace(/@[xX][Aa]5/g, "\1n\1m\1" + "2\1i"); // Blinking magenta on green
		text = text.replace(/@[xX][Aa]6/g, "\1n\1y\1" + "2\1i"); // Blinking yellow/brown on green
		text = text.replace(/@[xX][Aa]7/g, "\1n\1w\1" + "2\1i"); // Blinking white on green
		text = text.replace(/@[xX][Aa]8/g, "\1h\1k\1" + "2\1i"); // Blinking bright black on green
		text = text.replace(/@[xX][Aa]9/g, "\1h\1b\1" + "2\1i"); // Blinking bright blue on green
		text = text.replace(/@[xX][Aa][Aa]/g, "\1h\1g\1" + "2\1i"); // Blinking bright green on green
		text = text.replace(/@[xX][Aa][Bb]/g, "\1h\1c\1" + "2\1i"); // Blinking bright cyan on green
		text = text.replace(/@[xX][Aa][Cc]/g, "\1h\1r\1" + "2\1i"); // Blinking bright red on green
		text = text.replace(/@[xX][Aa][Dd]/g, "\1h\1m\1" + "2\1i"); // Blinking bright magenta on green
		text = text.replace(/@[xX][Aa][Ee]/g, "\1h\1y\1" + "2\1i"); // Blinking bright yellow on green
		text = text.replace(/@[xX][Aa][Ff]/g, "\1h\1w\1" + "2\1i"); // Blinking bright white on green
		text = text.replace(/@[xX][Bb]0/g, "\1n\1k\1" + "6\1i"); // Blinking black on cyan
		text = text.replace(/@[xX][Bb]1/g, "\1n\1b\1" + "6\1i"); // Blinking blue on cyan
		text = text.replace(/@[xX][Bb]2/g, "\1n\1g\1" + "6\1i"); // Blinking green on cyan
		text = text.replace(/@[xX][Bb]3/g, "\1n\1c\1" + "6\1i"); // Blinking cyan on cyan
		text = text.replace(/@[xX][Bb]4/g, "\1n\1r\1" + "6\1i"); // Blinking red on cyan
		text = text.replace(/@[xX][Bb]5/g, "\1n\1m\1" + "6\1i"); // Blinking magenta on cyan
		text = text.replace(/@[xX][Bb]6/g, "\1n\1y\1" + "6\1i"); // Blinking yellow/brown on cyan
		text = text.replace(/@[xX][Bb]7/g, "\1n\1w\1" + "6\1i"); // Blinking white on cyan
		text = text.replace(/@[xX][Bb]8/g, "\1h\1k\1" + "6\1i"); // Blinking bright black on cyan
		text = text.replace(/@[xX][Bb]9/g, "\1h\1b\1" + "6\1i"); // Blinking bright blue on cyan
		text = text.replace(/@[xX][Bb][Aa]/g, "\1h\1g\1" + "6\1i"); // Blinking bright green on cyan
		text = text.replace(/@[xX][Bb][Bb]/g, "\1h\1c\1" + "6\1i"); // Blinking bright cyan on cyan
		text = text.replace(/@[xX][Bb][Cc]/g, "\1h\1r\1" + "6\1i"); // Blinking bright red on cyan
		text = text.replace(/@[xX][Bb][Dd]/g, "\1h\1m\1" + "6\1i"); // Blinking bright magenta on cyan
		text = text.replace(/@[xX][Bb][Ee]/g, "\1h\1y\1" + "6\1i"); // Blinking bright yellow on cyan
		text = text.replace(/@[xX][Bb][Ff]/g, "\1h\1w\1" + "6\1i"); // Blinking bright white on cyan
		text = text.replace(/@[xX][Cc]0/g, "\1n\1k\1" + "1\1i"); // Blinking black on red
		text = text.replace(/@[xX][Cc]1/g, "\1n\1b\1" + "1\1i"); // Blinking blue on red
		text = text.replace(/@[xX][Cc]2/g, "\1n\1g\1" + "1\1i"); // Blinking green on red
		text = text.replace(/@[xX][Cc]3/g, "\1n\1c\1" + "1\1i"); // Blinking cyan on red
		text = text.replace(/@[xX][Cc]4/g, "\1n\1r\1" + "1\1i"); // Blinking red on red
		text = text.replace(/@[xX][Cc]5/g, "\1n\1m\1" + "1\1i"); // Blinking magenta on red
		text = text.replace(/@[xX][Cc]6/g, "\1n\1y\1" + "1\1i"); // Blinking yellow/brown on red
		text = text.replace(/@[xX][Cc]7/g, "\1n\1w\1" + "1\1i"); // Blinking white on red
		text = text.replace(/@[xX][Cc]8/g, "\1h\1k\1" + "1\1i"); // Blinking bright black on red
		text = text.replace(/@[xX][Cc]9/g, "\1h\1b\1" + "1\1i"); // Blinking bright blue on red
		text = text.replace(/@[xX][Cc][Aa]/g, "\1h\1g\1" + "1\1i"); // Blinking bright green on red
		text = text.replace(/@[xX][Cc][Bb]/g, "\1h\1c\1" + "1\1i"); // Blinking bright cyan on red
		text = text.replace(/@[xX][Cc][Cc]/g, "\1h\1r\1" + "1\1i"); // Blinking bright red on red
		text = text.replace(/@[xX][Cc][Dd]/g, "\1h\1m\1" + "1\1i"); // Blinking bright magenta on red
		text = text.replace(/@[xX][Cc][Ee]/g, "\1h\1y\1" + "1\1i"); // Blinking bright yellow on red
		text = text.replace(/@[xX][Cc][Ff]/g, "\1h\1w\1" + "1\1i"); // Blinking bright white on red
		text = text.replace(/@[xX][Dd]0/g, "\1n\1k\1" + "5\1i"); // Blinking black on magenta
		text = text.replace(/@[xX][Dd]1/g, "\1n\1b\1" + "5\1i"); // Blinking blue on magenta
		text = text.replace(/@[xX][Dd]2/g, "\1n\1g\1" + "5\1i"); // Blinking green on magenta
		text = text.replace(/@[xX][Dd]3/g, "\1n\1c\1" + "5\1i"); // Blinking cyan on magenta
		text = text.replace(/@[xX][Dd]4/g, "\1n\1r\1" + "5\1i"); // Blinking red on magenta
		text = text.replace(/@[xX][Dd]5/g, "\1n\1m\1" + "5\1i"); // Blinking magenta on magenta
		text = text.replace(/@[xX][Dd]6/g, "\1n\1y\1" + "5\1i"); // Blinking yellow/brown on magenta
		text = text.replace(/@[xX][Dd]7/g, "\1n\1w\1" + "5\1i"); // Blinking white on magenta
		text = text.replace(/@[xX][Dd]8/g, "\1h\1k\1" + "5\1i"); // Blinking bright black on magenta
		text = text.replace(/@[xX][Dd]9/g, "\1h\1b\1" + "5\1i"); // Blinking bright blue on magenta
		text = text.replace(/@[xX][Dd][Aa]/g, "\1h\1g\1" + "5\1i"); // Blinking bright green on magenta
		text = text.replace(/@[xX][Dd][Bb]/g, "\1h\1c\1" + "5\1i"); // Blinking bright cyan on magenta
		text = text.replace(/@[xX][Dd][Cc]/g, "\1h\1r\1" + "5\1i"); // Blinking bright red on magenta
		text = text.replace(/@[xX][Dd][Dd]/g, "\1h\1m\1" + "5\1i"); // Blinking bright magenta on magenta
		text = text.replace(/@[xX][Dd][Ee]/g, "\1h\1y\1" + "5\1i"); // Blinking bright yellow on magenta
		text = text.replace(/@[xX][Dd][Ff]/g, "\1h\1w\1" + "5\1i"); // Blinking bright white on magenta
		text = text.replace(/@[xX][Ee]0/g, "\1n\1k\1" + "3\1i"); // Blinking black on brown
		text = text.replace(/@[xX][Ee]1/g, "\1n\1b\1" + "3\1i"); // Blinking blue on brown
		text = text.replace(/@[xX][Ee]2/g, "\1n\1g\1" + "3\1i"); // Blinking green on brown
		text = text.replace(/@[xX][Ee]3/g, "\1n\1c\1" + "3\1i"); // Blinking cyan on brown
		text = text.replace(/@[xX][Ee]4/g, "\1n\1r\1" + "3\1i"); // Blinking red on brown
		text = text.replace(/@[xX][Ee]5/g, "\1n\1m\1" + "3\1i"); // Blinking magenta on brown
		text = text.replace(/@[xX][Ee]6/g, "\1n\1y\1" + "3\1i"); // Blinking yellow/brown on brown
		text = text.replace(/@[xX][Ee]7/g, "\1n\1w\1" + "3\1i"); // Blinking white on brown
		text = text.replace(/@[xX][Ee]8/g, "\1h\1k\1" + "3\1i"); // Blinking bright black on brown
		text = text.replace(/@[xX][Ee]9/g, "\1h\1b\1" + "3\1i"); // Blinking bright blue on brown
		text = text.replace(/@[xX][Ee][Aa]/g, "\1h\1g\1" + "3\1i"); // Blinking bright green on brown
		text = text.replace(/@[xX][Ee][Bb]/g, "\1h\1c\1" + "3\1i"); // Blinking bright cyan on brown
		text = text.replace(/@[xX][Ee][Cc]/g, "\1h\1r\1" + "3\1i"); // Blinking bright red on brown
		text = text.replace(/@[xX][Ee][Dd]/g, "\1h\1m\1" + "3\1i"); // Blinking bright magenta on brown
		text = text.replace(/@[xX][Ee][Ee]/g, "\1h\1y\1" + "3\1i"); // Blinking bright yellow on brown
		text = text.replace(/@[xX][Ee][Ff]/g, "\1h\1w\1" + "3\1i"); // Blinking bright white on brown
		text = text.replace(/@[xX][Ff]0/g, "\1n\1k\1" + "7\1i"); // Blinking black on white
		text = text.replace(/@[xX][Ff]1/g, "\1n\1b\1" + "7\1i"); // Blinking blue on white
		text = text.replace(/@[xX][Ff]2/g, "\1n\1g\1" + "7\1i"); // Blinking green on white
		text = text.replace(/@[xX][Ff]3/g, "\1n\1c\1" + "7\1i"); // Blinking cyan on white
		text = text.replace(/@[xX][Ff]4/g, "\1n\1r\1" + "7\1i"); // Blinking red on white
		text = text.replace(/@[xX][Ff]5/g, "\1n\1m\1" + "7\1i"); // Blinking magenta on white
		text = text.replace(/@[xX][Ff]6/g, "\1n\1y\1" + "7\1i"); // Blinking yellow/brown on white
		text = text.replace(/@[xX][Ff]7/g, "\1n\1w\1" + "7\1i"); // Blinking white on white
		text = text.replace(/@[xX][Ff]8/g, "\1h\1k\1" + "7\1i"); // Blinking bright black on white
		text = text.replace(/@[xX][Ff]9/g, "\1h\1b\1" + "7\1i"); // Blinking bright blue on white
		text = text.replace(/@[xX][Ff][Aa]/g, "\1h\1g\1" + "7\1i"); // Blinking bright green on white
		text = text.replace(/@[xX][Ff][Bb]/g, "\1h\1c\1" + "7\1i"); // Blinking bright cyan on white
		text = text.replace(/@[xX][Ff][Cc]/g, "\1h\1r\1" + "7\1i"); // Blinking bright red on white
		text = text.replace(/@[xX][Ff][Dd]/g, "\1h\1m\1" + "7\1i"); // Blinking bright magenta on white
		text = text.replace(/@[xX][Ff][Ee]/g, "\1h\1y\1" + "7\1i"); // Blinking bright yellow on white
		text = text.replace(/@[xX][Ff][Ff]/g, "\1h\1w\1" + "7\1i"); // Blinking bright white on white

		return text;
	}
	else
		return pText; // No PCBoard-style attribute codes found, so just return the text.
}

// Converts Wildcat attribute codes to Synchronet attribute codes.
//
// Parameters:
//  pText: A string containing the text to convert
//
// Return value: The text with the color codes converted
function wildcatAttrsToSyncAttrs(pText)
{
	// First, see if the text has any Wildcat-style attribute codes at
	// all.  We'll be performing a bunch of search & replace commands,
	// so we don't want to do all that work for nothing.. :)
	if (/@[0-9A-Fa-f]{2}@/.test(pText))
	{
		// Black background
		var text = pText.replace(/@00@/g, "\1n\1k\1" + "0"); // Black on black
		text = text.replace(/@01@/g, "\1n\1b\1" + "0"); // Blue on black
		text = text.replace(/@02@/g, "\1n\1g\1" + "0"); // Green on black
		text = text.replace(/@03@/g, "\1n\1c\1" + "0"); // Cyan on black
		text = text.replace(/@04@/g, "\1n\1r\1" + "0"); // Red on black
		text = text.replace(/@05@/g, "\1n\1m\1" + "0"); // Magenta on black
		text = text.replace(/@06@/g, "\1n\1y\1" + "0"); // Yellow/brown on black
		text = text.replace(/@07@/g, "\1n\1w\1" + "0"); // White on black
		text = text.replace(/@08@/g, "\1n\1w\1" + "0"); // White on black
		text = text.replace(/@09@/g, "\1n\1w\1" + "0"); // White on black
		text = text.replace(/@08@/g, "\1h\1k\1" + "0"); // Bright black on black
		text = text.replace(/@09@/g, "\1h\1b\1" + "0"); // Bright blue on black
		text = text.replace(/@0[Aa]@/g, "\1h\1g\1" + "0"); // Bright green on black
		text = text.replace(/@0[Bb]@/g, "\1h\1c\1" + "0"); // Bright cyan on black
		text = text.replace(/@0[Cc]@/g, "\1h\1r\1" + "0"); // Bright red on black
		text = text.replace(/@0[Dd]@/g, "\1h\1m\1" + "0"); // Bright magenta on black
		text = text.replace(/@0[Ee]@/g, "\1h\1y\1" + "0"); // Bright yellow on black
		text = text.replace(/@0[Ff]@/g, "\1h\1w\1" + "0"); // Bright white on black
		text = text.replace(/@10@/g, "\1n\1k\1" + "4"); // Black on blue
		text = text.replace(/@11@/g, "\1n\1b\1" + "4"); // Blue on blue
		text = text.replace(/@12@/g, "\1n\1g\1" + "4"); // Green on blue
		text = text.replace(/@13@/g, "\1n\1c\1" + "4"); // Cyan on blue
		text = text.replace(/@14@/g, "\1n\1r\1" + "4"); // Red on blue
		text = text.replace(/@15@/g, "\1n\1m\1" + "4"); // Magenta on blue
		text = text.replace(/@16@/g, "\1n\1y\1" + "4"); // Yellow/brown on blue
		text = text.replace(/@17@/g, "\1n\1w\1" + "4"); // White on blue
		text = text.replace(/@18@/g, "\1h\1k\1" + "4"); // Bright black on blue
		text = text.replace(/@19@/g, "\1h\1b\1" + "4"); // Bright blue on blue
		text = text.replace(/@1[Aa]@/g, "\1h\1g\1" + "4"); // Bright green on blue
		text = text.replace(/@1[Bb]@/g, "\1h\1c\1" + "4"); // Bright cyan on blue
		text = text.replace(/@1[Cc]@/g, "\1h\1r\1" + "4"); // Bright red on blue
		text = text.replace(/@1[Dd]@/g, "\1h\1m\1" + "4"); // Bright magenta on blue