Skip to content
Snippets Groups Projects
DDMsgReader.js 563 KiB
Newer Older
11001 11002 11003 11004 11005 11006 11007 11008 11009 11010 11011 11012 11013 11014 11015 11016 11017 11018 11019 11020 11021 11022 11023 11024 11025 11026 11027 11028 11029 11030 11031 11032 11033 11034 11035 11036 11037 11038 11039 11040 11041 11042 11043 11044 11045 11046 11047 11048 11049 11050 11051 11052 11053 11054 11055 11056 11057 11058 11059 11060 11061 11062 11063 11064 11065 11066 11067 11068 11069 11070 11071 11072 11073 11074 11075 11076 11077 11078 11079 11080 11081 11082 11083 11084 11085 11086 11087 11088 11089 11090 11091 11092 11093 11094 11095 11096 11097 11098 11099 11100 11101 11102 11103 11104 11105 11106 11107 11108 11109 11110 11111 11112 11113 11114 11115 11116 11117 11118 11119 11120 11121 11122 11123 11124 11125 11126 11127 11128 11129 11130 11131 11132 11133 11134 11135 11136 11137 11138 11139 11140 11141 11142 11143 11144 11145 11146 11147 11148 11149 11150 11151 11152 11153 11154 11155 11156 11157 11158 11159 11160 11161 11162 11163 11164 11165 11166 11167 11168 11169 11170 11171 11172 11173 11174 11175 11176 11177 11178 11179 11180 11181 11182 11183 11184 11185 11186 11187 11188 11189 11190 11191 11192 11193 11194 11195 11196 11197 11198 11199 11200 11201 11202 11203 11204 11205 11206 11207 11208 11209 11210 11211 11212 11213 11214 11215 11216 11217 11218 11219 11220 11221 11222 11223 11224 11225 11226 11227 11228 11229 11230 11231 11232 11233 11234 11235 11236 11237 11238 11239 11240 11241 11242 11243 11244 11245 11246 11247 11248 11249 11250 11251 11252 11253 11254 11255 11256 11257 11258 11259 11260 11261 11262 11263 11264 11265 11266 11267 11268 11269 11270 11271 11272 11273 11274 11275 11276 11277 11278 11279 11280 11281 11282 11283 11284 11285 11286 11287 11288 11289 11290 11291 11292 11293 11294 11295 11296 11297 11298 11299 11300 11301 11302 11303 11304 11305 11306 11307 11308 11309 11310 11311 11312 11313 11314 11315 11316 11317 11318 11319 11320 11321 11322 11323 11324 11325 11326 11327 11328 11329 11330 11331 11332 11333 11334 11335 11336 11337 11338 11339 11340 11341 11342 11343 11344 11345 11346 11347 11348 11349 11350 11351 11352 11353 11354 11355 11356 11357 11358 11359 11360 11361 11362 11363 11364 11365 11366 11367 11368 11369 11370 11371 11372 11373 11374 11375 11376 11377 11378 11379 11380 11381 11382 11383 11384 11385 11386 11387 11388 11389 11390 11391 11392 11393 11394 11395 11396 11397 11398 11399 11400 11401 11402 11403 11404 11405 11406 11407 11408 11409 11410 11411 11412 11413 11414 11415 11416 11417 11418 11419 11420 11421 11422 11423 11424 11425 11426 11427 11428 11429 11430 11431 11432 11433 11434 11435 11436 11437 11438 11439 11440 11441 11442 11443 11444 11445 11446 11447 11448 11449 11450 11451 11452 11453 11454 11455 11456 11457 11458 11459 11460 11461 11462 11463 11464 11465 11466 11467 11468 11469 11470 11471 11472 11473 11474 11475 11476 11477 11478 11479 11480 11481 11482 11483 11484 11485 11486 11487 11488 11489 11490 11491 11492 11493 11494 11495 11496 11497 11498 11499 11500 11501 11502 11503 11504 11505 11506 11507 11508 11509 11510 11511 11512 11513 11514 11515 11516 11517 11518 11519 11520 11521 11522 11523 11524 11525 11526 11527 11528 11529 11530 11531 11532 11533 11534 11535 11536 11537 11538 11539 11540 11541 11542 11543 11544 11545 11546 11547 11548 11549 11550 11551 11552 11553 11554 11555 11556 11557 11558 11559 11560 11561 11562 11563 11564 11565 11566 11567 11568 11569 11570 11571 11572 11573 11574 11575 11576 11577 11578 11579 11580 11581 11582 11583 11584 11585 11586 11587 11588 11589 11590 11591 11592 11593 11594 11595 11596 11597 11598 11599 11600 11601 11602 11603 11604 11605 11606 11607 11608 11609 11610 11611 11612 11613 11614 11615 11616 11617 11618 11619 11620 11621 11622 11623 11624 11625 11626 11627 11628 11629 11630 11631 11632 11633 11634 11635 11636 11637 11638 11639 11640 11641 11642 11643 11644 11645 11646 11647 11648 11649 11650 11651 11652 11653 11654 11655 11656 11657 11658 11659 11660 11661 11662 11663 11664 11665 11666 11667 11668 11669 11670 11671 11672 11673 11674 11675 11676 11677 11678 11679 11680 11681 11682 11683 11684 11685 11686 11687 11688 11689 11690 11691 11692 11693 11694 11695 11696 11697 11698 11699 11700 11701 11702 11703 11704 11705 11706 11707 11708 11709 11710 11711 11712 11713 11714 11715 11716 11717 11718 11719 11720 11721 11722 11723 11724 11725 11726 11727 11728 11729 11730 11731 11732 11733 11734 11735 11736 11737 11738 11739 11740 11741 11742 11743 11744 11745 11746 11747 11748 11749 11750 11751 11752 11753 11754 11755 11756 11757 11758 11759 11760 11761 11762 11763 11764 11765 11766 11767 11768 11769 11770 11771 11772 11773 11774 11775 11776 11777 11778 11779 11780 11781 11782 11783 11784 11785 11786 11787 11788 11789 11790 11791 11792 11793 11794 11795 11796 11797 11798 11799 11800 11801 11802 11803 11804 11805 11806 11807 11808 11809 11810 11811 11812 11813 11814 11815 11816 11817 11818 11819 11820 11821 11822 11823 11824 11825 11826 11827 11828 11829 11830 11831 11832 11833 11834 11835 11836 11837 11838 11839 11840 11841 11842 11843 11844 11845 11846 11847 11848 11849 11850 11851 11852 11853 11854 11855 11856 11857 11858 11859 11860 11861 11862 11863 11864 11865 11866 11867 11868 11869 11870 11871 11872 11873 11874 11875 11876 11877 11878 11879 11880 11881 11882 11883 11884 11885 11886 11887 11888 11889 11890 11891 11892 11893 11894 11895 11896 11897 11898 11899 11900 11901 11902 11903 11904 11905 11906 11907 11908 11909 11910 11911 11912 11913 11914 11915 11916 11917 11918 11919 11920 11921 11922 11923 11924 11925 11926 11927 11928 11929 11930 11931 11932 11933 11934 11935 11936 11937 11938 11939 11940 11941 11942 11943 11944 11945 11946 11947 11948 11949 11950 11951 11952 11953 11954 11955 11956 11957 11958 11959 11960 11961 11962 11963 11964 11965 11966 11967 11968 11969 11970 11971 11972 11973 11974 11975 11976 11977 11978 11979 11980 11981 11982 11983 11984 11985 11986 11987 11988 11989 11990 11991 11992 11993 11994 11995 11996 11997 11998 11999 12000
      else
      {
         if ((grpIdx > 0) && (msg_area.grp_list[grpIdx-1].sub_list.length > 0))
         {
            --grpIdx;
            subIdx = msg_area.grp_list[grpIdx].sub_list.length - 1;
         }
         else
            searchForSubBoard = false;
      }
      // If we can search, then do it.
      if (searchForSubBoard)
      {
         while (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) == 0)
         {
            if (subIdx > 0)
               --subIdx;
            else
            {
               if ((grpIdx > 0) && (msg_area.grp_list[grpIdx-1].sub_list.length > 0))
               {
                  --grpIdx;
                  subIdx = msg_area.grp_list[grpIdx].sub_list.length - 1;
               }
               else
                  break; // Stop searching
            }
         }
      }
   }
   // If we found a sub-board with messages in it, then set the variables
   // in the return object
   if (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) > 0)
   {
      retObj.grpIdx = grpIdx;
      retObj.subIdx = subIdx;
      retObj.subCode = msg_area.grp_list[grpIdx].sub_list[subIdx].code;
      retObj.foundSubBoard = true;
      retObj.subChanged = ((grpIdx != pStartGrpIdx) || (subIdx != pStartSubIdx));
   }

   return retObj;
}

// Returns the number of messages in a sub-board.
//
// Parameters:
//  pSubBoardCode: The internal code of the sub-board to check
//  pIncludeDeleted: Optional boolean - Whether or not to include deleted
//                   messages in the count.  Defaults to false.
//
// Return value: The number of messages in the sub-board
function numMsgsInSubBoard(pSubBoardCode, pIncludeDeleted)
{
   var numMessages = 0;
   var msgbase = new MsgBase(pSubBoardCode);
   if (msgbase.open())
   {
      var includeDeleted = (typeof(pIncludeDeleted) == "boolean" ? pIncludeDeleted : false);
      if (includeDeleted)
         numMessages = msgbase.total_msgs;
      else
      {
         // Don't include deleted messages.  Go through each message
         // in the sub-board and count the ones that aren't marked
         // as deleted.
         for (var msgIdx = 0; msgIdx < msgbase.total_msgs; ++msgIdx)
         {
            var msgHdr = msgbase.get_msg_header(true, msgIdx, false);
            if ((msgHdr != null) && ((msgHdr.attr & MSG_DELETE) == 0))
               ++numMessages;
         }
      }
      msgbase.close();
   }
   return numMessages;
}

// Replaces @-codes in a string and returns the new string.
//
// Parameters:
//  pStr: A string in which to replace @-codes
//
// Return value: A version of the string with @-codes interpreted
function replaceAtCodesInStr(pStr)
{
	if (typeof(pStr) != "string")
		return "";

	// This code was originally written by Deuce.  I updated it to check whether
	// the string returned by bbs.atcode() is null, and if so, just return
	// the original string.
	return pStr.replace(/@([^@]+)@/g, function(m, code) {
		var decoded = bbs.atcode(code);
		return (decoded != null ? decoded : "@" + code + "@");
	});
}

// Shortens a string, accounting for control/attribute codes.  Returns a new
// (shortened) copy of the string.
//
// Parameters:
//  pStr: The string to shorten
//  pNewLength: The new (shorter) length of the string
//
// Return value: The shortened version of the string
function shortenStrWithAttrCodes(pStr, pNewLength)
{
   if (typeof(pStr) != "string")
      return "";
   if (typeof(pNewLength) != "number")
      return pStr;
   if (pNewLength >= console.strlen(pStr))
      return pStr;

   var strCopy = "";
   var tmpStr = "";
   var strIdx = 0;
   var lengthGood = true;
   while (lengthGood && (strIdx < pStr.length))
   {
      tmpStr = strCopy + pStr.charAt(strIdx++);
      if (console.strlen(tmpStr) <= pNewLength)
         strCopy = tmpStr;
      else
         lengthGood = false;
   }
   return strCopy;
}

// Returns whether a given name matches the logged-in user's handle, alias, or
// name.
//
// Parameters:
//  pName: A name to match against the logged-in user
//
// Return value: Boolean - Whether or not the given name matches the logged-in
//               user's handle, alias, or name
function userHandleAliasNameMatch(pName)
{
   if (typeof(pName) != "string")
      return false;

   var userMatch = false;
   var nameUpper = pName.toUpperCase();
   if (user.handle.length > 0)
      userMatch = (nameUpper.indexOf(user.handle.toUpperCase()) > -1);
   if (!userMatch && (user.alias.length > 0))
      userMatch = (nameUpper.indexOf(user.alias.toUpperCase()) > -1);
   if (!userMatch && (user.name.length > 0))
      userMatch = (nameUpper.indexOf(user.name.toUpperCase()) > -1);
   return userMatch;
}

// Displays a range of text lines on the screen and allows scrolling through them
// with the up & down arrow keys, PageUp, PageDown, HOME, and END.  It is assumed
// that the array of text lines are already truncated to fit in the width of the
// text area, as a speed optimization.
//
// Parameters:
//  pTxtLines: The array of text lines to allow scrolling for
//  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.
//  pTopLeftX: The upper-left corner column for the text area
//  pTopLeftY: The upper-left corner row for the text area
//  pWidth: The width of the text area
//  pHeight: The height of the text area
//  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 scrollTextLines(pTxtLines, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pTopLeftX, pTopLeftY,
                          pWidth, pHeight, pPostWriteCurX, pPostWriteCurY, pScrollUpdateFn)
{
   // Variables for the top line index for the last page, scrolling, etc.
   var topLineIdxForLastPage = pTxtLines.length - pHeight;
   if (topLineIdxForLastPage < 0)
      topLineIdxForLastPage = 0;
   var msgFractionShown = pHeight / pTxtLines.length;
   if (msgFractionShown > 1)
      msgFractionShown = 1.0;
   var fractionToLastPage = 0;
   var lastTxtRow = pTopLeftY + pHeight - 1;
   var txtLineFormatStr = "%-" + pWidth + "s";

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

   var writeTxtLines = pWriteTxtLines;
   var continueOn = true;
   while (continueOn)
   {
      // If we are to write the text lines, then write each of them and also
      // clear out the rest of the row on the screen
      if (writeTxtLines)
      {
         // If the scroll update function parameter is a function, then calculate
         // the fraction to the last page and call the scroll update function.
         if (typeof(pScrollUpdateFn) == "function")
         {
            if (topLineIdxForLastPage != 0)
               fractionToLastPage = retObj.topLineIdx / topLineIdxForLastPage;
            pScrollUpdateFn(fractionToLastPage);
         }
         var screenY = pTopLeftY;
         for (var lineIdx = retObj.topLineIdx; (lineIdx < pTxtLines.length) && (screenY <= lastTxtRow); ++lineIdx)
         {
            console.gotoxy(pTopLeftX, screenY++);
            // Print the text line, then clear the rest of the line
            console.print(pTxtAttrib + pTxtLines[lineIdx]);
            printf("\1n%" + +(pWidth - console.strlen(pTxtLines[lineIdx])) + "s", "");
         }
         // If there are still some lines left in the message reading area, then
         // clear the lines.
         console.print("\1n" + pTxtAttrib);
         while (screenY <= lastTxtRow)
         {
            console.gotoxy(pTopLeftX, screenY++);
            printf(txtLineFormatStr, "");
         }
      }

      // 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) {
               --retObj.topLineIdx;
               writeTxtLines = true;
            }
            else
               writeTxtLines = false;
            break;
         case KEY_DOWN:
            if (retObj.topLineIdx < topLineIdxForLastPage) {
               ++retObj.topLineIdx;
               writeTxtLines = true;
            }
            else
               writeTxtLines = false;
            break;
         case KEY_PAGE_DOWN: // Next page
            if (retObj.topLineIdx < topLineIdxForLastPage) {
               retObj.topLineIdx += pHeight;
               if (retObj.topLineIdx > topLineIdxForLastPage)
                  retObj.topLineIdx = topLineIdxForLastPage;
               writeTxtLines = true;
            }
            else
               writeTxtLines = false;
            break;
         case KEY_PAGE_UP: // Previous page
            if (retObj.topLineIdx > 0) {
               retObj.topLineIdx -= pHeight;
               if (retObj.topLineIdx < 0)
                  retObj.topLineIdx = 0;
               writeTxtLines = true;
            }
            else
               writeTxtLines = false;
            break;
         case KEY_HOME: // First page
            if (retObj.topLineIdx > 0) {
               retObj.topLineIdx = 0;
               writeTxtLines = true;
            }
            else
               writeTxtLines = false;
            break;
         case KEY_END: // Last page
            if (retObj.topLineIdx < topLineIdxForLastPage) {
               retObj.topLineIdx = topLineIdxForLastPage;
               writeTxtLines = true;
            }
            else
               writeTxtLines = false;
            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 msgIsFromUser(pMsgHdr);
					}
				}
				else
				{
					matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
						var msgText = strip_ctrl(pMsgBase.get_msg_body(true, pMsgHdr.offset));
						return msgIsToLoggedInUserNum(pMsgHdr);
					}
				}
			}
			break;
		case SEARCH_KEYWORD:
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				var msgText = strip_ctrl(pMsgBase.get_msg_body(true, pMsgHdr.offset));
				//return ((pMsgHdr["subject"].toUpperCase().indexOf(pSearchStr) > -1) || (msgText.toUpperCase().indexOf(pSearchStr) > -1));
				var keywordFound = ((pMsgHdr.subject.toUpperCase().indexOf(pSearchStr) > -1) || (msgText.toUpperCase().indexOf(pSearchStr) > -1));
				if (pSubBoardCode == "mail")
					return keywordFound && msgIsToLoggedInUserNum(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 && msgIsToLoggedInUserNum(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 (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, true);
				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, true);
			// 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 logged-in user and is not deleted.
//
// Parameters:
//  pMsgHdr: A message header object
//
// Return value: Boolean - Whether or not the message is to the logged-in user
//               and is not deleted.
function msgIsToLoggedInUserNum(pMsgHdr)
{
	if (typeof(pMsgHdr) != "object")
		return false;
	return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.to_ext == user.number));
}

// Returns whether or not a message is from the logged-in user 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;
	var isFromCurrentUser = false;
	if (((pMsgHdr.attr & MSG_DELETE) == 0) && pMsgHdr.hasOwnProperty("from_ext"))
		isFromCurrentUser = (pMsgHdr.from_ext == user.number);
	return isFromCurrentUser;
}

/////////////////////////////////////////////////////////////////////////
// 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\14"); // 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\10"); // Black on black
		text = text.replace(/@[xX]01/g, "\1n\1b\10"); // Blue on black
		text = text.replace(/@[xX]02/g, "\1n\1g\10"); // Green on black
		text = text.replace(/@[xX]03/g, "\1n\1c\10"); // Cyan on black
		text = text.replace(/@[xX]04/g, "\1n\1r\10"); // Red on black
		text = text.replace(/@[xX]05/g, "\1n\1m\10"); // Magenta on black
		text = text.replace(/@[xX]06/g, "\1n\1y\10"); // Yellow/brown on black
		text = text.replace(/@[xX]07/g, "\1n\1w\10"); // White on black
		text = text.replace(/@[xX]08/g, "\1n\1w\10"); // White on black
		text = text.replace(/@[xX]09/g, "\1n\1w\10"); // White on black
		text = text.replace(/@[xX]08/g, "\1h\1k\10"); // Bright black on black
		text = text.replace(/@[xX]09/g, "\1h\1b\10"); // Bright blue on black
		text = text.replace(/@[xX]0[Aa]/g, "\1h\1g\10"); // Bright green on black
		text = text.replace(/@[xX]0[Bb]/g, "\1h\1c\10"); // Bright cyan on black
		text = text.replace(/@[xX]0[Cc]/g, "\1h\1r\10"); // Bright red on black
		text = text.replace(/@[xX]0[Dd]/g, "\1h\1m\10"); // Bright magenta on black
		text = text.replace(/@[xX]0[Ee]/g, "\1h\1y\10"); // Bright yellow on black
		text = text.replace(/@[xX]0[Ff]/g, "\1h\1w\10"); // Bright white on black
		// Blinking foreground

		// Blue background
		text = text.replace(/@[xX]10/g, "\1n\1k\14"); // Black on blue
		text = text.replace(/@[xX]11/g, "\1n\1b\14"); // Blue on blue
		text = text.replace(/@[xX]12/g, "\1n\1g\14"); // Green on blue
		text = text.replace(/@[xX]13/g, "\1n\1c\14"); // Cyan on blue
		text = text.replace(/@[xX]14/g, "\1n\1r\14"); // Red on blue
		text = text.replace(/@[xX]15/g, "\1n\1m\14"); // Magenta on blue
		text = text.replace(/@[xX]16/g, "\1n\1y\14"); // Yellow/brown on blue
		text = text.replace(/@[xX]17/g, "\1n\1w\14"); // White on blue
		text = text.replace(/@[xX]18/g, "\1h\1k\14"); // Bright black on blue
		text = text.replace(/@[xX]19/g, "\1h\1b\14"); // Bright blue on blue
		text = text.replace(/@[xX]1[Aa]/g, "\1h\1g\14"); // Bright green on blue
		text = text.replace(/@[xX]1[Bb]/g, "\1h\1c\14"); // Bright cyan on blue
		text = text.replace(/@[xX]1[Cc]/g, "\1h\1r\14"); // Bright red on blue
		text = text.replace(/@[xX]1[Dd]/g, "\1h\1m\14"); // Bright magenta on blue
		text = text.replace(/@[xX]1[Ee]/g, "\1h\1y\14"); // Bright yellow on blue
		text = text.replace(/@[xX]1[Ff]/g, "\1h\1w\14"); // Bright white on blue

		// Green background
		text = text.replace(/@[xX]20/g, "\1n\1k\12"); // Black on green
		text = text.replace(/@[xX]21/g, "\1n\1b\12"); // Blue on green
		text = text.replace(/@[xX]22/g, "\1n\1g\12"); // Green on green
		text = text.replace(/@[xX]23/g, "\1n\1c\12"); // Cyan on green
		text = text.replace(/@[xX]24/g, "\1n\1r\12"); // Red on green
		text = text.replace(/@[xX]25/g, "\1n\1m\12"); // Magenta on green
		text = text.replace(/@[xX]26/g, "\1n\1y\12"); // Yellow/brown on green
		text = text.replace(/@[xX]27/g, "\1n\1w\12"); // White on green
		text = text.replace(/@[xX]28/g, "\1h\1k\12"); // Bright black on green
		text = text.replace(/@[xX]29/g, "\1h\1b\12"); // Bright blue on green
		text = text.replace(/@[xX]2[Aa]/g, "\1h\1g\12"); // Bright green on green
		text = text.replace(/@[xX]2[Bb]/g, "\1h\1c\12"); // Bright cyan on green
		text = text.replace(/@[xX]2[Cc]/g, "\1h\1r\12"); // Bright red on green
		text = text.replace(/@[xX]2[Dd]/g, "\1h\1m\12"); // Bright magenta on green
		text = text.replace(/@[xX]2[Ee]/g, "\1h\1y\12"); // Bright yellow on green
		text = text.replace(/@[xX]2[Ff]/g, "\1h\1w\12"); // Bright white on green

		// Cyan background
		text = text.replace(/@[xX]30/g, "\1n\1k\16"); // Black on cyan
		text = text.replace(/@[xX]31/g, "\1n\1b\16"); // Blue on cyan
		text = text.replace(/@[xX]32/g, "\1n\1g\16"); // Green on cyan
		text = text.replace(/@[xX]33/g, "\1n\1c\16"); // Cyan on cyan
		text = text.replace(/@[xX]34/g, "\1n\1r\16"); // Red on cyan
		text = text.replace(/@[xX]35/g, "\1n\1m\16"); // Magenta on cyan
		text = text.replace(/@[xX]36/g, "\1n\1y\16"); // Yellow/brown on cyan
		text = text.replace(/@[xX]37/g, "\1n\1w\16"); // White on cyan
		text = text.replace(/@[xX]38/g, "\1h\1k\16"); // Bright black on cyan
		text = text.replace(/@[xX]39/g, "\1h\1b\16"); // Bright blue on cyan
		text = text.replace(/@[xX]3[Aa]/g, "\1h\1g\16"); // Bright green on cyan
		text = text.replace(/@[xX]3[Bb]/g, "\1h\1c\16"); // Bright cyan on cyan
		text = text.replace(/@[xX]3[Cc]/g, "\1h\1r\16"); // Bright red on cyan
		text = text.replace(/@[xX]3[Dd]/g, "\1h\1m\16"); // Bright magenta on cyan
		text = text.replace(/@[xX]3[Ee]/g, "\1h\1y\16"); // Bright yellow on cyan
		text = text.replace(/@[xX]3[Ff]/g, "\1h\1w\16"); // Bright white on cyan

		// Red background
		text = text.replace(/@[xX]40/g, "\1n\1k\11"); // Black on red
		text = text.replace(/@[xX]41/g, "\1n\1b\11"); // Blue on red
		text = text.replace(/@[xX]42/g, "\1n\1g\11"); // Green on red
		text = text.replace(/@[xX]43/g, "\1n\1c\11"); // Cyan on red
		text = text.replace(/@[xX]44/g, "\1n\1r\11"); // Red on red
		text = text.replace(/@[xX]45/g, "\1n\1m\11"); // Magenta on red
		text = text.replace(/@[xX]46/g, "\1n\1y\11"); // Yellow/brown on red
		text = text.replace(/@[xX]47/g, "\1n\1w\11"); // White on red
		text = text.replace(/@[xX]48/g, "\1h\1k\11"); // Bright black on red
		text = text.replace(/@[xX]49/g, "\1h\1b\11"); // Bright blue on red
		text = text.replace(/@[xX]4[Aa]/g, "\1h\1g\11"); // Bright green on red
		text = text.replace(/@[xX]4[Bb]/g, "\1h\1c\11"); // Bright cyan on red
		text = text.replace(/@[xX]4[Cc]/g, "\1h\1r\11"); // Bright red on red
		text = text.replace(/@[xX]4[Dd]/g, "\1h\1m\11"); // Bright magenta on red
		text = text.replace(/@[xX]4[Ee]/g, "\1h\1y\11"); // Bright yellow on red
		text = text.replace(/@[xX]4[Ff]/g, "\1h\1w\11"); // Bright white on red

		// Magenta background
		text = text.replace(/@[xX]50/g, "\1n\1k\15"); // Black on magenta
		text = text.replace(/@[xX]51/g, "\1n\1b\15"); // Blue on magenta
		text = text.replace(/@[xX]52/g, "\1n\1g\15"); // Green on magenta
		text = text.replace(/@[xX]53/g, "\1n\1c\15"); // Cyan on magenta
		text = text.replace(/@[xX]54/g, "\1n\1r\15"); // Red on magenta
		text = text.replace(/@[xX]55/g, "\1n\1m\15"); // Magenta on magenta
		text = text.replace(/@[xX]56/g, "\1n\1y\15"); // Yellow/brown on magenta
		text = text.replace(/@[xX]57/g, "\1n\1w\15"); // White on magenta
		text = text.replace(/@[xX]58/g, "\1h\1k\15"); // Bright black on magenta
		text = text.replace(/@[xX]59/g, "\1h\1b\15"); // Bright blue on magenta
		text = text.replace(/@[xX]5[Aa]/g, "\1h\1g\15"); // Bright green on magenta
		text = text.replace(/@[xX]5[Bb]/g, "\1h\1c\15"); // Bright cyan on magenta
		text = text.replace(/@[xX]5[Cc]/g, "\1h\1r\15"); // Bright red on magenta
		text = text.replace(/@[xX]5[Dd]/g, "\1h\1m\15"); // Bright magenta on magenta
		text = text.replace(/@[xX]5[Ee]/g, "\1h\1y\15"); // Bright yellow on magenta
		text = text.replace(/@[xX]5[Ff]/g, "\1h\1w\15"); // Bright white on magenta

		// Brown background
		text = text.replace(/@[xX]60/g, "\1n\1k\13"); // Black on brown
		text = text.replace(/@[xX]61/g, "\1n\1b\13"); // Blue on brown
		text = text.replace(/@[xX]62/g, "\1n\1g\13"); // Green on brown
		text = text.replace(/@[xX]63/g, "\1n\1c\13"); // Cyan on brown
		text = text.replace(/@[xX]64/g, "\1n\1r\13"); // Red on brown
		text = text.replace(/@[xX]65/g, "\1n\1m\13"); // Magenta on brown
		text = text.replace(/@[xX]66/g, "\1n\1y\13"); // Yellow/brown on brown
		text = text.replace(/@[xX]67/g, "\1n\1w\13"); // White on brown
		text = text.replace(/@[xX]68/g, "\1h\1k\13"); // Bright black on brown
		text = text.replace(/@[xX]69/g, "\1h\1b\13"); // Bright blue on brown
		text = text.replace(/@[xX]6[Aa]/g, "\1h\1g\13"); // Bright breen on brown
		text = text.replace(/@[xX]6[Bb]/g, "\1h\1c\13"); // Bright cyan on brown
		text = text.replace(/@[xX]6[Cc]/g, "\1h\1r\13"); // Bright red on brown
		text = text.replace(/@[xX]6[Dd]/g, "\1h\1m\13"); // Bright magenta on brown
		text = text.replace(/@[xX]6[Ee]/g, "\1h\1y\13"); // Bright yellow on brown
		text = text.replace(/@[xX]6[Ff]/g, "\1h\1w\13"); // Bright white on brown

		// White background
		text = text.replace(/@[xX]70/g, "\1n\1k\17"); // Black on white
		text = text.replace(/@[xX]71/g, "\1n\1b\17"); // Blue on white
		text = text.replace(/@[xX]72/g, "\1n\1g\17"); // Green on white
		text = text.replace(/@[xX]73/g, "\1n\1c\17"); // Cyan on white
		text = text.replace(/@[xX]74/g, "\1n\1r\17"); // Red on white
		text = text.replace(/@[xX]75/g, "\1n\1m\17"); // Magenta on white
		text = text.replace(/@[xX]76/g, "\1n\1y\17"); // Yellow/brown on white
		text = text.replace(/@[xX]77/g, "\1n\1w\17"); // White on white
		text = text.replace(/@[xX]78/g, "\1h\1k\17"); // Bright black on white
		text = text.replace(/@[xX]79/g, "\1h\1b\17"); // Bright blue on white
		text = text.replace(/@[xX]7[Aa]/g, "\1h\1g\17"); // Bright green on white
		text = text.replace(/@[xX]7[Bb]/g, "\1h\1c\17"); // Bright cyan on white
		text = text.replace(/@[xX]7[Cc]/g, "\1h\1r\17"); // Bright red on white
		text = text.replace(/@[xX]7[Dd]/g, "\1h\1m\17"); // Bright magenta on white
		text = text.replace(/@[xX]7[Ee]/g, "\1h\1y\17"); // Bright yellow on white
		text = text.replace(/@[xX]7[Ff]/g, "\1h\1w\17"); // Bright white on white

		// Black background, blinking foreground
		text = text.replace(/@[xX]80/g, "\1n\1k\10\1i"); // Blinking black on black
		text = text.replace(/@[xX]81/g, "\1n\1b\10\1i"); // Blinking blue on black
		text = text.replace(/@[xX]82/g, "\1n\1g\10\1i"); // Blinking green on black
		text = text.replace(/@[xX]83/g, "\1n\1c\10\1i"); // Blinking cyan on black
		text = text.replace(/@[xX]84/g, "\1n\1r\10\1i"); // Blinking red on black
		text = text.replace(/@[xX]85/g, "\1n\1m\10\1i"); // Blinking magenta on black
		text = text.replace(/@[xX]86/g, "\1n\1y\10\1i"); // Blinking yellow/brown on black
		text = text.replace(/@[xX]87/g, "\1n\1w\10\1i"); // Blinking white on black
		text = text.replace(/@[xX]88/g, "\1h\1k\10\1i"); // Blinking bright black on black
		text = text.replace(/@[xX]89/g, "\1h\1b\10\1i"); // Blinking bright blue on black
		text = text.replace(/@[xX]8[Aa]/g, "\1h\1g\10\1i"); // Blinking bright green on black
		text = text.replace(/@[xX]8[Bb]/g, "\1h\1c\10\1i"); // Blinking bright cyan on black
		text = text.replace(/@[xX]8[Cc]/g, "\1h\1r\10\1i"); // Blinking bright red on black
		text = text.replace(/@[xX]8[Dd]/g, "\1h\1m\10\1i"); // Blinking bright magenta on black
		text = text.replace(/@[xX]8[Ee]/g, "\1h\1y\10\1i"); // Blinking bright yellow on black
		text = text.replace(/@[xX]8[Ff]/g, "\1h\1w\10\1i"); // Blinking bright white on black

		// Blue background, blinking foreground
		text = text.replace(/@[xX]90/g, "\1n\1k\14\1i"); // Blinking black on blue
		text = text.replace(/@[xX]91/g, "\1n\1b\14\1i"); // Blinking blue on blue
		text = text.replace(/@[xX]92/g, "\1n\1g\14\1i"); // Blinking green on blue
		text = text.replace(/@[xX]93/g, "\1n\1c\14\1i"); // Blinking cyan on blue
		text = text.replace(/@[xX]94/g, "\1n\1r\14\1i"); // Blinking red on blue
		text = text.replace(/@[xX]95/g, "\1n\1m\14\1i"); // Blinking magenta on blue
		text = text.replace(/@[xX]96/g, "\1n\1y\14\1i"); // Blinking yellow/brown on blue
		text = text.replace(/@[xX]97/g, "\1n\1w\14\1i"); // Blinking white on blue
		text = text.replace(/@[xX]98/g, "\1h\1k\14\1i"); // Blinking bright black on blue
		text = text.replace(/@[xX]99/g, "\1h\1b\14\1i"); // Blinking bright blue on blue
		text = text.replace(/@[xX]9[Aa]/g, "\1h\1g\14\1i"); // Blinking bright green on blue
		text = text.replace(/@[xX]9[Bb]/g, "\1h\1c\14\1i"); // Blinking bright cyan on blue
		text = text.replace(/@[xX]9[Cc]/g, "\1h\1r\14\1i"); // Blinking bright red on blue
		text = text.replace(/@[xX]9[Dd]/g, "\1h\1m\14\1i"); // Blinking bright magenta on blue
		text = text.replace(/@[xX]9[Ee]/g, "\1h\1y\14\1i"); // Blinking bright yellow on blue
		text = text.replace(/@[xX]9[Ff]/g, "\1h\1w\14\1i"); // Blinking bright white on blue

		// Green background, blinking foreground
		text = text.replace(/@[xX][Aa]0/g, "\1n\1k\12\1i"); // Blinking black on green
		text = text.replace(/@[xX][Aa]1/g, "\1n\1b\12\1i"); // Blinking blue on green
		text = text.replace(/@[xX][Aa]2/g, "\1n\1g\12\1i"); // Blinking green on green
		text = text.replace(/@[xX][Aa]3/g, "\1n\1c\12\1i"); // Blinking cyan on green
		text = text.replace(/@[xX][Aa]4/g, "\1n\1r\12\1i"); // Blinking red on green
		text = text.replace(/@[xX][Aa]5/g, "\1n\1m\12\1i"); // Blinking magenta on green
		text = text.replace(/@[xX][Aa]6/g, "\1n\1y\12\1i"); // Blinking yellow/brown on green
		text = text.replace(/@[xX][Aa]7/g, "\1n\1w\12\1i"); // Blinking white on green
		text = text.replace(/@[xX][Aa]8/g, "\1h\1k\12\1i"); // Blinking bright black on green
		text = text.replace(/@[xX][Aa]9/g, "\1h\1b\12\1i"); // Blinking bright blue on green
		text = text.replace(/@[xX][Aa][Aa]/g, "\1h\1g\12\1i"); // Blinking bright green on green
		text = text.replace(/@[xX][Aa][Bb]/g, "\1h\1c\12\1i"); // Blinking bright cyan on green
		text = text.replace(/@[xX][Aa][Cc]/g, "\1h\1r\12\1i"); // Blinking bright red on green
		text = text.replace(/@[xX][Aa][Dd]/g, "\1h\1m\12\1i"); // Blinking bright magenta on green
		text = text.replace(/@[xX][Aa][Ee]/g, "\1h\1y\12\1i"); // Blinking bright yellow on green
		text = text.replace(/@[xX][Aa][Ff]/g, "\1h\1w\12\1i"); // Blinking bright white on green

		// Cyan background, blinking foreground
		text = text.replace(/@[xX][Bb]0/g, "\1n\1k\16\1i"); // Blinking black on cyan
		text = text.replace(/@[xX][Bb]1/g, "\1n\1b\16\1i"); // Blinking blue on cyan
		text = text.replace(/@[xX][Bb]2/g, "\1n\1g\16\1i"); // Blinking green on cyan
		text = text.replace(/@[xX][Bb]3/g, "\1n\1c\16\1i"); // Blinking cyan on cyan
		text = text.replace(/@[xX][Bb]4/g, "\1n\1r\16\1i"); // Blinking red on cyan
		text = text.replace(/@[xX][Bb]5/g, "\1n\1m\16\1i"); // Blinking magenta on cyan
		text = text.replace(/@[xX][Bb]6/g, "\1n\1y\16\1i"); // Blinking yellow/brown on cyan
		text = text.replace(/@[xX][Bb]7/g, "\1n\1w\16\1i"); // Blinking white on cyan
		text = text.replace(/@[xX][Bb]8/g, "\1h\1k\16\1i"); // Blinking bright black on cyan
		text = text.replace(/@[xX][Bb]9/g, "\1h\1b\16\1i"); // Blinking bright blue on cyan
		text = text.replace(/@[xX][Bb][Aa]/g, "\1h\1g\16\1i"); // Blinking bright green on cyan
		text = text.replace(/@[xX][Bb][Bb]/g, "\1h\1c\16\1i"); // Blinking bright cyan on cyan
		text = text.replace(/@[xX][Bb][Cc]/g, "\1h\1r\16\1i"); // Blinking bright red on cyan
		text = text.replace(/@[xX][Bb][Dd]/g, "\1h\1m\16\1i"); // Blinking bright magenta on cyan
		text = text.replace(/@[xX][Bb][Ee]/g, "\1h\1y\16\1i"); // Blinking bright yellow on cyan
		text = text.replace(/@[xX][Bb][Ff]/g, "\1h\1w\16\1i"); // Blinking bright white on cyan

		// Red background, blinking foreground
		text = text.replace(/@[xX][Cc]0/g, "\1n\1k\11\1i"); // Blinking black on red
		text = text.replace(/@[xX][Cc]1/g, "\1n\1b\11\1i"); // Blinking blue on red
		text = text.replace(/@[xX][Cc]2/g, "\1n\1g\11\1i"); // Blinking green on red
		text = text.replace(/@[xX][Cc]3/g, "\1n\1c\11\1i"); // Blinking cyan on red
		text = text.replace(/@[xX][Cc]4/g, "\1n\1r\11\1i"); // Blinking red on red
		text = text.replace(/@[xX][Cc]5/g, "\1n\1m\11\1i"); // Blinking magenta on red
		text = text.replace(/@[xX][Cc]6/g, "\1n\1y\11\1i"); // Blinking yellow/brown on red
		text = text.replace(/@[xX][Cc]7/g, "\1n\1w\11\1i"); // Blinking white on red
		text = text.replace(/@[xX][Cc]8/g, "\1h\1k\11\1i"); // Blinking bright black on red
		text = text.replace(/@[xX][Cc]9/g, "\1h\1b\11\1i"); // Blinking bright blue on red
		text = text.replace(/@[xX][Cc][Aa]/g, "\1h\1g\11\1i"); // Blinking bright green on red
		text = text.replace(/@[xX][Cc][Bb]/g, "\1h\1c\11\1i"); // Blinking bright cyan on red
		text = text.replace(/@[xX][Cc][Cc]/g, "\1h\1r\11\1i"); // Blinking bright red on red
		text = text.replace(/@[xX][Cc][Dd]/g, "\1h\1m\11\1i"); // Blinking bright magenta on red
		text = text.replace(/@[xX][Cc][Ee]/g, "\1h\1y\11\1i"); // Blinking bright yellow on red
		text = text.replace(/@[xX][Cc][Ff]/g, "\1h\1w\11\1i"); // Blinking bright white on red1

		// Magenta background, blinking foreground
		text = text.replace(/@[xX][Dd]0/g, "\1n\1k\15\1i"); // Blinking black on magenta
		text = text.replace(/@[xX][Dd]1/g, "\1n\1b\15\1i"); // Blinking blue on magenta
		text = text.replace(/@[xX][Dd]2/g, "\1n\1g\15\1i"); // Blinking green on magenta
		text = text.replace(/@[xX][Dd]3/g, "\1n\1c\15\1i"); // Blinking cyan on magenta
		text = text.replace(/@[xX][Dd]4/g, "\1n\1r\15\1i"); // Blinking red on magenta
		text = text.replace(/@[xX][Dd]5/g, "\1n\1m\15\1i"); // Blinking magenta on magenta
		text = text.replace(/@[xX][Dd]6/g, "\1n\1y\15\1i"); // Blinking yellow/brown on magenta
		text = text.replace(/@[xX][Dd]7/g, "\1n\1w\15\1i"); // Blinking white on magenta
		text = text.replace(/@[xX][Dd]8/g, "\1h\1k\15\1i"); // Blinking bright black on magenta
		text = text.replace(/@[xX][Dd]9/g, "\1h\1b\15\1i"); // Blinking bright blue on magenta
		text = text.replace(/@[xX][Dd][Aa]/g, "\1h\1g\15\1i"); // Blinking bright green on magenta
		text = text.replace(/@[xX][Dd][Bb]/g, "\1h\1c\15\1i"); // Blinking bright cyan on magenta
		text = text.replace(/@[xX][Dd][Cc]/g, "\1h\1r\15\1i"); // Blinking bright red on magenta
		text = text.replace(/@[xX][Dd][Dd]/g, "\1h\1m\15\1i"); // Blinking bright magenta on magenta
		text = text.replace(/@[xX][Dd][Ee]/g, "\1h\1y\15\1i"); // Blinking bright yellow on magenta
		text = text.replace(/@[xX][Dd][Ff]/g, "\1h\1w\15\1i"); // Blinking bright white on magenta

		// Brown background, blinking foreground
		text = text.replace(/@[xX][Ee]0/g, "\1n\1k\13\1i"); // Blinking black on brown
		text = text.replace(/@[xX][Ee]1/g, "\1n\1b\13\1i"); // Blinking blue on brown
		text = text.replace(/@[xX][Ee]2/g, "\1n\1g\13\1i"); // Blinking green on brown
		text = text.replace(/@[xX][Ee]3/g, "\1n\1c\13\1i"); // Blinking cyan on brown
		text = text.replace(/@[xX][Ee]4/g, "\1n\1r\13\1i"); // Blinking red on brown
		text = text.replace(/@[xX][Ee]5/g, "\1n\1m\13\1i"); // Blinking magenta on brown
		text = text.replace(/@[xX][Ee]6/g, "\1n\1y\13\1i"); // Blinking yellow/brown on brown
		text = text.replace(/@[xX][Ee]7/g, "\1n\1w\13\1i"); // Blinking white on brown
		text = text.replace(/@[xX][Ee]8/g, "\1h\1k\13\1i"); // Blinking bright black on brown
		text = text.replace(/@[xX][Ee]9/g, "\1h\1b\13\1i"); // Blinking bright blue on brown
		text = text.replace(/@[xX][Ee][Aa]/g, "\1h\1g\13\1i"); // Blinking bright green on brown
		text = text.replace(/@[xX][Ee][Bb]/g, "\1h\1c\13\1i"); // Blinking bright cyan on brown
		text = text.replace(/@[xX][Ee][Cc]/g, "\1h\1r\13\1i"); // Blinking bright red on brown
		text = text.replace(/@[xX][Ee][Dd]/g, "\1h\1m\13\1i"); // Blinking bright magenta on brown
		text = text.replace(/@[xX][Ee][Ee]/g, "\1h\1y\13\1i"); // Blinking bright yellow on brown
		text = text.replace(/@[xX][Ee][Ff]/g, "\1h\1w\13\1i"); // Blinking bright white on brown