diff --git a/ctrl/SlyDCTColors_Default.cfg b/ctrl/SlyDCTColors_Default.cfg
index f88e6a665d259e3675e33105249cc638a96803b3..023e86d121a4653f3667b6c42607a44e80e56dae 100644
--- a/ctrl/SlyDCTColors_Default.cfg
+++ b/ctrl/SlyDCTColors_Default.cfg
@@ -1,82 +1,87 @@
-; This is a color theme file for SlyEdit's DCT Style.
-; This color scheme mimics DCT Edit's default color scheme.
-
-; Text edit color
-TextEditColor=nw
-; The color to use for quoted lines in the message
-QuoteLineColor=nc
-
-; Border colors
-TopBorderColor1=nr
-TopBorderColor2=nrh
-EditAreaBorderColor1=ng
-EditAreaBorderColor2=ngh
-EditModeBrackets=nkh
-EditMode=nw
-
-; Colors for the top informational area
-TopLabelColor=nbh
-TopLabelColonColor=nb
-TopFromColor=nch
-TopFromFillColor=nc
-TopToColor=nch
-TopToFillColor=nc
-TopSubjColor=nwh
-TopSubjFillColor=nw
-TopAreaColor=ngh
-TopAreaFillColor=ng
-TopTimeColor=nyh
-TopTimeFillColor=nr
-TopTimeLeftColor=nyh
-TopTimeLeftFillColor=nr
-TopInfoBracketColor=nm
-
-; Colors for the quote window
-QuoteWinText=n7b
-QuoteLineHighlightColor=nw
-QuoteWinBorderTextColor=n7r
-QuoteWinBorderColor=nk7
-
-; Colors for the bottom row help text
-BottomHelpBrackets=nkh
-BottomHelpKeys=nrh
-BottomHelpFill=nr
-BottomHelpKeyDesc=nc
-
-; Colors for text boxes
-TextBoxBorder=nk7
-TextBoxBorderText=nr7
-TextBoxInnerText=nb7
-YesNoBoxBrackets=nk7
-YesNoBoxYesNoText=nwh7
-
-; Colors for the menus
-SelectedMenuLabelBorders=nw
-SelectedMenuLabelText=nk7
-UnselectedMenuLabelText=nwh
-MenuBorders=nk7
-MenuSelectedItems=nw
-MenuUnselectedItems=nk7
-MenuHotkeys=nwh7
-
-; Colors for the cross-post selection box
-crossPostBorder=ng
-crossPostBorderText=nbh
-crossPostMsgAreaNum=nhw
-crossPostMsgAreaNumHighlight=n4hw
-crossPostMsgAreaDesc=nc
-crossPostMsgAreaDescHighlight=n4c
-crossPostChk=nhy
-crossPostChkHighlight=n4hy
-crossPostMsgGrpMark=nhg
-crossPostMsgGrpMarkHighlight=n4hg
-
-; Colors for message saving and sub-board post info when exiting SlyEdit
-msgWillBePostedHdr=nc
-msgPostedGrpHdr=nhb
-msgPostedSubBoardName=ng
-msgPostedOriginalAreaText=nc
-msgHasBeenSavedText=nhc
-msgAbortedText=nhm
-emptyMsgNotSentText=nhm
+; This is a color theme file for SlyEdit's DCT Style.
+; This color scheme mimics DCT Edit's default color scheme.
+
+; Text edit color
+TextEditColor=nw
+; The color to use for quoted lines in the message
+QuoteLineColor=nc
+
+; Border colors
+TopBorderColor1=nr
+TopBorderColor2=nrh
+EditAreaBorderColor1=ng
+EditAreaBorderColor2=ngh
+EditModeBrackets=nkh
+EditMode=nw
+
+; Colors for the top informational area
+TopLabelColor=nbh
+TopLabelColonColor=nb
+TopFromColor=nch
+TopFromFillColor=nc
+TopToColor=nch
+TopToFillColor=nc
+TopSubjColor=nwh
+TopSubjFillColor=nw
+TopAreaColor=ngh
+TopAreaFillColor=ng
+TopTimeColor=nyh
+TopTimeFillColor=nr
+TopTimeLeftColor=nyh
+TopTimeLeftFillColor=nr
+TopInfoBracketColor=nm
+
+; Colors for the quote window
+QuoteWinText=n7b
+QuoteLineHighlightColor=nw
+QuoteWinBorderTextColor=n7r
+QuoteWinBorderColor=nk7
+
+; Colors for the bottom row help text
+BottomHelpBrackets=nkh
+BottomHelpKeys=nrh
+BottomHelpFill=nr
+BottomHelpKeyDesc=nc
+
+; Colors for text boxes
+TextBoxBorder=nk7
+TextBoxBorderText=nr7
+TextBoxInnerText=nb7
+YesNoBoxBrackets=nk7
+YesNoBoxYesNoText=nwh7
+
+; Colors for the menus
+SelectedMenuLabelBorders=nw
+SelectedMenuLabelText=nk7
+UnselectedMenuLabelText=nwh
+MenuBorders=nk7
+MenuSelectedItems=nw
+MenuUnselectedItems=nk7
+MenuHotkeys=nwh7
+
+; Color settings for list boxes
+listBoxBorder=ng
+listBoxBorderText=nbh
+
+; Colors for the cross-post selection box
+crossPostMsgAreaNum=nhw
+crossPostMsgAreaNumHighlight=n4hw
+crossPostMsgAreaDesc=nc
+crossPostMsgAreaDescHighlight=n4c
+crossPostChk=nhy
+crossPostChkHighlight=n4hy
+crossPostMsgGrpMark=nhg
+crossPostMsgGrpMarkHighlight=n4hg
+
+; Colors for the text replacement list
+txtReplacementList=nc
+
+; Colors for message saving and sub-board post info when exiting SlyEdit
+msgWillBePostedHdr=nc
+msgPostedGrpHdr=nhb
+msgPostedSubBoardName=ng
+msgPostedOriginalAreaText=nc
+msgHasBeenSavedText=nhc
+msgAbortedText=nhm
+emptyMsgNotSentText=nhm
 genMsgErrorText=nhm
\ No newline at end of file
diff --git a/ctrl/SlyDCTColors_Midnight.cfg b/ctrl/SlyDCTColors_Midnight.cfg
index 07a28f1cb77d2f4082ae69be9553fcaca1fddf7a..8de32e937b5bff82162532a788163d07969318bf 100644
--- a/ctrl/SlyDCTColors_Midnight.cfg
+++ b/ctrl/SlyDCTColors_Midnight.cfg
@@ -1,82 +1,89 @@
-; This is a color theme file for SlyEdit's DCT Style.
-; This is a color scheme I call "Midnight".
-
-; Text edit color
-TextEditColor=nw
-; The color to use for quoted lines in the message
-QuoteLineColor=nc
-
-; Border colors
-TopBorderColor1=nb
-TopBorderColor2=nkh
-EditAreaBorderColor1=nbh
-EditAreaBorderColor2=nkh
-EditModeBrackets=nkh
-EditMode=nw
-
-; Colors for the top informational area
-TopLabelColor=nbh
-TopLabelColonColor=nb
-TopFromColor=nc
-TopFromFillColor=nkh
-TopToColor=nb
-TopToFillColor=nkh
-TopSubjColor=nkh
-TopSubjFillColor=nkh
-TopAreaColor=nb
-TopAreaFillColor=nkh
-TopTimeColor=nkh
-TopTimeFillColor=nkh
-TopTimeLeftColor=nkh
-TopTimeLeftFillColor=nkh
-TopInfoBracketColor=nw
-
-; Colors for the quote window
-QuoteWinText=n7b
-QuoteLineHighlightColor=nw
-QuoteWinBorderTextColor=n7r
-QuoteWinBorderColor=nk7
-
-; Colors for the bottom row help text
-BottomHelpBrackets=nkh
-BottomHelpKeys=nb
-BottomHelpFill=nkh
-BottomHelpKeyDesc=nc
-
-; Colors for text boxes
-TextBoxBorder=nkh
-TextBoxBorderText=nbh
-TextBoxInnerText=nw
-YesNoBoxBrackets=nkh
-YesNoBoxYesNoText=nw
-
-; Colors for the menus
-SelectedMenuLabelBorders=nb
-SelectedMenuLabelText=nk4
-UnselectedMenuLabelText=nb
-MenuBorders=nkh
-MenuSelectedItems=nw
-MenuUnselectedItems=nb
-MenuHotkeys=nbh
-
-; Colors for the cross-post selection box
-crossPostBorder=nhk
-crossPostBorderText=nbh
-crossPostMsgAreaNum=nhw
-crossPostMsgAreaNumHighlight=n4hw
-crossPostMsgAreaDesc=nc
-crossPostMsgAreaDescHighlight=n4c
-crossPostChk=nhy
-crossPostChkHighlight=n4hy
-crossPostMsgGrpMark=nhg
-crossPostMsgGrpMarkHighlight=n4hg
-
-; Colors for message saving and sub-board post info when exiting SlyEdit
-msgWillBePostedHdr=nc
-msgPostedGrpHdr=nhb
-msgPostedSubBoardName=ng
-msgPostedOriginalAreaText=nc
-msgHasBeenSavedText=nhc
-msgAbortedText=nhm
-emptyMsgNotSentText=nhm
+; This is a color theme file for SlyEdit's DCT Style.
+; This is a color scheme I call "Midnight".
+
+; Text edit color
+TextEditColor=nw
+; The color to use for quoted lines in the message
+QuoteLineColor=nc
+
+; Border colors
+TopBorderColor1=nb
+TopBorderColor2=nkh
+EditAreaBorderColor1=nbh
+EditAreaBorderColor2=nkh
+EditModeBrackets=nkh
+EditMode=nw
+
+; Colors for the top informational area
+TopLabelColor=nbh
+TopLabelColonColor=nb
+TopFromColor=nc
+TopFromFillColor=nkh
+TopToColor=nb
+TopToFillColor=nkh
+TopSubjColor=nkh
+TopSubjFillColor=nkh
+TopAreaColor=nb
+TopAreaFillColor=nkh
+TopTimeColor=nkh
+TopTimeFillColor=nkh
+TopTimeLeftColor=nkh
+TopTimeLeftFillColor=nkh
+TopInfoBracketColor=nw
+
+; Colors for the quote window
+QuoteWinText=n7b
+QuoteLineHighlightColor=nw
+QuoteWinBorderTextColor=n7r
+QuoteWinBorderColor=nk7
+
+; Colors for the bottom row help text
+BottomHelpBrackets=nkh
+BottomHelpKeys=nb
+BottomHelpFill=nkh
+BottomHelpKeyDesc=nc
+
+; Colors for text boxes
+TextBoxBorder=nkh
+TextBoxBorderText=nbh
+TextBoxInnerText=nw
+YesNoBoxBrackets=nkh
+YesNoBoxYesNoText=nw
+
+; Colors for the menus
+SelectedMenuLabelBorders=nb
+SelectedMenuLabelText=nk4
+UnselectedMenuLabelText=nb
+MenuBorders=nkh
+MenuSelectedItems=nw
+MenuUnselectedItems=nb
+MenuHotkeys=nbh
+
+; Color settings for list boxes
+listBoxBorder=nhk
+listBoxBorderText=nbh
+
+; Colors for the cross-post selection box
+crossPostBorder=nhk
+crossPostBorderText=nbh
+crossPostMsgAreaNum=nhw
+crossPostMsgAreaNumHighlight=n4hw
+crossPostMsgAreaDesc=nc
+crossPostMsgAreaDescHighlight=n4c
+crossPostChk=nhy
+crossPostChkHighlight=n4hy
+crossPostMsgGrpMark=nhg
+crossPostMsgGrpMarkHighlight=n4hg
+
+; Colors for the text replacement list
+txtReplacementList=nbh
+
+; Colors for message saving and sub-board post info when exiting SlyEdit
+msgWillBePostedHdr=nc
+msgPostedGrpHdr=nhb
+msgPostedSubBoardName=ng
+msgPostedOriginalAreaText=nc
+msgHasBeenSavedText=nhc
+msgAbortedText=nhm
+emptyMsgNotSentText=nhm
 genMsgErrorText=nhm
\ No newline at end of file
diff --git a/ctrl/SlyEdit.cfg b/ctrl/SlyEdit.cfg
index 8981a84dd213a8e5fbfdc18a6c2db757c1ebda01..c49274ece52bda8abf922ae0420bd99f9a7f7e9d 100644
--- a/ctrl/SlyEdit.cfg
+++ b/ctrl/SlyEdit.cfg
@@ -13,6 +13,12 @@ useQuoteLineInitials=true
 indentQuoteLinesWithInitials=true
 ; Whether or not to allow cross-posting
 allowCrossPosting=true
+; Whether or not to enable text replacements (AKA macros).
+; enableTextReplacements can have one of the following values:
+; false : Text replacement is disabled
+; true  : Text replacement is enabled and performed as literal search and replace
+; regex : Text replacement is enabled using regular expressions
+enableTextReplacements=true
 ; 3rd-party startup scripts
 ;add3rdPartyStartupScript=
 ; JavaScript commands to run upon start of SlyEdit
diff --git a/ctrl/SlyEdit_TextReplacements.cfg b/ctrl/SlyEdit_TextReplacements.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..71a3e313a6a04b1996310e4417c1700098b763cb
--- /dev/null
+++ b/ctrl/SlyEdit_TextReplacements.cfg
@@ -0,0 +1,10 @@
+; This file configures text replacements (AKA Macros) for SlyEdit.  Each line
+; needs to be in this format:
+; originalWord=replacementText
+; where originalWord is the word to be replaced, and replacementText is the
+; text to replace the word with.
+; The option enableTextReplacements in SlyEdit.cfg can have one of the
+; following values:
+; false (disabled)
+; true (enabled, literal string matching)
+; regex (enabled, using regular expressions)
diff --git a/ctrl/SlyIceColors_BlueIce.cfg b/ctrl/SlyIceColors_BlueIce.cfg
index 3d87ebb200a139963b78da6e460a75e129f16fbd..e19588b3d1eec1bdc322266e98450e5800d65492 100644
--- a/ctrl/SlyIceColors_BlueIce.cfg
+++ b/ctrl/SlyIceColors_BlueIce.cfg
@@ -1,56 +1,61 @@
-; This is a color theme file for SlyEdit's Ice Style.
-; This color scheme mimics IceEdit/QuikEdit's "Blue Ice" theme.
-
-; Text edit color
-TextEditColor=nw
-; The color to use for quoted lines in the message
-QuoteLineColor=nc
-
-; Border colors
-BorderColor1=nb
-BorderColor2=nbh
-KeyInfoLabelColor=ch
-
-; Colors for the top informational area
-TopInfoBkgColor=4
-TopLabelColor=ch
-TopLabelColonColor=bh
-TopToColor=wh
-TopFromColor=wh
-TopSubjectColor=wh
-TopTimeColor=gh
-TopTimeLeftColor=gh
-EditMode=ch
-
-; Colors for the quote window
-QuoteWinText=nhw
-QuoteLineHighlightColor=4hc
-QuoteWinBorderTextColor=nch
-
-; Colors for the multi-choice options
-SelectedOptionBorderColor=nbh4
-SelectedOptionTextColor=nch4
-UnselectedOptionBorderColor=nb
-UnselectedOptionTextColor=nw
-
-; Colors for the cross-post selection box
-crossPostBorder=nb
-crossPostBorderText=nbh
-crossPostMsgAreaNum=nhw
-crossPostMsgAreaNumHighlight=n4hw
-crossPostMsgAreaDesc=nc
-crossPostMsgAreaDescHighlight=n4c
-crossPostChk=nhy
-crossPostChkHighlight=n4hy
-crossPostMsgGrpMark=nhg
-crossPostMsgGrpMarkHighlight=n4hg
-
-; Colors for message saving and sub-board post info when exiting SlyEdit
-msgWillBePostedHdr=nc
-msgPostedGrpHdr=nhb
-msgPostedSubBoardName=ng
-msgPostedOriginalAreaText=nc
-msgHasBeenSavedText=nhc
-msgAbortedText=nhm
-emptyMsgNotSentText=nhm
+; This is a color theme file for SlyEdit's Ice Style.
+; This color scheme mimics IceEdit/QuikEdit's "Blue Ice" theme.
+
+; Text edit color
+TextEditColor=nw
+; The color to use for quoted lines in the message
+QuoteLineColor=nc
+
+; Border colors
+BorderColor1=nb
+BorderColor2=nbh
+KeyInfoLabelColor=ch
+
+; Colors for the top informational area
+TopInfoBkgColor=4
+TopLabelColor=ch
+TopLabelColonColor=bh
+TopToColor=wh
+TopFromColor=wh
+TopSubjectColor=wh
+TopTimeColor=gh
+TopTimeLeftColor=gh
+EditMode=ch
+
+; Colors for the quote window
+QuoteWinText=nhw
+QuoteLineHighlightColor=4hc
+QuoteWinBorderTextColor=nch
+
+; Colors for the multi-choice options
+SelectedOptionBorderColor=nbh4
+SelectedOptionTextColor=nch4
+UnselectedOptionBorderColor=nb
+UnselectedOptionTextColor=nw
+
+; Color settings for list boxes
+listBoxBorder=nb
+listBoxBorderText=nbh
+
+; Colors for the cross-post selection box
+crossPostMsgAreaNum=nhw
+crossPostMsgAreaNumHighlight=n4hw
+crossPostMsgAreaDesc=nc
+crossPostMsgAreaDescHighlight=n4c
+crossPostChk=nhy
+crossPostChkHighlight=n4hy
+crossPostMsgGrpMark=nhg
+crossPostMsgGrpMarkHighlight=n4hg
+
+; Colors for the text replacement list
+txtReplacementList=nc
+
+; Colors for message saving and sub-board post info when exiting SlyEdit
+msgWillBePostedHdr=nc
+msgPostedGrpHdr=nhb
+msgPostedSubBoardName=ng
+msgPostedOriginalAreaText=nc
+msgHasBeenSavedText=nhc
+msgAbortedText=nhm
+emptyMsgNotSentText=nhm
 genMsgErrorText=nhm
\ No newline at end of file
diff --git a/ctrl/SlyIceColors_EmeraldCity.cfg b/ctrl/SlyIceColors_EmeraldCity.cfg
index e687c077aca6de07e6cc6f68e50ce96a34d6c7ff..b2ec661d7a5ca4e7c14204150633ac94bb148934 100644
--- a/ctrl/SlyIceColors_EmeraldCity.cfg
+++ b/ctrl/SlyIceColors_EmeraldCity.cfg
@@ -1,56 +1,61 @@
-; This is a color theme file for SlyEdit's Ice Style.
-; This color scheme mimics IceEdit/QuikEdit's "Emerald City" theme.
-
-; Text edit color
-TextEditColor=nw
-; The color to use for quoted lines in the message
-QuoteLineColor=ng
-
-; Border colors
-BorderColor1=ng
-BorderColor2=ngh
-KeyInfoLabelColor=ch
-
-; Colors for the top informational area
-TopInfoBkgColor=2
-TopLabelColor=ch
-TopLabelColonColor=gh
-TopToColor=wh
-TopFromColor=wh
-TopSubjectColor=wh
-TopTimeColor=gh
-TopTimeLeftColor=gh
-EditMode=ch
-
-; Colors for the quote window
-QuoteWinText=nhw
-QuoteLineHighlightColor=6hc
-QuoteWinBorderTextColor=nch
-
-; Colors for the multi-choice options
-SelectedOptionBorderColor=nch6
-SelectedOptionTextColor=nch6
-UnselectedOptionBorderColor=ng
-UnselectedOptionTextColor=nw
-
-; Colors for the cross-post selection box
-crossPostBorder=ng
-crossPostBorderText=ngh
-crossPostMsgAreaNum=nhw
-crossPostMsgAreaNumHighlight=n4hw
-crossPostMsgAreaDesc=nc
-crossPostMsgAreaDescHighlight=n4c
-crossPostChk=nhy
-crossPostChkHighlight=n4hy
-crossPostMsgGrpMark=nhg
-crossPostMsgGrpMarkHighlight=n4hg
-
-; Colors for message saving and sub-board post info when exiting SlyEdit
-msgWillBePostedHdr=nc
-msgPostedGrpHdr=nhb
-msgPostedSubBoardName=ng
-msgPostedOriginalAreaText=nc
-msgHasBeenSavedText=nhc
-msgAbortedText=nhm
-emptyMsgNotSentText=nhm
+; This is a color theme file for SlyEdit's Ice Style.
+; This color scheme mimics IceEdit/QuikEdit's "Emerald City" theme.
+
+; Text edit color
+TextEditColor=nw
+; The color to use for quoted lines in the message
+QuoteLineColor=ng
+
+; Border colors
+BorderColor1=ng
+BorderColor2=ngh
+KeyInfoLabelColor=ch
+
+; Colors for the top informational area
+TopInfoBkgColor=2
+TopLabelColor=ch
+TopLabelColonColor=gh
+TopToColor=wh
+TopFromColor=wh
+TopSubjectColor=wh
+TopTimeColor=gh
+TopTimeLeftColor=gh
+EditMode=ch
+
+; Colors for the quote window
+QuoteWinText=nhw
+QuoteLineHighlightColor=6hc
+QuoteWinBorderTextColor=nch
+
+; Colors for the multi-choice options
+SelectedOptionBorderColor=nch6
+SelectedOptionTextColor=nch6
+UnselectedOptionBorderColor=ng
+UnselectedOptionTextColor=nw
+
+; Color settings for list boxes
+listBoxBorder=ng
+listBoxBorderText=ngh
+
+; Colors for the cross-post selection box
+crossPostMsgAreaNum=nhw
+crossPostMsgAreaNumHighlight=n4hw
+crossPostMsgAreaDesc=nc
+crossPostMsgAreaDescHighlight=n4c
+crossPostChk=nhy
+crossPostChkHighlight=n4hy
+crossPostMsgGrpMark=nhg
+crossPostMsgGrpMarkHighlight=n4hg
+
+; Colors for the text replacement list
+txtReplacementList=ng
+
+; Colors for message saving and sub-board post info when exiting SlyEdit
+msgWillBePostedHdr=nc
+msgPostedGrpHdr=nhb
+msgPostedSubBoardName=ng
+msgPostedOriginalAreaText=nc
+msgHasBeenSavedText=nhc
+msgAbortedText=nhm
+emptyMsgNotSentText=nhm
 genMsgErrorText=nhm
\ No newline at end of file
diff --git a/ctrl/SlyIceColors_FieryInferno.cfg b/ctrl/SlyIceColors_FieryInferno.cfg
index 4bf851feeda1184670f82e16f6a0f70f1c40a6b1..a22b541424efaa502ea7d943dc7147be5beb885f 100644
--- a/ctrl/SlyIceColors_FieryInferno.cfg
+++ b/ctrl/SlyIceColors_FieryInferno.cfg
@@ -1,56 +1,61 @@
-; This is a color theme file for SlyEdit's Ice Style.
-; This color scheme mimics IceEdit/QuikEdit's "Fiery Inferno" theme.
-
-; Text edit color
-TextEditColor=nw
-; The color to use for quoted lines in the message
-QuoteLineColor=nc
-
-; Border colors
-BorderColor1=nr
-BorderColor2=nrh
-KeyInfoLabelColor=yh
-
-; Colors for the top informational area
-TopInfoBkgColor=1
-TopLabelColor=yh
-TopLabelColonColor=rh
-TopToColor=wh
-TopFromColor=wh
-TopSubjectColor=wh
-TopTimeColor=wh
-TopTimeLeftColor=wh
-EditMode=yh
-
-; Colors for the quote window
-QuoteWinText=nhw
-QuoteLineHighlightColor=1hy
-QuoteWinBorderTextColor=nyh
-
-; Colors for the multi-choice options
-SelectedOptionBorderColor=nrh1
-SelectedOptionTextColor=nyh1
-UnselectedOptionBorderColor=nr
-UnselectedOptionTextColor=nw
-
-; Colors for the cross-post selection box
-crossPostBorder=nr
-crossPostBorderText=nrh
-crossPostMsgAreaNum=nhw
-crossPostMsgAreaNumHighlight=n4hw
-crossPostMsgAreaDesc=nc
-crossPostMsgAreaDescHighlight=n4c
-crossPostChk=nhy
-crossPostChkHighlight=n4hy
-crossPostMsgGrpMark=nhg
-crossPostMsgGrpMarkHighlight=n4hg
-
-; Colors for message saving and sub-board post info when exiting SlyEdit
-msgWillBePostedHdr=nc
-msgPostedGrpHdr=nhb
-msgPostedSubBoardName=ng
-msgPostedOriginalAreaText=nc
-msgHasBeenSavedText=nhc
-msgAbortedText=nhm
-emptyMsgNotSentText=nhm
+; This is a color theme file for SlyEdit's Ice Style.
+; This color scheme mimics IceEdit/QuikEdit's "Fiery Inferno" theme.
+
+; Text edit color
+TextEditColor=nw
+; The color to use for quoted lines in the message
+QuoteLineColor=nc
+
+; Border colors
+BorderColor1=nr
+BorderColor2=nrh
+KeyInfoLabelColor=yh
+
+; Colors for the top informational area
+TopInfoBkgColor=1
+TopLabelColor=yh
+TopLabelColonColor=rh
+TopToColor=wh
+TopFromColor=wh
+TopSubjectColor=wh
+TopTimeColor=wh
+TopTimeLeftColor=wh
+EditMode=yh
+
+; Colors for the quote window
+QuoteWinText=nhw
+QuoteLineHighlightColor=1hy
+QuoteWinBorderTextColor=nyh
+
+; Colors for the multi-choice options
+SelectedOptionBorderColor=nrh1
+SelectedOptionTextColor=nyh1
+UnselectedOptionBorderColor=nr
+UnselectedOptionTextColor=nw
+
+; Color settings for list boxes
+listBoxBorder=nr
+listBoxBorderText=nrh
+
+; Colors for the cross-post selection box
+crossPostMsgAreaNum=nhw
+crossPostMsgAreaNumHighlight=n4hw
+crossPostMsgAreaDesc=nc
+crossPostMsgAreaDescHighlight=n4c
+crossPostChk=nhy
+crossPostChkHighlight=n4hy
+crossPostMsgGrpMark=nhg
+crossPostMsgGrpMarkHighlight=n4hg
+
+; Colors for the text replacement list
+txtReplacementList=nrh
+
+; Colors for message saving and sub-board post info when exiting SlyEdit
+msgWillBePostedHdr=nc
+msgPostedGrpHdr=nhb
+msgPostedSubBoardName=ng
+msgPostedOriginalAreaText=nc
+msgHasBeenSavedText=nhc
+msgAbortedText=nhm
+emptyMsgNotSentText=nhm
 genMsgErrorText=nhm
\ No newline at end of file
diff --git a/ctrl/SlyIceColors_Fire-N-Ice.cfg b/ctrl/SlyIceColors_Fire-N-Ice.cfg
index 7d4f22aefe859c68c8f9d71f01ad8313cb0aa088..1e7b7bff290eff15b246d47f96c3fe592e995bac 100644
--- a/ctrl/SlyIceColors_Fire-N-Ice.cfg
+++ b/ctrl/SlyIceColors_Fire-N-Ice.cfg
@@ -1,56 +1,61 @@
-; This is a color theme file for SlyEdit's Ice Style.
-; This color scheme mimics IceEdit/QuikEdit's "Fire & Ice" theme.
-
-; Text edit color
-TextEditColor=nw
-; The color to use for quoted lines in the message
-QuoteLineColor=nc
-
-; Border colors
-BorderColor1=nr
-BorderColor2=nrh
-KeyInfoLabelColor=yh
-
-; Colors for the top informational area
-TopInfoBkgColor=4
-TopLabelColor=yh
-TopLabelColonColor=kh
-TopToColor=wh
-TopFromColor=wh
-TopSubjectColor=wh
-TopTimeColor=wh
-TopTimeLeftColor=wh
-EditMode=yh
-
-; Colors for the quote window
-QuoteWinText=nhw
-QuoteLineHighlightColor=4hy
-QuoteWinBorderTextColor=nyh
-
-; Colors for the multi-choice options
-SelectedOptionBorderColor=nbh4
-SelectedOptionTextColor=nyh4
-UnselectedOptionBorderColor=nb
-UnselectedOptionTextColor=nw
-
-; Colors for the cross-post selection box
-crossPostBorder=nr
-crossPostBorderText=nbh
-crossPostMsgAreaNum=nhw
-crossPostMsgAreaNumHighlight=n4hw
-crossPostMsgAreaDesc=nc
-crossPostMsgAreaDescHighlight=n4c
-crossPostChk=nhy
-crossPostChkHighlight=n4hy
-crossPostMsgGrpMark=nhg
-crossPostMsgGrpMarkHighlight=n4hg
-
-; Colors for message saving and sub-board post info when exiting SlyEdit
-msgWillBePostedHdr=nc
-msgPostedGrpHdr=nhb
-msgPostedSubBoardName=ng
-msgPostedOriginalAreaText=nc
-msgHasBeenSavedText=nhc
-msgAbortedText=nhm
-emptyMsgNotSentText=nhm
+; This is a color theme file for SlyEdit's Ice Style.
+; This color scheme mimics IceEdit/QuikEdit's "Fire & Ice" theme.
+
+; Text edit color
+TextEditColor=nw
+; The color to use for quoted lines in the message
+QuoteLineColor=nc
+
+; Border colors
+BorderColor1=nr
+BorderColor2=nrh
+KeyInfoLabelColor=yh
+
+; Colors for the top informational area
+TopInfoBkgColor=4
+TopLabelColor=yh
+TopLabelColonColor=kh
+TopToColor=wh
+TopFromColor=wh
+TopSubjectColor=wh
+TopTimeColor=wh
+TopTimeLeftColor=wh
+EditMode=yh
+
+; Colors for the quote window
+QuoteWinText=nhw
+QuoteLineHighlightColor=4hy
+QuoteWinBorderTextColor=nyh
+
+; Colors for the multi-choice options
+SelectedOptionBorderColor=nbh4
+SelectedOptionTextColor=nyh4
+UnselectedOptionBorderColor=nb
+UnselectedOptionTextColor=nw
+
+; Color settings for list boxes
+listBoxBorder=nr
+listBoxBorderText=nbh
+
+; Colors for the cross-post selection box
+crossPostMsgAreaNum=nhw
+crossPostMsgAreaNumHighlight=n4hw
+crossPostMsgAreaDesc=nc
+crossPostMsgAreaDescHighlight=n4c
+crossPostChk=nhy
+crossPostChkHighlight=n4hy
+crossPostMsgGrpMark=nhg
+crossPostMsgGrpMarkHighlight=n4hg
+
+; Colors for the text replacement list
+txtReplacementList=nbh
+
+; Colors for message saving and sub-board post info when exiting SlyEdit
+msgWillBePostedHdr=nc
+msgPostedGrpHdr=nhb
+msgPostedSubBoardName=ng
+msgPostedOriginalAreaText=nc
+msgHasBeenSavedText=nhc
+msgAbortedText=nhm
+emptyMsgNotSentText=nhm
 genMsgErrorText=nhm
\ No newline at end of file
diff --git a/ctrl/SlyIceColors_GenericBlue.cfg b/ctrl/SlyIceColors_GenericBlue.cfg
index 6d3162ef630d40eba1ae7d9bce65dd6d5c26d913..9c120c67aa1a2cafda3c99f76406c9b0f0ae6bd0 100644
--- a/ctrl/SlyIceColors_GenericBlue.cfg
+++ b/ctrl/SlyIceColors_GenericBlue.cfg
@@ -1,56 +1,61 @@
-; This is a color theme file for SlyEdit's Ice Style.
-; This color scheme mimics IceEdit/QuikEdit's "Generic Blue" theme.
-
-; Text edit color
-TextEditColor=nw
-; The color to use for quoted lines in the message
-QuoteLineColor=nc
-
-; Border colors
-BorderColor1=nb
-BorderColor2=nb
-KeyInfoLabelColor=ch
-
-; Colors for the top informational area
-TopInfoBkgColor=4
-TopLabelColor=ch
-TopLabelColonColor=bh
-TopToColor=wh
-TopFromColor=wh
-TopSubjectColor=wh
-TopTimeColor=bh
-TopTimeLeftColor=bh
-EditMode=ch
-
-; Colors for the quote window
-QuoteWinText=nhw
-QuoteLineHighlightColor=4hc
-QuoteWinBorderTextColor=nch
-
-; Colors for the multi-choice options
-SelectedOptionBorderColor=nbh4
-SelectedOptionTextColor=nch4
-UnselectedOptionBorderColor=nw
-UnselectedOptionTextColor=nw
-
-; Colors for the cross-post selection box
-crossPostBorder=nb
-crossPostBorderText=nbh
-crossPostMsgAreaNum=nhw
-crossPostMsgAreaNumHighlight=n4hw
-crossPostMsgAreaDesc=nc
-crossPostMsgAreaDescHighlight=n4c
-crossPostChk=nhy
-crossPostChkHighlight=n4hy
-crossPostMsgGrpMark=nhg
-crossPostMsgGrpMarkHighlight=n4hg
-
-; Colors for message saving and sub-board post info when exiting SlyEdit
-msgWillBePostedHdr=nc
-msgPostedGrpHdr=nhb
-msgPostedSubBoardName=ng
-msgPostedOriginalAreaText=nc
-msgHasBeenSavedText=nhc
-msgAbortedText=nhm
-emptyMsgNotSentText=nhm
+; This is a color theme file for SlyEdit's Ice Style.
+; This color scheme mimics IceEdit/QuikEdit's "Generic Blue" theme.
+
+; Text edit color
+TextEditColor=nw
+; The color to use for quoted lines in the message
+QuoteLineColor=nc
+
+; Border colors
+BorderColor1=nb
+BorderColor2=nb
+KeyInfoLabelColor=ch
+
+; Colors for the top informational area
+TopInfoBkgColor=4
+TopLabelColor=ch
+TopLabelColonColor=bh
+TopToColor=wh
+TopFromColor=wh
+TopSubjectColor=wh
+TopTimeColor=bh
+TopTimeLeftColor=bh
+EditMode=ch
+
+; Colors for the quote window
+QuoteWinText=nhw
+QuoteLineHighlightColor=4hc
+QuoteWinBorderTextColor=nch
+
+; Colors for the multi-choice options
+SelectedOptionBorderColor=nbh4
+SelectedOptionTextColor=nch4
+UnselectedOptionBorderColor=nw
+UnselectedOptionTextColor=nw
+
+; Color settings for list boxes
+listBoxBorder=nb
+listBoxBorderText=nbh
+
+; Colors for the cross-post selection box
+crossPostMsgAreaNum=nhw
+crossPostMsgAreaNumHighlight=n4hw
+crossPostMsgAreaDesc=nc
+crossPostMsgAreaDescHighlight=n4c
+crossPostChk=nhy
+crossPostChkHighlight=n4hy
+crossPostMsgGrpMark=nhg
+crossPostMsgGrpMarkHighlight=n4hg
+
+; Colors for the text replacement list
+txtReplacementList=nbh
+
+; Colors for message saving and sub-board post info when exiting SlyEdit
+msgWillBePostedHdr=nc
+msgPostedGrpHdr=nhb
+msgPostedSubBoardName=ng
+msgPostedOriginalAreaText=nc
+msgHasBeenSavedText=nhc
+msgAbortedText=nhm
+emptyMsgNotSentText=nhm
 genMsgErrorText=nhm
\ No newline at end of file
diff --git a/ctrl/SlyIceColors_ShadesOfGrey.cfg b/ctrl/SlyIceColors_ShadesOfGrey.cfg
index 1b960059a5edaad1dea430dc3450bd59888bea19..1b4f42a4e15126b4879887b37e705e520705696c 100644
--- a/ctrl/SlyIceColors_ShadesOfGrey.cfg
+++ b/ctrl/SlyIceColors_ShadesOfGrey.cfg
@@ -1,56 +1,61 @@
-; This is a color theme file for SlyEdit's Ice Style.
-; This color scheme mimics IceEdit/QuikEdit's "Shades of Gray" theme.
-
-; Text edit color
-TextEditColor=nw
-; The color to use for quoted lines in the message
-QuoteLineColor=nkh
-
-; Border colors
-BorderColor1=nkh
-BorderColor2=nb
-KeyInfoLabelColor=nw
-
-; Colors for the top informational area
-TopInfoBkgColor=4
-TopLabelColor=nw4
-TopLabelColonColor=bh
-TopToColor=wh
-TopFromColor=wh
-TopSubjectColor=wh
-TopTimeColor=wh
-TopTimeLeftColor=wh
-EditMode=nw4
-
-; Colors for the quote window
-QuoteWinText=nhw
-QuoteLineHighlightColor=7hw
-QuoteWinBorderTextColor=nw
-
-; Colors for the multi-choice options
-SelectedOptionBorderColor=nwh7
-SelectedOptionTextColor=nwh7
-UnselectedOptionBorderColor=nkh
-UnselectedOptionTextColor=nwh
-
-; Colors for the cross-post selection box
-crossPostBorder=nhk
-crossPostBorderText=nw
-crossPostMsgAreaNum=nhw
-crossPostMsgAreaNumHighlight=n4hw
-crossPostMsgAreaDesc=nc
-crossPostMsgAreaDescHighlight=n4c
-crossPostChk=nhy
-crossPostChkHighlight=n4hy
-crossPostMsgGrpMark=nhg
-crossPostMsgGrpMarkHighlight=n4hg
-
-; Colors for message saving and sub-board post info when exiting SlyEdit
-msgWillBePostedHdr=nc
-msgPostedGrpHdr=nhb
-msgPostedSubBoardName=ng
-msgPostedOriginalAreaText=nc
-msgHasBeenSavedText=nhc
-msgAbortedText=nhm
-emptyMsgNotSentText=nhm
+; This is a color theme file for SlyEdit's Ice Style.
+; This color scheme mimics IceEdit/QuikEdit's "Shades of Gray" theme.
+
+; Text edit color
+TextEditColor=nw
+; The color to use for quoted lines in the message
+QuoteLineColor=nkh
+
+; Border colors
+BorderColor1=nkh
+BorderColor2=nb
+KeyInfoLabelColor=nw
+
+; Colors for the top informational area
+TopInfoBkgColor=4
+TopLabelColor=nw4
+TopLabelColonColor=bh
+TopToColor=wh
+TopFromColor=wh
+TopSubjectColor=wh
+TopTimeColor=wh
+TopTimeLeftColor=wh
+EditMode=nw4
+
+; Colors for the quote window
+QuoteWinText=nhw
+QuoteLineHighlightColor=7hw
+QuoteWinBorderTextColor=nw
+
+; Colors for the multi-choice options
+SelectedOptionBorderColor=nwh7
+SelectedOptionTextColor=nwh7
+UnselectedOptionBorderColor=nkh
+UnselectedOptionTextColor=nwh
+
+; Color settings for list boxes
+listBoxBorder=nhk
+listBoxBorderText=nw
+
+; Colors for the cross-post selection box
+crossPostMsgAreaNum=nhw
+crossPostMsgAreaNumHighlight=n4hw
+crossPostMsgAreaDesc=nc
+crossPostMsgAreaDescHighlight=n4c
+crossPostChk=nhy
+crossPostChkHighlight=n4hy
+crossPostMsgGrpMark=nhg
+crossPostMsgGrpMarkHighlight=n4hg
+
+; Colors for the text replacement list
+txtReplacementList=nw
+
+; Colors for message saving and sub-board post info when exiting SlyEdit
+msgWillBePostedHdr=nc
+msgPostedGrpHdr=nhb
+msgPostedSubBoardName=ng
+msgPostedOriginalAreaText=nc
+msgHasBeenSavedText=nhc
+msgAbortedText=nhm
+emptyMsgNotSentText=nhm
 genMsgErrorText=nhm
\ No newline at end of file
diff --git a/docs/SlyEdit_DD_Message_Lister_notes.txt b/docs/SlyEdit_DD_Message_Lister_notes.txt
index 387b64a6f7ac11e4d534789669c64a962857821b..031ecf00a1018189330363a0ba215a78697a08b8 100644
--- a/docs/SlyEdit_DD_Message_Lister_notes.txt
+++ b/docs/SlyEdit_DD_Message_Lister_notes.txt
@@ -14,6 +14,7 @@ Message Lister with the recent versions of SlyEdit:
 
 SlyEdit      Digital Distortion message Lister
 -------      ---------------------------------
+1.29         1.36+
 1.28         1.36+
 1.27         1.36+
 1.26         1.35
diff --git a/docs/SlyEdit_ReadMe.txt b/docs/SlyEdit_ReadMe.txt
index da96ae96e3ac24e85865ee5360866cf3c8cbbc5d..5564d1a099fe6824f81874d8d50a9dea2c018ac6 100644
--- a/docs/SlyEdit_ReadMe.txt
+++ b/docs/SlyEdit_ReadMe.txt
@@ -1,6 +1,6 @@
                          SlyEdit message editor
-                              Version 1.28
-                        Release date: 2013-08-24
+                              Version 1.29
+                        Release date: 2013-09-02
 
                                   by
 
@@ -18,15 +18,16 @@ Upgrading.txt.
 
 Contents
 ========
-1. Disclaimer
-2. Introduction
-3. Installation & Setup
-4. Features
-5. Digital Distortion Message Lister note
-6. Configuration file
-7. Ice-style Color Theme Settings
-8. DCT-style Color Theme Settings
-9. Common colors (appearing in both Ice and DCT color theme files)
+ 1. Disclaimer
+ 2. Introduction
+ 3. Installation & Setup
+ 4. Features
+ 5. Digital Distortion Message Lister note
+ 6. Configuration file
+ 7. Ice-style Color Theme Settings
+ 8. DCT-style Color Theme Settings
+ 9. Common colors (appearing in both Ice and DCT color theme files)
+10. Text replacements (AKA Macros)
 
 
 1. Disclaimer
@@ -195,8 +196,10 @@ Help keys                                     Slash commands (on blank line)
 Ctrl-G       : General help                 � /A     : Abort
 Ctrl-P       : Command key help             � /S     : Save
 Ctrl-R       : Program information          � /Q     : Quote message
+Ctrl-T       : List text replacements       � /T     : List text replacements
                                             � /C     : Cross-post selection
 
+
 Command/edit keys
 -----------------
 Ctrl-A       : Abort message                � Ctrl-W : Page up
@@ -300,6 +303,15 @@ allowCrossPosting                 Whether or not to allow cross-posting
                                   messages into different/multiple message
                                   sub-boards.  Valid values are true and false.
 
+enableTextReplacements            Toggles the use of text replacements (AKA
+                                  macros) as the user types a message.  Valid
+                                  values are true, false, and regex.  If regex
+                                  is used, text replacements will be enabled
+                                  and used as regular expressions as
+                                  implemented by JavaScript.  For more
+                                  information, see section 10 (Text
+                                  replacements (AKA Macros)).
+
 Ice colors
 ----------
 Setting                           Description
@@ -523,11 +535,19 @@ MenuHotkeys                       The color to use for the hotkey characters in
 
 9. Common colors (appearing in both Ice and DCT color theme files)
 ==================================================================
-crossPostBorder                   The color to use for the border of the cross-
-                                  post area selection box
-
-crossPostBorderText               The color to use for the top border text of
-                                  the cross-post area selection box
+listBoxBorder                     The color to use for the border of list
+                                  boxes, such as the cross-post area selection
+                                  box and the text replacements list box.  Note
+                                  that this setting replaces crossPostBorder,
+                                  which was used in previous versions of
+                                  SlyEdit.
+
+listBoxBorderText                 The color to use for text in the borders of
+                                  list boxes, such as the cross-post area
+                                  selection box and the text replacements list
+                                  box.  Note that this setting replaces
+                                  crossPostBorderText, which was used in
+                                  previous versions of SlyEdit.
 
 crossPostMsgAreaNum               The color to use for the message group/sub-
                                   board numbers in the cross-post area
@@ -590,4 +610,97 @@ emptyMsgNotSentText               The color to use for the Message Not Sent
                                   text when exiting SlyEdit
 
 genMsgErrorText                   The color to use for general message error
-                                  text when exiting SlyEdit
\ No newline at end of file
+                                  text when exiting SlyEdit
+
+10. Text replacements (AKA Macros)
+==================================
+SlyEdit version 1.29 added text replacements (AKA Macros), which lets you (the
+sysop) define words to be replaced with other text as the user types a message.
+This feature can be used, for instance, to replace commonly misspelled words
+with their correct versions or to replace swear words with less offensive
+words.  This feature is toggled by the enableTextReplacements option in
+SlyEdit.cfg can can have one of three values:
+false : Text replacement is disabled
+true  : Text replacement is enabled and performed as literal search and replace
+regex : Text replacement is enabled using regular expressions as implemented by
+        JavaScript (more accurately, Synchronet's JavaScript interpreter, which
+        at the time of this writing is Mozilla's JavaScript engine (AKA
+        JavaScript-C or "SpiderMonkey").
+
+The text searches are performed on single words only, as the user types the
+message, and are replaced by whatever text you configure for the word.  The
+configuration for text replacing is read from a configuration file called
+SlyEdit_TextReplacements.cfg, which is plain text and can be placed in either
+sbbs/ctrl or in the same directory as SlyEdit's .js files (SlyEdit.js,
+SlyEdit_Misc.js, etc.).  Each line in SlyEdit_TextReplacements.cfg needs to
+have the following format:
+originalWord=replacementText
+where originalWord is the word to be replaced, and replacementText is the text
+to replace the word with.
+
+The options for the enableTextReplacements configuration optoin are explained
+in more detail below:
+
+- false: Text replacing is disabled
+
+- true: Literal text search and replace - The words will be matched literally
+and replaced with the text on the right side of the = in
+SlyEdit_TextReplacements.cfg.  In this mode, word matching is not
+case-sensitive.  The words can have both uppercase and lowercase letters and
+will still be matched to the words configured in SlyEdit_TextReplacements.cfg.
+In this mode, if the first letter of the original word is uppercase, then the
+first letter of the replaced text will also be uppercase.
+While this works, one drawback to literal text searching is that it won't
+replace a word if there are punctuation characters or other characters around
+the word or if the word is misspelled.
+
+- regex: With this option, SlyEdit will treat the word searches (on the left
+side of the = in SlyEdit_TextReplacements.cfg) as regular expressions, as
+implemented by JavaScript.  When using regular expressions, SlyEdit will start
+trying all the regular expressions provided and apply only the first one that
+changes the text and will stop there.
+Regular expressions offer a more flexible way to serach and replace text.  For
+example, in case a word is surrounded by punctuation or other characters, a
+regular expression can be given that will still match the word.  For example,
+you might want a regular expression that changes "teh" to "the" (since "teh"
+is a common misspelling of "the").  To ensure that the word is replaced even if
+the word has other characters around it, you could use a regular exprssion and
+replacement such as this:
+(.*)teh(.*)=$1the$2
+That way, even if the word is enclosed in quotes (such as "teh"), the word will
+still be converted to "the".  And to preserve the first letter's case (if it's
+uppercase or lowercase), this regular expression and replacement string would
+handle that:
+(.*)([tT])eh(.*)=$1$2he$3
+SlyEdit applies the regular expressions on a per-word basis; that is, the
+expression .* in a regular expression will match any characters only in the
+last word the user typed, not the entire line.
+
+For more information on regular expressions, the
+following are some web pages that explain them:
+
+General information about regular expressions:
+http://www.regular-expressions.info/tutorial.html
+
+Information on text grouping and backreferencing with parenthesis (the page
+calls them "round brackets") - A very powerful feature of regular expressions:
+http://www.regular-expressions.info/brackets.html
+
+General information about regular expressions geared toward JavaScript:
+http://www.javascriptkit.com/javatutors/re.shtml
+
+Information about using backreferences with regular expressions in JavaScript:
+http://stackoverflow.com/questions/2447915/javascript-string-regex-backreferences
+
+JavaScript-specific information on regular exprssions:
+http://www.w3schools.com/js/js_obj_regexp.asp
+http://www.w3schools.com/jsref/jsref_obj_regexp.asp
+
+One nice thing about JavaScript's implementation (among others) of regular
+expressions is that it supports the use of parentheses for character grouping
+and backreferencing to place a portion of the matched text in the replacement
+text.  In JavaScript, each numbered capture buffer is preceded by a dollar sign
+($).  For example, the regular expression (darn) will match the word "darn" and
+store it in buffer 1, and in JavaScript (and with SlyEdit's search and
+replace), you would use $1 to refer to the word "darn".  For example, for
+(darn), the replacement $1it would replace the word "darn" with "darnit".
\ No newline at end of file
diff --git a/docs/SlyEdit_Upgrading.txt b/docs/SlyEdit_Upgrading.txt
index 21475d060cc8695bdba50d1c09f0fa82657382dd..d508ae4d43862d3eb8376292c0c989cad638c72a 100644
--- a/docs/SlyEdit_Upgrading.txt
+++ b/docs/SlyEdit_Upgrading.txt
@@ -41,6 +41,46 @@ configuration should look like this (ICE mode is used here):
 
 2. Notes and new configuration file settings
 ============================================
+Upgrading to version 1.29
+-------------------------
+A new "text replacements" (AKA macros) feature has been added.  This feature
+lets you (the sysop) define words to be replaced with other text.  This feature
+can be used, for instance, to replace commonly misspelled words with their
+correct versions or to replace swear words with less offensive words as the user
+types a message.  For more details, see the section regarding text replacements
+in the "Read Me.txt" file.
+
+
+The color settings crossPostBorder and crossPostBorderText have been
+renamed to listBoxBorder and listBoxBorderText.  crossPostBorder and
+crossPostBorderText will still work, but if listBoxBorder and
+listBoxBorderText are used in your theme configuration files, those will be
+used instead.
+
+Also, the following color setting has been added (for both the DCT-style and
+Ice-style theme files):
+txtReplacementList         The color to use for the text replacements in the
+                           text replacement list.
+
+Upgrading to version 1.28
+-------------------------
+New general color settings (for both the DCT-style and Ice-style theme files):
+msgWillBePostedHdr         The color to use for the text �Your message will be posted into the following area(s)� text when exiting SlyEdit
+
+msgPostedGrpHdr            The color to use for the group name header when listing the message's posted message areas when exiting SlyEdit
+
+msgPostedSubBoardName      The color to use for the message sub-boards when listing the message's posted message areas when exiting SlyEdit
+
+msgPostedOriginalAreaText  The color to use for the text �(original message area)� when listing the message's posted message areas when exiting SlyEdit
+
+msgHasBeenSavedText        The color to use for the text �The message has been saved.� when exiting SlyEdit
+
+msgAbortedText             The color to use for the Message Aborted text when exiting SlyEdit
+
+emptyMsgNotSentText        The color to use for the Message Not Sent text when exiting SlyEdit
+
+genMsgErrorText            The color to use for general message error text when exiting SlyEdit
+
 Upgrading to version 1.20
 -------------------------
 Version 1.20 added a cross-posting feature, and there is a new configuration
diff --git a/exec/SlyEdit.js b/exec/SlyEdit.js
index d22665d6a31eaf92f3378c0bb3d7e9b11c6c7471..5870c9ea2288de0e9b6323b878a3542451504962 100644
--- a/exec/SlyEdit.js
+++ b/exec/SlyEdit.js
@@ -13,43 +13,31 @@
  * 2009-08-22 Eric Oulashin     Version 1.00
  *                              Initial public release
  * ....Removed some comments...
- * 2013-05-16 Eric Oulashin     Version 1.26 beta
- *                              Fix for getting author initials for quote lines:
- *                              Updated to use bbs.msg_number only for Synchronet
- *                              3.16 builds starting on May 12, 2013 and to use
- *                              bbs.smb_curmsg before that build date.  May 12,
- *                              2013 was right after Digital Man put in his change
- *                              to make bbs.msg_number work in JavaScript scripts,
- *                              and it's accurate in all situations.
- * 2013-05-17 Eric Oulashin     Version 1.26
- *                              Made another change to make sure the call
- *                              to bbs.get_msg_header() in getFromNameForCurMsg()
- *                              in SlyEdit_Misc.js had the correct first parameter
- *                              to specify whether the message number is an
- *                              offset.  Released as version 1.26 after some
- *                              testing.
- * 2013-05-24 Eric Oulashin     Version 1.27
- *                              Updated to check whether bbs.msg_number is > 0
- *                              rather than checking the Synchronet version
- *                              >= 3.16 & build date at least May 12, 2013
- *                              to determine whether to use bbs.msg_number or
- *                              bbs.smb_curmsg.  Sysops who use Digital
- *                              Message Lister must now update the message
- *                              lister to version 1.36 or newer to properly
- *                              work with this version of SlyEdit.  Also,
- *                              in SlyEdit_Misc.js, updated to add one more
- *                              data member to the return object:
- *                              msgNumIsOffset, which stores whether or not the
- *                              message number is an offset.
- * 2013-08-23 Eric Oulashin     Version 1.28 Beta (start)
- *                              Added 8 new color settings for the text displayed
- *                              when SlyEdit exits regarding which message
- *                              areas the message was posted in, and the saved
- *                              and abort messages.
  * 2013-08-24 Eric Oulashin     Version 1.28
  *                              Bug fix: SlyEdit sometimes didn't quote the last
  *                              line of a message when using author's initials.
  *                              This has been fixed.
+ *                              Added 8 new color settings for the text displayed
+ *                              when SlyEdit exits regarding which message
+ *                              areas the message was posted in, and the saved
+ *                              and abort messages.
+ * 2013-08-28 Eric Oulashin     Version 1.29 Beta (started)
+ *                              Started working on a macros feature as in
+ *                              QuikEdit.  I'll call it text replacements.
+ * 2013-08-31 Eric Oulashin     Worked on doPrintableChar() for text replacements
+ *                              and started working on doEnterKey() for text
+ *                              replacements.
+ * 2013-09-01 Eric Oulashin     Worked on doEnterKey() for text replacements.
+ *                              Started working on a new function, listTextReplacements().
+ * 2013-09-02 Eric Oulashin     Version 1.29
+ *                              Worked on doEnterKey() for text replacements to
+ *                              improve the line splitting if the new text has
+ *                              spaces in it.  Updated the text replacment list
+ *                              box in the UI to show the page number and total
+ *                              number of pages.  Updated so that the command to
+ *                              list the text replacements will only work if
+ *                              text replacements is enabled.
+ *                              After some more testing, I decided to release 1.29 today.
  */
 
 /* Command-line arguments:
@@ -73,13 +61,13 @@ var EDITOR_STYLE = "DCT";
 // The second command-line argument (argv[1]) can change this.
 if (typeof(argv[1]) != "undefined")
 {
-	var styleUpper = argv[1].toUpperCase();
-	// Make sure styleUpper is valid before setting EDITOR_STYLE.
-	if (styleUpper == "DCT")
-		EDITOR_STYLE = "DCT";
-	else if (styleUpper == "ICE")
-		EDITOR_STYLE = "ICE";
-	else if (styleUpper == "RANDOM")
+   var styleUpper = argv[1].toUpperCase();
+   // Make sure styleUpper is valid before setting EDITOR_STYLE.
+   if (styleUpper == "DCT")
+      EDITOR_STYLE = "DCT";
+   else if (styleUpper == "ICE")
+      EDITOR_STYLE = "ICE";
+   else if (styleUpper == "RANDOM")
       EDITOR_STYLE = (Math.floor(Math.random()*2) == 0) ? "DCT" : "ICE";
 }
 
@@ -103,27 +91,27 @@ const EDITOR_PROGRAM_NAME = "SlyEdit";
 if (system.version_num < 31400)
 {
   console.print("n");
-	console.crlf();
-	console.print("nhyi* Warning:nhw " + EDITOR_PROGRAM_NAME);
-	console.print(" " + "requires version g3.14w or");
-	console.crlf();
-	console.print("higher of Synchronet.  This BBS is using version g");
-	console.print(system.version + "w.  Please notify the sysop.");
-	console.crlf();
-	console.pause();
-	exit(1); // 1: Aborted
+   console.crlf();
+   console.print("nhyi* Warning:nhw " + EDITOR_PROGRAM_NAME);
+   console.print(" " + "requires version g3.14w or");
+   console.crlf();
+   console.print("higher of Synchronet.  This BBS is using version g");
+   console.print(system.version + "w.  Please notify the sysop.");
+   console.crlf();
+   console.pause();
+   exit(1); // 1: Aborted
 }
 // If the user's terminal doesn't support ANSI, then exit.
 if (!console.term_supports(USER_ANSI))
 {
-	console.print("n\r\nhyERROR: w" + EDITOR_PROGRAM_NAME +
-	              " requires an ANSI terminal.n\r\np");
-	exit(1); // 1: Aborted
+   console.print("n\r\nhyERROR: w" + EDITOR_PROGRAM_NAME +
+                 " requires an ANSI terminal.n\r\np");
+   exit(1); // 1: Aborted
 }
 
 // Constants
-const EDITOR_VERSION = "1.28";
-const EDITOR_VER_DATE = "2013-08-24";
+const EDITOR_VERSION = "1.29";
+const EDITOR_VER_DATE = "2013-09-02";
 
 
 // Program variables
@@ -140,8 +128,8 @@ var gEditRight = gEditLeft + 79; // Based on gEditLeft being 1-based
 // If the screen has less than 80 columns, then use the whole screen.
 if (console.screen_columns < 80)
 {
-	gEditLeft = 1;
-	gEditRight = console.screen_columns;
+   gEditLeft = 1;
+   gEditRight = console.screen_columns;
 }
 
 // Colors
@@ -238,41 +226,41 @@ var fpHandleESCMenu = null;
 var fpDisplayTime = null;
 if (EDITOR_STYLE == "DCT")
 {
-  load(gStartupPath + "SlyEdit_DCTStuff.js");
-	gEditTop = 6;
-	gQuoteWinTextColor = gConfigSettings.DCTColors.QuoteWinText;
-	gQuoteLineHighlightColor = gConfigSettings.DCTColors.QuoteLineHighlightColor;
-	gTextAttrs = gConfigSettings.DCTColors.TextEditColor;
-	gQuoteLineColor = gConfigSettings.DCTColors.QuoteLineColor;
-
-	// Function pointers for the DCTEdit-style screen update functions
-	fpDrawQuoteWindowTopBorder = DrawQuoteWindowTopBorder_DCTStyle;
-	fpDisplayTextAreaBottomBorder = DisplayTextAreaBottomBorder_DCTStyle;
-	fpDrawQuoteWindowBottomBorder = DrawQuoteWindowBottomBorder_DCTStyle;
-	fpRedrawScreen = redrawScreen_DCTStyle;
-	fpUpdateInsertModeOnScreen = updateInsertModeOnScreen_DCTStyle;
-	fpDisplayBottomHelpLine = DisplayBottomHelpLine_DCTStyle;
-	fpHandleESCMenu = handleDCTESCMenu;
-	fpDisplayTime = displayTime_DCTStyle;
+   load(gStartupPath + "SlyEdit_DCTStuff.js");
+   gEditTop = 6;
+   gQuoteWinTextColor = gConfigSettings.DCTColors.QuoteWinText;
+   gQuoteLineHighlightColor = gConfigSettings.DCTColors.QuoteLineHighlightColor;
+   gTextAttrs = gConfigSettings.DCTColors.TextEditColor;
+   gQuoteLineColor = gConfigSettings.DCTColors.QuoteLineColor;
+
+   // Function pointers for the DCTEdit-style screen update functions
+   fpDrawQuoteWindowTopBorder = DrawQuoteWindowTopBorder_DCTStyle;
+   fpDisplayTextAreaBottomBorder = DisplayTextAreaBottomBorder_DCTStyle;
+   fpDrawQuoteWindowBottomBorder = DrawQuoteWindowBottomBorder_DCTStyle;
+   fpRedrawScreen = redrawScreen_DCTStyle;
+   fpUpdateInsertModeOnScreen = updateInsertModeOnScreen_DCTStyle;
+   fpDisplayBottomHelpLine = DisplayBottomHelpLine_DCTStyle;
+   fpHandleESCMenu = handleDCTESCMenu;
+   fpDisplayTime = displayTime_DCTStyle;
 }
 else if (EDITOR_STYLE == "ICE")
 {
-  load(gStartupPath + "SlyEdit_IceStuff.js");
-	gEditTop = 5;
-	gQuoteWinTextColor = gConfigSettings.iceColors.QuoteWinText;
-	gQuoteLineHighlightColor = gConfigSettings.iceColors.QuoteLineHighlightColor;
-	gTextAttrs = gConfigSettings.iceColors.TextEditColor;
-	gQuoteLineColor = gConfigSettings.iceColors.QuoteLineColor;
-
-	// Function pointers for the IceEdit-style screen update functions
-	fpDrawQuoteWindowTopBorder = DrawQuoteWindowTopBorder_IceStyle;
-	fpDisplayTextAreaBottomBorder = DisplayTextAreaBottomBorder_IceStyle;
-	fpDrawQuoteWindowBottomBorder = DrawQuoteWindowBottomBorder_IceStyle;
-	fpRedrawScreen = redrawScreen_IceStyle;
-	fpUpdateInsertModeOnScreen = updateInsertModeOnScreen_IceStyle;
-	fpDisplayBottomHelpLine = DisplayBottomHelpLine_IceStyle;
-	fpHandleESCMenu = handleIceESCMenu;
-	fpDisplayTime = displayTime_IceStyle;
+   load(gStartupPath + "SlyEdit_IceStuff.js");
+   gEditTop = 5;
+   gQuoteWinTextColor = gConfigSettings.iceColors.QuoteWinText;
+   gQuoteLineHighlightColor = gConfigSettings.iceColors.QuoteLineHighlightColor;
+   gTextAttrs = gConfigSettings.iceColors.TextEditColor;
+   gQuoteLineColor = gConfigSettings.iceColors.QuoteLineColor;
+
+   // Function pointers for the IceEdit-style screen update functions
+   fpDrawQuoteWindowTopBorder = DrawQuoteWindowTopBorder_IceStyle;
+   fpDisplayTextAreaBottomBorder = DisplayTextAreaBottomBorder_IceStyle;
+   fpDrawQuoteWindowBottomBorder = DrawQuoteWindowBottomBorder_IceStyle;
+   fpRedrawScreen = redrawScreen_IceStyle;
+   fpUpdateInsertModeOnScreen = updateInsertModeOnScreen_IceStyle;
+   fpDisplayBottomHelpLine = DisplayBottomHelpLine_IceStyle;
+   fpHandleESCMenu = handleIceESCMenu;
+   fpDisplayTime = displayTime_IceStyle;
 }
 
 // Temporary (for testing): Make the edit area small
@@ -311,35 +299,41 @@ function clearEditAreaBuffer()
       gEditAreaBuffer[lineNum] = "";
 }
 clearEditAreaBuffer();
+// gTxtreplacements will be an associative array that stores words (in upper
+// case) to be replaced with other words.
+var gNumTxtReplacements = 0;
+var gTxtReplacements = new Object();
+if (gConfigSettings.enableTextReplacements)
+   gNumTxtReplacements = populateTxtReplacements(gTxtReplacements, gConfigSettings.textReplacementsUseRegex);
 
 // Set some stuff up for message editing
 var gUseQuotes = true;
 var gInputFilename = file_getcase(system.node_dir + "QUOTES.TXT");
 if (gInputFilename == undefined)
 {
-	gUseQuotes = false;
-	if ((argc > 0) && (gInputFilename == undefined))
-		gInputFilename = argv[0];
+   gUseQuotes = false;
+   if ((argc > 0) && (gInputFilename == undefined))
+      gInputFilename = argv[0];
 }
 else
 {
-	var all_files = directory(system.node_dir + "*");
-	var newest_filedate = -Infinity;
-	var newest_filename;
-	for (var file in all_files)
-	{
-		if (all_files[file].search(/quotes.txt$/i) != -1)
-		{
-			var this_date = file_date(all_files[file]);
-			if (this_date > newest_filedate)
-			{
-				newest_filename = all_files[file];
-				newest_filedate = this_date;
-			}
-		}
-	}
-	if (newest_filename != undefined)
-		gInputFilename = newest_filename;
+   var all_files = directory(system.node_dir + "*");
+   var newest_filedate = -Infinity;
+   var newest_filename;
+   for (var file in all_files)
+   {
+      if (all_files[file].search(/quotes.txt$/i) != -1)
+      {
+         var this_date = file_date(all_files[file]);
+         if (this_date > newest_filedate)
+         {
+            newest_filename = all_files[file];
+            newest_filedate = this_date;
+         }
+      }
+   }
+   if (newest_filename != undefined)
+      gInputFilename = newest_filename;
 }
 
 var gOldStatus = bbs.sys_status;
@@ -363,25 +357,25 @@ var dropFileTime = -Infinity;
 var dropFileName = file_getcase(system.node_dir + "msginf");
 if (dropFileName != undefined)
 {
-	if (file_date(dropFileName) >= dropFileTime)
-	{
-		var dropFile = new File(dropFileName);
-		if (dropFile.exists && dropFile.open("r"))
-		{
-			dropFileTime = dropFile.date;
-			info = dropFile.readAll();
-			dropFile.close();
-
-			gFromName = info[0];
-			gToName = info[1];
-			gMsgSubj = info[2];
-			gMsgArea = info[4];
+   if (file_date(dropFileName) >= dropFileTime)
+   {
+      var dropFile = new File(dropFileName);
+      if (dropFile.exists && dropFile.open("r"))
+      {
+         dropFileTime = dropFile.date;
+         info = dropFile.readAll();
+         dropFile.close();
+
+         gFromName = info[0];
+         gToName = info[1];
+         gMsgSubj = info[2];
+         gMsgArea = info[4];
 
       // Now that we know the name of the message area
       // that the message is being posted in, call
       // getCurMsgInfo() to set gMsgAreaInfo.
-			gMsgAreaInfo = getCurMsgInfo(gMsgArea);
-			setMsgAreaInfoObj = true;
+         gMsgAreaInfo = getCurMsgInfo(gMsgArea);
+         setMsgAreaInfoObj = true;
 
       // If we're configured to use poster's initials in the
       // quote lines, then do it.
@@ -407,22 +401,22 @@ if (dropFileName != undefined)
         gQuotePrefix = "";
         if (gConfigSettings.indentQuoteLinesWithInitials)
           gQuotePrefix = " ";
-				// Use the initials or first 2 characters from the
-				// quoted name for gQuotePrefix.
-				var spaceIndex = quotedName.indexOf(" ");
-				if (spaceIndex > -1) // If a space exists, use the initials
-				{
+            // Use the initials or first 2 characters from the
+            // quoted name for gQuotePrefix.
+            var spaceIndex = quotedName.indexOf(" ");
+            if (spaceIndex > -1) // If a space exists, use the initials
+            {
           gQuotePrefix += quotedName.charAt(0).toUpperCase();
-					if (quotedName.length > spaceIndex+1)
-						gQuotePrefix += quotedName.charAt(spaceIndex+1).toUpperCase();
-					gQuotePrefix += "> ";
-				}
-				else // A space doesn't exist; use the first 2 letters
-					gQuotePrefix += quotedName.substr(0, 2) + "> ";
+               if (quotedName.length > spaceIndex+1)
+                  gQuotePrefix += quotedName.charAt(spaceIndex+1).toUpperCase();
+               gQuotePrefix += "> ";
+            }
+            else // A space doesn't exist; use the first 2 letters
+               gQuotePrefix += quotedName.substr(0, 2) + "> ";
+      }
       }
-		}
-	}
-	file_remove(dropFileName);
+   }
+   file_remove(dropFileName);
 }
 // If gMsgAreaInfo hasn't been set yet, then set it.
 if (!setMsgAreaInfoObj)
@@ -441,10 +435,10 @@ if (postingInMsgSubBoard(gMsgArea))
 var inputFile = new File(gInputFilename);
 if (inputFile.open("r", false))
 {
-	// Read into the gQuoteLines or gEditLines array, depending on the value
-	// of gUseQuotes.  Use a buffer size that should be long enough.
-	if (gUseQuotes)
-	{
+   // Read into the gQuoteLines or gEditLines array, depending on the value
+   // of gUseQuotes.  Use a buffer size that should be long enough.
+   if (gUseQuotes)
+   {
       var textLine = null;  // Line of text read from the quotes file
       while (!inputFile.eof)
       {
@@ -478,45 +472,45 @@ if (inputFile.open("r", false))
            gQuoteLines[i] = quote_msg(gQuoteLines[i], maxQuoteLineWidth, gQuotePrefix);
        }
      }
-	}
-	else
-	{
-		var textLine = null;
-		while (!inputFile.eof)
-		{
-			textLine = new TextLine();
-			textLine.text = inputFile.readln(2048);
-			if (typeof(textLine.text) == "string")
-				textLine.text = strip_ctrl(textLine.text);
-			else
-				textLine.text = "";
-			textLine.hardNewlineEnd = true;
-			// If there would still be room on the line for at least
-			// 1 more character, then add a space to the end of the
-			// line.
-			if (textLine.text.length < console.screen_columns-1)
-				textLine.text += " ";
-			gEditLines.push(textLine);
-		}
-
-		// If the last edit line is undefined (which is possible after reading the end
-		// of the quotes file), then remove it from gEditLines.
-		if (gEditLines.length > 0)
-		{
-			if (gEditLines.length > 0)
-			{
-				var lastQuoteLineIndex = gEditLines.length - 1;
-				if (gEditLines[lastQuoteLineIndex].text == undefined)
-					gEditLines.splice(lastQuoteLineIndex, 1);
-			}
-		}
-	}
-	inputFile.close();
+   }
+   else
+   {
+      var textLine = null;
+      while (!inputFile.eof)
+      {
+         textLine = new TextLine();
+         textLine.text = inputFile.readln(2048);
+         if (typeof(textLine.text) == "string")
+            textLine.text = strip_ctrl(textLine.text);
+         else
+            textLine.text = "";
+         textLine.hardNewlineEnd = true;
+         // If there would still be room on the line for at least
+         // 1 more character, then add a space to the end of the
+         // line.
+         if (textLine.text.length < console.screen_columns-1)
+            textLine.text += " ";
+         gEditLines.push(textLine);
+      }
+
+      // If the last edit line is undefined (which is possible after reading the end
+      // of the quotes file), then remove it from gEditLines.
+      if (gEditLines.length > 0)
+      {
+         if (gEditLines.length > 0)
+         {
+            var lastQuoteLineIndex = gEditLines.length - 1;
+            if (gEditLines[lastQuoteLineIndex].text == undefined)
+               gEditLines.splice(lastQuoteLineIndex, 1);
+         }
+      }
+   }
+   inputFile.close();
 }
 
 // If the subject is blank, set it to something.
 if (gMsgSubj == "")
-	gMsgSubj = gToName.replace(/^.*[\\\/]/,'');
+   gMsgSubj = gToName.replace(/^.*[\\\/]/,'');
 // Store a copy of the current subject (possibly allowing the user to
 // change the subject in the future)
 var gOldSubj = gMsgSubj;
@@ -767,81 +761,82 @@ exit(exitCode);
 // Edit mode & input loop
 function doEditLoop()
 {
-	// Return codes:
-	// 0: Success
-	// 1: Aborted
-	var returnCode = 0;
+   // Return codes:
+   // 0: Success
+   // 1: Aborted
+   var returnCode = 0;
 
   // Set the shortcut keys.  Note: Avoid CTRL_H because that
-  // backspace.
-	const ABORT_KEY             = CTRL_A;
-	const CROSSPOST_KEY         = CTRL_C;
-	const DELETE_LINE_KEY       = CTRL_D;
-	const GENERAL_HELP_KEY      = CTRL_G;
-	const TOGGLE_INSERT_KEY     = CTRL_I;
-	const CHANGE_COLOR_KEY      = CTRL_K;
-	const FIND_TEXT_KEY         = CTRL_N;
-	const IMPORT_FILE_KEY       = CTRL_O;
-	const CMDLIST_HELP_KEY      = CTRL_P;
-	const QUOTE_KEY             = CTRL_Q;
-	const PROGRAM_INFO_HELP_KEY = CTRL_R;
-	const PAGE_DOWN_KEY         = CTRL_S;
-	const PAGE_UP_KEY           = CTRL_W;
-	const EXPORT_FILE_KEY       = CTRL_X;
-	const SAVE_KEY              = CTRL_Z;
+  // is backspace.
+   const ABORT_KEY                 = CTRL_A;
+   const CROSSPOST_KEY             = CTRL_C;
+   const DELETE_LINE_KEY           = CTRL_D;
+   const GENERAL_HELP_KEY          = CTRL_G;
+   const TOGGLE_INSERT_KEY         = CTRL_I;
+   const CHANGE_COLOR_KEY          = CTRL_K;
+   const FIND_TEXT_KEY             = CTRL_N;
+   const IMPORT_FILE_KEY           = CTRL_O;
+   const CMDLIST_HELP_KEY          = CTRL_P;
+   const QUOTE_KEY                 = CTRL_Q;
+   const PROGRAM_INFO_HELP_KEY     = CTRL_R;
+   const PAGE_DOWN_KEY             = CTRL_S;
+   const LIST_TXT_REPLACEMENTS_KEY = CTRL_T;
+   const PAGE_UP_KEY               = CTRL_W;
+   const EXPORT_FILE_KEY           = CTRL_X;
+   const SAVE_KEY                  = CTRL_Z;
 
    // Draw the screen.
    // Note: This is purposefully drawing the top of the message.  We
    // want to place the cursor at the first character on the top line,
-   // too.  This is for the case where we're editin an existing message -
+   // too.  This is for the case where we're editing an existing message -
    // we want to start editigng it at the top.
-	fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
-		             gInsertMode, gUseQuotes, 0, displayEditLines);
-
-	var curpos = new Object();
-	curpos.x = gEditLeft;
-	curpos.y = gEditTop;
-	console.gotoxy(curpos);
-
-	// Input loop
-	var userInput = "";
-	var currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
-	var numKeysPressed = 0; // Used only to determine when to call updateTime()
-	var continueOn = true;
-	while (continueOn)
-	{
-		// Get a key, and time out after 5 minutes.
-		// Get a keypress from the user.  If the setting for using the
-		// input timeout is enabled and the user is not a sysop, then use
-		// the input timeout specified in the config file.  Otherwise,
-		// don't use a timeout.
-		if (gConfigSettings.userInputTimeout && !gIsSysop)
-			userInput = console.inkey(K_NOCRLF|K_NOSPIN, gConfigSettings.inputTimeoutMS);
-		else
-			userInput = console.getkey(K_NOCRLF|K_NOSPIN);
-		// If userInput is blank, then the input timeout was probably
-		// reached, so abort.
-		if (userInput == "")
-		{
-			returnCode = 1; // Aborted
-			continueOn = false;
-			console.crlf();
-			console.print("nhr" + EDITOR_PROGRAM_NAME + ": Input timeout reached.");
-			continue;
-		}
-
-		// If we reach this code, the timeout wasn't reached.
-		++numKeysPressed;
-
-		// If gEditLines currently has 1 less line than we need,
-		// then add a new line to gEditLines.
-		if (gEditLines.length == gEditLinesIndex)
-			gEditLines.push(new TextLine());
-
-		// Take the appropriate action for the key pressed.
-		switch (userInput)
-		{
-			case ABORT_KEY:
+   fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+                   gInsertMode, gUseQuotes, 0, displayEditLines);
+
+   var curpos = new Object();
+   curpos.x = gEditLeft;
+   curpos.y = gEditTop;
+   console.gotoxy(curpos);
+
+   // Input loop
+   var userInput = "";
+   var currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+   var numKeysPressed = 0; // Used only to determine when to call updateTime()
+   var continueOn = true;
+   while (continueOn)
+   {
+      // Get a key, and time out after 5 minutes.
+      // Get a keypress from the user.  If the setting for using the
+      // input timeout is enabled and the user is not a sysop, then use
+      // the input timeout specified in the config file.  Otherwise,
+      // don't use a timeout.
+      if (gConfigSettings.userInputTimeout && !gIsSysop)
+         userInput = console.inkey(K_NOCRLF|K_NOSPIN, gConfigSettings.inputTimeoutMS);
+      else
+         userInput = console.getkey(K_NOCRLF|K_NOSPIN);
+      // If userInput is blank, then the input timeout was probably
+      // reached, so abort.
+      if (userInput == "")
+      {
+         returnCode = 1; // Aborted
+         continueOn = false;
+         console.crlf();
+         console.print("nhr" + EDITOR_PROGRAM_NAME + ": Input timeout reached.");
+         continue;
+      }
+
+      // If we get here, that means the timeout wasn't reached.
+      ++numKeysPressed;
+
+      // If gEditLines currently has 1 less line than we need,
+      // then add a new line to gEditLines.
+      if (gEditLines.length == gEditLinesIndex)
+         gEditLines.push(new TextLine());
+
+      // Take the appropriate action for the key pressed.
+      switch (userInput)
+      {
+         case ABORT_KEY:
             // Before aborting, ask they user if they really want to abort.
             if (promptYesNo("Abort message", false, "Abort"))
             {
@@ -854,33 +849,33 @@ function doEditLoop()
                //console.print("n" + gTextAttrs);
                console.print(chooseEditColor());
             }
-				break;
-			case SAVE_KEY:
-				returnCode = 0; // Save
-				continueOn = false;
-				break;
-			case CMDLIST_HELP_KEY:
-				displayCommandList(true, true, true, gCanCrossPost, gIsSysop);
-				clearEditAreaBuffer();
+            break;
+         case SAVE_KEY:
+            returnCode = 0; // Save
+            continueOn = false;
+            break;
+         case CMDLIST_HELP_KEY:
+            displayCommandList(true, true, true, gCanCrossPost, gIsSysop, gConfigSettings.enableTextReplacements);
+            clearEditAreaBuffer();
+            fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+                           gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
+                           displayEditLines);
+            break;
+         case GENERAL_HELP_KEY:
+            displayGeneralHelp(true, true, true);
+            clearEditAreaBuffer();
             fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
-		                     gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
-		                     displayEditLines);
-				break;
-			case GENERAL_HELP_KEY:
-				displayGeneralHelp(true, true, true);
-				clearEditAreaBuffer();
-				fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
-				               gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
-				               displayEditLines);
-				break;
-			case PROGRAM_INFO_HELP_KEY:
-				displayProgramInfo(true, true, true);
-				clearEditAreaBuffer();
-				fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
-				               gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
-				               displayEditLines);
+                           gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
+                           displayEditLines);
             break;
-			case QUOTE_KEY:
+         case PROGRAM_INFO_HELP_KEY:
+            displayProgramInfo(true, true);
+            clearEditAreaBuffer();
+            fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+                           gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
+                           displayEditLines);
+            break;
+         case QUOTE_KEY:
             // Let the user choose & insert quote lines into the message.
             if (gUseQuotes)
             {
@@ -900,7 +895,7 @@ function doEditLoop()
             }
             break;
          case CHANGE_COLOR_KEY:
-				    /*
+                /*
             // Let the user change the text color.
             if (gConfigSettings.allowColorSelection)
             {
@@ -920,260 +915,260 @@ function doEditLoop()
             }
             */
             break;
-			case KEY_UP:
-				// Move the cursor up one line.
-				if (gEditLinesIndex > 0)
-				{
-					--gEditLinesIndex;
-
-					// gTextLineIndex should containg the index in the text
-					// line where the cursor would add text.  If the previous
-					// line is shorter than the one we just left, then
-					// gTextLineIndex and curpos.x need to be adjusted.
-					if (gTextLineIndex > gEditLines[gEditLinesIndex].length())
-					{
-						gTextLineIndex = gEditLines[gEditLinesIndex].length();
-						curpos.x = gEditLeft + gEditLines[gEditLinesIndex].length();
-					}
-					// Figure out the vertical coordinate of where the
-					// cursor should be.
-					// If the cursor is at the top of the edit area,
-					// then scroll up through the message by 1 line.
-					if (curpos.y == gEditTop)
-						displayEditLines(gEditTop, gEditLinesIndex, gEditBottom, true, /*true*/false);
-					else
-						--curpos.y;
-
-					console.gotoxy(curpos);
-					currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
-					console.print(chooseEditColor()); // Make sure the edit color is correct
-				}
-				break;
-			case KEY_DOWN:
-				// Move the cursor down one line.
-				if (gEditLinesIndex < gEditLines.length-1)
-				{
-					++gEditLinesIndex;
-					// gTextLineIndex should containg the index in the text
-					// line where the cursor would add text.  If the next
-					// line is shorter than the one we just left, then
-					// gTextLineIndex and curpos.x need to be adjusted.
-					if (gTextLineIndex > gEditLines[gEditLinesIndex].length())
-					{
-						gTextLineIndex = gEditLines[gEditLinesIndex].length();
-						curpos.x = gEditLeft + gEditLines[gEditLinesIndex].length();
-					}
-					// Figure out the vertical coordinate of where the
-					// cursor should be.
-					// If the cursor is at the bottom of the edit area,
-					// then scroll down through the message by 1 line.
-					if (curpos.y == gEditBottom)
-					{
-						displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
-						                 gEditBottom, true, /*true*/false);
-					}
-					else
-						++curpos.y;
-
-					console.gotoxy(curpos);
-					currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
-					console.print(chooseEditColor()); // Make sure the edit color is correct
-				}
-				break;
-			case KEY_LEFT:
-				// If the horizontal cursor position is right of the
-				// leftmost edit position, then let it move left.
-				if (curpos.x > gEditLeft)
-				{
-					--curpos.x;
-					console.gotoxy(curpos);
-					if (gTextLineIndex > 0)
-						--gTextLineIndex;
-				}
-				else
-				{
-					// The cursor is at the leftmost position in the
-					// edit area.  If there are text lines above the
-					// current line, then move the cursor to the end
-					// of the previous line.
-					if (gEditLinesIndex > 0)
-					{
-						--gEditLinesIndex;
-						curpos.x = gEditLeft + gEditLines[gEditLinesIndex].length();
-						// Move the cursor up or scroll up by one line
-						if (curpos.y > 1)
-							--curpos.y;
-						else
-							displayEditLines(gEditTop, gEditLinesIndex, gEditBottom, true, /*true*/false);
-						gTextLineIndex = gEditLines[gEditLinesIndex].length();
-						console.gotoxy(curpos);
-					}
-				}
-				console.print(chooseEditColor());
-
-				// Update the current word length.
-				currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
-				// Make sure the edit color is correct
-				console.print(chooseEditColor());
-				break;
-			case KEY_RIGHT:
-				// If the horizontal cursor position is left of the
-				// rightmost edit position, then the cursor can move
-				// to the right.
-				if (curpos.x < gEditRight)
-				{
-					// The current line index must be within bounds
-					// before we can move the cursor to the right.
-					if (gTextLineIndex < gEditLines[gEditLinesIndex].length())
-					{
-						++curpos.x;
-						console.gotoxy(curpos);
-						++gTextLineIndex;
-					}
-					else
-					{
-						// The cursor is at the rightmost position on the
-						// line.  If there are text lines below the current
-						// line, then move the cursor to the start of the
-						// next line.
-						if (gEditLinesIndex < gEditLines.length-1)
-						{
-							++gEditLinesIndex;
-							curpos.x = gEditLeft;
-							// Move the cursor down or scroll down by one line
-							if (curpos.y < gEditBottom)
-								++curpos.y;
-							else
-								displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
-								                 gEditBottom, true, /*true*/false);
-							gTextLineIndex = 0;
-							console.gotoxy(curpos);
-						}
-					}
-				}
-				else
-				{
-					// The cursor is at the rightmost position in the
-					// edit area.  If there are text lines below the
-					// current line, then move the cursor to the start
-					// of the next line.
-					if (gEditLinesIndex < gEditLines.length-1)
-					{
-						++gEditLinesIndex;
-						curpos.x = gEditLeft;
-						// Move the cursor down or scroll down by one line
-						if (curpos.y < gEditBottom)
-							++curpos.y;
-						else
-							displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
-							                 gEditBottom, true, false);
-						gTextLineIndex = 0;
-						console.gotoxy(curpos);
-					}
-				}
-				console.print(chooseEditColor());
-
-				// Update the current word length.
-				currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
-				// Make sure the edit color is correct
-				console.print(chooseEditColor());
-				break;
-			case KEY_HOME:
-				// Go to the beginning of the line
-				gTextLineIndex = 0;
-				curpos.x = gEditLeft;
-				console.gotoxy(curpos);
-				// Update the current word length.
+         case KEY_UP:
+            // Move the cursor up one line.
+            if (gEditLinesIndex > 0)
+            {
+               --gEditLinesIndex;
+
+               // gTextLineIndex should containg the index in the text
+               // line where the cursor would add text.  If the previous
+               // line is shorter than the one we just left, then
+               // gTextLineIndex and curpos.x need to be adjusted.
+               if (gTextLineIndex > gEditLines[gEditLinesIndex].length())
+               {
+                  gTextLineIndex = gEditLines[gEditLinesIndex].length();
+                  curpos.x = gEditLeft + gEditLines[gEditLinesIndex].length();
+               }
+               // Figure out the vertical coordinate of where the
+               // cursor should be.
+               // If the cursor is at the top of the edit area,
+               // then scroll up through the message by 1 line.
+               if (curpos.y == gEditTop)
+                  displayEditLines(gEditTop, gEditLinesIndex, gEditBottom, true, /*true*/false);
+               else
+                  --curpos.y;
+
+               console.gotoxy(curpos);
+               currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+               console.print(chooseEditColor()); // Make sure the edit color is correct
+            }
+            break;
+         case KEY_DOWN:
+            // Move the cursor down one line.
+            if (gEditLinesIndex < gEditLines.length-1)
+            {
+               ++gEditLinesIndex;
+               // gTextLineIndex should containg the index in the text
+               // line where the cursor would add text.  If the next
+               // line is shorter than the one we just left, then
+               // gTextLineIndex and curpos.x need to be adjusted.
+               if (gTextLineIndex > gEditLines[gEditLinesIndex].length())
+               {
+                  gTextLineIndex = gEditLines[gEditLinesIndex].length();
+                  curpos.x = gEditLeft + gEditLines[gEditLinesIndex].length();
+               }
+               // Figure out the vertical coordinate of where the
+               // cursor should be.
+               // If the cursor is at the bottom of the edit area,
+               // then scroll down through the message by 1 line.
+               if (curpos.y == gEditBottom)
+               {
+                  displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+                                   gEditBottom, true, /*true*/false);
+               }
+               else
+                  ++curpos.y;
+
+               console.gotoxy(curpos);
+               currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+               console.print(chooseEditColor()); // Make sure the edit color is correct
+            }
+            break;
+         case KEY_LEFT:
+            // If the horizontal cursor position is right of the
+            // leftmost edit position, then let it move left.
+            if (curpos.x > gEditLeft)
+            {
+               --curpos.x;
+               console.gotoxy(curpos);
+               if (gTextLineIndex > 0)
+                  --gTextLineIndex;
+            }
+            else
+            {
+               // The cursor is at the leftmost position in the
+               // edit area.  If there are text lines above the
+               // current line, then move the cursor to the end
+               // of the previous line.
+               if (gEditLinesIndex > 0)
+               {
+                  --gEditLinesIndex;
+                  curpos.x = gEditLeft + gEditLines[gEditLinesIndex].length();
+                  // Move the cursor up or scroll up by one line
+                  if (curpos.y > 1)
+                     --curpos.y;
+                  else
+                     displayEditLines(gEditTop, gEditLinesIndex, gEditBottom, true, /*true*/false);
+                  gTextLineIndex = gEditLines[gEditLinesIndex].length();
+                  console.gotoxy(curpos);
+               }
+            }
+            console.print(chooseEditColor());
+
+            // Update the current word length.
+            currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+            // Make sure the edit color is correct
+            console.print(chooseEditColor());
+            break;
+         case KEY_RIGHT:
+            // If the horizontal cursor position is left of the
+            // rightmost edit position, then the cursor can move
+            // to the right.
+            if (curpos.x < gEditRight)
+            {
+               // The current line index must be within bounds
+               // before we can move the cursor to the right.
+               if (gTextLineIndex < gEditLines[gEditLinesIndex].length())
+               {
+                  ++curpos.x;
+                  console.gotoxy(curpos);
+                  ++gTextLineIndex;
+               }
+               else
+               {
+                  // The cursor is at the rightmost position on the
+                  // line.  If there are text lines below the current
+                  // line, then move the cursor to the start of the
+                  // next line.
+                  if (gEditLinesIndex < gEditLines.length-1)
+                  {
+                     ++gEditLinesIndex;
+                     curpos.x = gEditLeft;
+                     // Move the cursor down or scroll down by one line
+                     if (curpos.y < gEditBottom)
+                        ++curpos.y;
+                     else
+                        displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+                                         gEditBottom, true, /*true*/false);
+                     gTextLineIndex = 0;
+                     console.gotoxy(curpos);
+                  }
+               }
+            }
+            else
+            {
+               // The cursor is at the rightmost position in the
+               // edit area.  If there are text lines below the
+               // current line, then move the cursor to the start
+               // of the next line.
+               if (gEditLinesIndex < gEditLines.length-1)
+               {
+                  ++gEditLinesIndex;
+                  curpos.x = gEditLeft;
+                  // Move the cursor down or scroll down by one line
+                  if (curpos.y < gEditBottom)
+                     ++curpos.y;
+                  else
+                     displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+                                      gEditBottom, true, false);
+                  gTextLineIndex = 0;
+                  console.gotoxy(curpos);
+               }
+            }
+            console.print(chooseEditColor());
+
+            // Update the current word length.
             currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
-				break;
-			case KEY_END:
-				// Go to the end of the line
-				if (gEditLinesIndex < gEditLines.length)
-				{
-					gTextLineIndex = gEditLines[gEditLinesIndex].length();
-					curpos.x = gEditLeft + gTextLineIndex;
-					// If the cursor position would be to the right of the edit
-					// area, then place it at gEditRight.
-					if (curpos.x > gEditRight)
-					{
+            // Make sure the edit color is correct
+            console.print(chooseEditColor());
+            break;
+         case KEY_HOME:
+            // Go to the beginning of the line
+            gTextLineIndex = 0;
+            curpos.x = gEditLeft;
+            console.gotoxy(curpos);
+            // Update the current word length.
+            currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
+            break;
+         case KEY_END:
+            // Go to the end of the line
+            if (gEditLinesIndex < gEditLines.length)
+            {
+               gTextLineIndex = gEditLines[gEditLinesIndex].length();
+               curpos.x = gEditLeft + gTextLineIndex;
+               // If the cursor position would be to the right of the edit
+               // area, then place it at gEditRight.
+               if (curpos.x > gEditRight)
+               {
                   var difference = curpos.x - gEditRight;
                   curpos.x -= difference;
                   gTextLineIndex -= difference;
-					}
-					// Place the cursor where it should be.
-					console.gotoxy(curpos);
+               }
+               // Place the cursor where it should be.
+               console.gotoxy(curpos);
 
-					// Update the current word length.
+               // Update the current word length.
                currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
-				}
-				break;
-			case BACKSPACE:
-				// Delete the previous character
-				var retObject = doBackspace(curpos, currentWordLength);
-				curpos.x = retObject.x;
-				curpos.y = retObject.y;
-				currentWordLength = retObject.currentWordLength;
-				// Make sure the edit color is correct
-				console.print(chooseEditColor());
-				break;
-			case KEY_DEL:
-				// Delete the next character
-				var retObject = doDeleteKey(curpos, currentWordLength);
-				curpos.x = retObject.x;
-				curpos.y = retObject.y;
-				currentWordLength = retObject.currentWordLength;
-				// Make sure the edit color is correct
-				console.print(chooseEditColor());
-				break;
-			case KEY_ENTER:
-				var retObject = doEnterKey(curpos, currentWordLength);
-				curpos.x = retObject.x;
-				curpos.y = retObject.y;
-				currentWordLength = retObject.currentWordLength;
-				returnCode = retObject.returnCode;
-        continueOn = retObject.continueOn;
-        // Check for whether we should do quote selection or
-        // show the help screen (if the user entered /Q or /?)
-        if (continueOn)
-        {
-           if (retObject.doQuoteSelection)
-           {
-              if (gUseQuotes)
-              {
-                 retObject = doQuoteSelection(curpos, currentWordLength);
-                 curpos.x = retObject.x;
-                 curpos.y = retObject.y;
-                 currentWordLength = retObject.currentWordLength;
-                 // If user input timed out, then abort.
-                 if (retObject.timedOut)
-                 {
-                    returnCode = 1; // Aborted
-                    continueOn = false;
-                    console.crlf();
-                    console.print("nhr" + EDITOR_PROGRAM_NAME + ": Input timeout reached.");
-                    continue;
-                 }
-              }
-           }
-           else if (retObject.showHelp)
-           {
-              displayProgramInfo(true, true, false);
-              displayCommandList(false, false, true, gCanCrossPost, gIsSysop);
-              clearEditAreaBuffer();
-              fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
-                             gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
-                             displayEditLines);
-              console.gotoxy(curpos);
-           }
-           else if (retObject.doCrossPostSelection)
-           {
-              if (gCanCrossPost)
-                 doCrossPosting();
-           }
-        }
-        // Make sure the edit color is correct
-				console.print(chooseEditColor());
-				break;
+            }
+            break;
+         case BACKSPACE:
+            // Delete the previous character
+            var retObject = doBackspace(curpos, currentWordLength);
+            curpos.x = retObject.x;
+            curpos.y = retObject.y;
+            currentWordLength = retObject.currentWordLength;
+            // Make sure the edit color is correct
+            console.print(chooseEditColor());
+            break;
+         case KEY_DEL:
+            // Delete the next character
+            var retObject = doDeleteKey(curpos, currentWordLength);
+            curpos.x = retObject.x;
+            curpos.y = retObject.y;
+            currentWordLength = retObject.currentWordLength;
+            // Make sure the edit color is correct
+            console.print(chooseEditColor());
+            break;
+         case KEY_ENTER:
+            var retObject = doEnterKey(curpos, currentWordLength);
+            curpos.x = retObject.x;
+            curpos.y = retObject.y;
+            currentWordLength = retObject.currentWordLength;
+            returnCode = retObject.returnCode;
+            continueOn = retObject.continueOn;
+            // Check for whether we should do quote selection or
+            // show the help screen (if the user entered /Q or /?)
+            if (continueOn)
+            {
+               if (retObject.doQuoteSelection)
+               {
+                  if (gUseQuotes)
+                  {
+                     retObject = doQuoteSelection(curpos, currentWordLength);
+                     curpos.x = retObject.x;
+                     curpos.y = retObject.y;
+                     currentWordLength = retObject.currentWordLength;
+                     // If user input timed out, then abort.
+                     if (retObject.timedOut)
+                     {
+                        returnCode = 1; // Aborted
+                        continueOn = false;
+                        console.crlf();
+                        console.print("nhr" + EDITOR_PROGRAM_NAME + ": Input timeout reached.");
+                        continue;
+                     }
+                  }
+               }
+               else if (retObject.showHelp)
+               {
+                  displayProgramInfo(true, false);
+                  displayCommandList(false, false, true, gCanCrossPost, gIsSysop, gConfigSettings.enableTextReplacements);
+                  clearEditAreaBuffer();
+                  fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
+                                gInsertMode, gUseQuotes, gEditLinesIndex-(curpos.y-gEditTop),
+                                displayEditLines);
+                  console.gotoxy(curpos);
+               }
+               else if (retObject.doCrossPostSelection)
+               {
+                  if (gCanCrossPost)
+                     doCrossPosting();
+               }
+            }
+            // Make sure the edit color is correct
+            console.print(chooseEditColor());
+            break;
          // Insert/overwrite mode toggle
          case KEY_INSERT:
          case TOGGLE_INSERT_KEY:
@@ -1182,7 +1177,7 @@ function doEditLoop()
             console.print(chooseEditColor());
             console.gotoxy(curpos);
             break;
-			case KEY_ESC:
+         case KEY_ESC:
             // Do the ESC menu
             var retObj = fpHandleESCMenu(curpos, currentWordLength);
             returnCode = retObj.returnCode;
@@ -1198,7 +1193,7 @@ function doEditLoop()
                console.print(chooseEditColor());
                console.gotoxy(curpos);
             }
-				break;
+            break;
          case FIND_TEXT_KEY:
             var retObj = findText(curpos);
             curpos.x = retObj.x;
@@ -1343,9 +1338,13 @@ function doEditLoop()
             if (gCanCrossPost)
                doCrossPosting();
             break;
+         case LIST_TXT_REPLACEMENTS_KEY:
+            if (gConfigSettings.enableTextReplacements)
+               listTextReplacements();
+            break;
          default:
             // For the tab character, insert 3 spaces.  Otherwise,
-            // if it's a printabel character, add the character.
+            // if it's a printable character, add the character.
             if (/\t/.test(userInput))
             {
                var retObject;
@@ -1368,13 +1367,13 @@ function doEditLoop()
                }
             }
             break;
-		}
+      }
 
-		// For every 5 keys pressed, dheck the current time and update
-		// it on the screen if necessary.
-		if (numKeysPressed % 5 == 0)
-			updateTime();
-	}
+      // For every 5 keys pressed, dheck the current time and update
+      // it on the screen if necessary.
+      if (numKeysPressed % 5 == 0)
+         updateTime();
+   }
 
    // If gEditLines has only 1 line in it and it's blank, then
    // remove it so that we can test to see if the message is empty.
@@ -1384,7 +1383,7 @@ function doEditLoop()
          gEditLines.splice(0, 1);
    }
 
-	return returnCode;
+   return returnCode;
 }
 // Helper function for doEditLoop(): Handles the backspace behavior.
 //
@@ -1398,52 +1397,52 @@ function doEditLoop()
 function doBackspace(pCurpos, pCurrentWordLength)
 {
    // Create the return object.
-	var retObj = new Object();
-	retObj.x = pCurpos.x;
-	retObj.y = pCurpos.y;
-	retObj.currentWordLength = pCurrentWordLength;
-
-	var didBackspace = false;
-	// For later, store a backup of the current edit line index and
-	// cursor position.
-	var originalCurrentLineIndex = gEditLinesIndex;
-	var originalX = pCurpos.x;
-	var originalY = pCurpos.y;
-	var originallyOnLastLine = (gEditLinesIndex == gEditLines.length-1);
-
-	// If the cursor is beyond the leftmost position in
-	// the edit area, then we can simply remove the last
-	// character in the current line and move the cursor
-	// over to the left.
-	if (retObj.x > gEditLeft)
-	{
-		if (gTextLineIndex > 0)
-		{
-			console.print(BACKSPACE);
-			console.print(" ");
-			--retObj.x;
-			console.gotoxy(retObj.x, retObj.y);
-
-			// Remove the previous character from the text line
-			var textLineLength = gEditLines[gEditLinesIndex].length();
-			if (textLineLength > 0)
-			{
-				var textLine = gEditLines[gEditLinesIndex].text.substr(0, gTextLineIndex-1)
-				             + gEditLines[gEditLinesIndex].text.substr(gTextLineIndex);
-				gEditLines[gEditLinesIndex].text = textLine;
-				didBackspace = true;
-				--gTextLineIndex;
-			}
-		}
-	}
-	else
-	{
-		// The cursor is at the leftmost position in the edit area.
-		// If we are beyond the first text line, then move as much of
-		// the current text line as possible up to the previous line,
-		// if there's room (if not, don't do anything).
-		if (gEditLinesIndex > 0)
-		{
+   var retObj = new Object();
+   retObj.x = pCurpos.x;
+   retObj.y = pCurpos.y;
+   retObj.currentWordLength = pCurrentWordLength;
+
+   var didBackspace = false;
+   // For later, store a backup of the current edit line index and
+   // cursor position.
+   var originalCurrentLineIndex = gEditLinesIndex;
+   var originalX = pCurpos.x;
+   var originalY = pCurpos.y;
+   var originallyOnLastLine = (gEditLinesIndex == gEditLines.length-1);
+
+   // If the cursor is beyond the leftmost position in
+   // the edit area, then we can simply remove the last
+   // character in the current line and move the cursor
+   // over to the left.
+   if (retObj.x > gEditLeft)
+   {
+      if (gTextLineIndex > 0)
+      {
+         console.print(BACKSPACE);
+         console.print(" ");
+         --retObj.x;
+         console.gotoxy(retObj.x, retObj.y);
+
+         // Remove the previous character from the text line
+         var textLineLength = gEditLines[gEditLinesIndex].length();
+         if (textLineLength > 0)
+         {
+            var textLine = gEditLines[gEditLinesIndex].text.substr(0, gTextLineIndex-1)
+                         + gEditLines[gEditLinesIndex].text.substr(gTextLineIndex);
+            gEditLines[gEditLinesIndex].text = textLine;
+            didBackspace = true;
+            --gTextLineIndex;
+         }
+      }
+   }
+   else
+   {
+      // The cursor is at the leftmost position in the edit area.
+      // If we are beyond the first text line, then move as much of
+      // the current text line as possible up to the previous line,
+      // if there's room (if not, don't do anything).
+      if (gEditLinesIndex > 0)
+      {
          var prevLineIndex = gEditLinesIndex - 1;
          if (gEditLines[gEditLinesIndex].length() > 0)
          {
@@ -1522,20 +1521,20 @@ function doBackspace(pCurpos, pCurrentWordLength)
                didBackspace = true;
             }
          }
-		}
-	}
+      }
+   }
 
-	// If the backspace was performed, then re-adjust the text lines
-	// and refresh the screen.
-	if (didBackspace)
-	{
+   // If the backspace was performed, then re-adjust the text lines
+   // and refresh the screen.
+   if (didBackspace)
+   {
       // Store the previous line of text now so we can compare it later
       var prevTextline = "";
       if (gEditLinesIndex > 0)
          prevTextline = gEditLines[gEditLinesIndex-1].text;
 
       // Re-adjust the text lines
-		reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth);
+      reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth);
 
       // If the previous line's length increased, that probably means that the
       // user backspaced to the beginning of the current line and the word was
@@ -1569,10 +1568,10 @@ function doBackspace(pCurpos, pCurrentWordLength)
       // only refresh that one line on the screen.
       else if ((originalX > gEditLeft) && (originalX < gEditLeft + gEditWidth - 1) && originallyOnLastLine)
          displayEditLines(originalY, originalCurrentLineIndex, originalY, false);
-		// If scrolling was to be done, then refresh the entire
-		// current message text on the screen from the top of the
-		// edit area.  Otherwise, only refresh starting from the
-		// original horizontal position and message line.
+      // If scrolling was to be done, then refresh the entire
+      // current message text on the screen from the top of the
+      // edit area.  Otherwise, only refresh starting from the
+      // original horizontal position and message line.
       else
       {
          // Display the edit lines starting with the previous line if possible.
@@ -1584,11 +1583,11 @@ function doBackspace(pCurpos, pCurrentWordLength)
 
       // Make sure the current word length is correct.
       retObj.currentWordLength = getWordLength(gEditLinesIndex, gTextLineIndex);
-	}
+   }
 
    // Make sure the cursor is placed where it should be.
    console.gotoxy(retObj.x, retObj.y);
-	return retObj;
+   return retObj;
 }
 
 // Helper function for doEditLoop(): Handles the delete key behavior.
@@ -1604,9 +1603,9 @@ function doDeleteKey(pCurpos, pCurrentWordLength)
 {
    // Create the return object
   var returnObject = new Object();
-	returnObject.x = pCurpos.x;
-	returnObject.y = pCurpos.y;
-	returnObject.currentWordLength = pCurrentWordLength;
+   returnObject.x = pCurpos.x;
+   returnObject.y = pCurpos.y;
+   returnObject.currentWordLength = pCurrentWordLength;
 
   // Store the original line text (for testing to see if we should update the screen).
   var originalLineText = gEditLines[gEditLinesIndex].text;
@@ -1737,8 +1736,8 @@ function doPrintableChar(pUserInput, pCurpos, pCurrentWordLength)
    // Create the return object.
    var retObj = new Object();
    retObj.x = pCurpos.x;
-	retObj.y = pCurpos.y;
-	retObj.currentWordLength = pCurrentWordLength;
+   retObj.y = pCurpos.y;
+   retObj.currentWordLength = pCurrentWordLength;
 
    // Note: gTextLineIndex is where the new character will appear in the line.
    // If gTextLineIndex is somehow past the end of the current line, then
@@ -1768,6 +1767,21 @@ function doPrintableChar(pUserInput, pCurpos, pCurrentWordLength)
       }
    }
 
+   // Handle text replacement (AKA macros).  Added 2013-08-31.
+   var madeTxtReplacement = false; // For screen refresh purposes
+   if (gConfigSettings.enableTextReplacements && (pUserInput == " "))
+   {
+      var txtReplaceObj = doMacroTxtReplacementInEditLine(gTxtReplacements, gEditLinesIndex,
+                                                     gTextLineIndex,
+                                                     gConfigSettings.textReplacementsUseRegex);
+      madeTxtReplacement = txtReplaceObj.madeTxtReplacement;
+      if (madeTxtReplacement)
+      {
+         retObj.x += txtReplaceObj.wordLenDiff;
+         gTextLineIndex += txtReplaceObj.wordLenDiff;
+      }
+   }
+
    // Store a copy of the current line so that we can compare it later to see
    // if it was modified by reAdjustTextLines().
    var originalAfterCharApplied = gEditLines[gEditLinesIndex].text;
@@ -1782,9 +1796,10 @@ function doPrintableChar(pUserInput, pCurpos, pCurrentWordLength)
    // spot using console.gotoxy() at the end.  This is an optimization.
    var placeCursorAtEnd = true;
 
-   // If the current text line is now different (modified by reAdjustTextLines()),
-   // then we'll need to refresh multiple lines on the screen.
-   if (reAdjusted && (gEditLines[gEditLinesIndex].text != originalAfterCharApplied))
+   // If the current text line is now different (modified by reAdjustTextLines())
+   // or text replacements were made, then we'll need to refresh multiple lines
+   // on the screen.
+   if ((reAdjusted && (gEditLines[gEditLinesIndex].text != originalAfterCharApplied)) || madeTxtReplacement)
    {
       // TODO: In case the user entered a whole line of text without any spaces,
       // the new character would appear on the next line, so we need to figure
@@ -1909,14 +1924,18 @@ function doEnterKey(pCurpos, pCurrentWordLength)
 {
    // Create the return object
    var retObj = new Object();
-	retObj.x = pCurpos.x;
-	retObj.y = pCurpos.y;
-	retObj.currentWordLength = pCurrentWordLength;
-	retObj.returnCode = 0;
-	retObj.continueOn = true;
-	retObj.doQuoteSelection = false;
-	retObj.doCrossPostSelection = false;
-	retObj.showHelp = false;
+   retObj.x = pCurpos.x;
+   retObj.y = pCurpos.y;
+   retObj.currentWordLength = pCurrentWordLength;
+   retObj.returnCode = 0;
+   retObj.continueOn = true;
+   retObj.doQuoteSelection = false;
+   retObj.doCrossPostSelection = false;
+   retObj.showHelp = false;
+
+   // Store the current screen row position and gEditLines index.
+   var initialScreenLine = pCurpos.y;
+   var initialEditLinesIndex = gEditLinesIndex;
 
    // Check for slash commands (/S, /A, /?).  If the user has
    // typed one of them by itself at the beginning of the line,
@@ -2009,21 +2028,96 @@ function doEnterKey(pCurpos, pCurrentWordLength)
          console.gotoxy(retObj.x, retObj.y);
          return(retObj);
       }
+      else if (lineUpper == "/T")
+      {
+         if (gConfigSettings.enableTextReplacements)
+            listTextReplacements();
+         // Blank out the data in the text line, set the data in
+         // retObj, and return it.
+         gEditLines[gEditLinesIndex].text = "";
+         retObj.currentWordLength = 0;
+         gTextLineIndex = 0;
+         retObj.x = gEditLeft;
+         retObj.y = pCurpos.y;
+         // Blank out the /C on the screen
+         //console.print("n" + gTextAttrs);
+         console.print(chooseEditColor());
+         retObj.x = gEditLeft;
+         console.gotoxy(retObj.x, retObj.y);
+         console.print("  ");
+         // Put the cursor where it should be and return.
+         console.gotoxy(retObj.x, retObj.y);
+         return(retObj);
+      }
    }
 
-	// Store the current screen row position and gEditLines index.
-	var initialScreenLine = pCurpos.y;
-	var initialEditLinesIndex = gEditLinesIndex;
+   // Handle text replacement (AKA macros).  Added 2013-08-31.
+   var reAdjustedTxtLines = false; // For screen refresh purposes
+   // cursorHorizDiff will be set if the replaced word is too long to fit on
+   // the end of the line - In that case, the cursor and line index will have
+   // to be adjusted since the new word will be moved to the next line.
+   var cursorHorizDiff = 0;
+   if (gConfigSettings.enableTextReplacements)
+   {
+      var txtReplaceObj = doMacroTxtReplacementInEditLine(gTxtReplacements, gEditLinesIndex,
+                                                     gTextLineIndex-1,
+                                                     gConfigSettings.textReplacementsUseRegex);
+      if (txtReplaceObj.madeTxtReplacement)
+      {
+         gTextLineIndex += txtReplaceObj.wordLenDiff;
+         retObj.x += txtReplaceObj.wordLenDiff;
+         retObj.currentWordLength += txtReplaceObj.wordLenDiff;
+
+         // If the replaced text on the line is too long to print on the screen,then
+         // then we'll need to wrap the line.
+         // If the logical screen column of the last character of the last word
+         // is beyond the rightmost colum of the edit area, then wrap the line.
+         if (gEditLeft + txtReplaceObj.newTextEndIdx - 1 >= gEditRight - 1)
+         {
+            //reAdjustedTxtLines = reAdjustTextLines(gEditLines, gEditLinesIndex, gEditLines.length, gEditWidth);
+            // If the replaced text contains at least one space, then look for
+            // the last space that can appear within the edit area on the screen.
+            if (gEditLines[gEditLinesIndex].text.indexOf(" ", txtReplaceObj.wordStartIdx) > -1)
+            {
+               var spaceIdx = gEditLines[gEditLinesIndex].text.lastIndexOf(" ", txtReplaceObj.textLineIndex);
+               while ((spaceIdx > -1) && (spaceIdx > gEditWidth-2)) // To split lines at the 79th column
+                  spaceIdx = gEditLines[gEditLinesIndex].text.lastIndexOf(" ", spaceIdx-1);
+               // If a space was found after the start of the new text, then
+               // set gTextLineIndex to the first character of the word we want
+               // to split the line at, and the horizontal cursor offset based on
+               // the difference.
+               if (spaceIdx > txtReplaceObj.wordStartIdx)
+               {
+                  gTextLineIndex = spaceIdx + 1;
+                  cursorHorizDiff = txtReplaceObj.newTextEndIdx - spaceIdx;
+               }
+               else
+               {
+                  gTextLineIndex = txtReplaceObj.wordStartIdx;
+                  cursorHorizDiff = txtReplaceObj.newTextLen;
+               }
+            }
+            else
+            {
+               // The new text doesn't contain a space, so set gTextLineIndex
+               // to the start of the new word so that
+               // enterKey_InsertOrAppendNewLine() will split the line there.
+               gTextLineIndex = txtReplaceObj.wordStartIdx;
+               cursorHorizDiff = txtReplaceObj.newTextLen;
+            }
+         }
+      }
+   }
 
-	// If we're currently on the last line, then we'll need to append
-	// a new line.  Otherwise, we'll need to splice a new line into
-	// gEditLines where appropriate.
+   // If we're currently on the last line, then we'll need to append
+   // a new line.  Otherwise, we'll need to splice a new line into
+   // gEditLines where appropriate.
 
-	var appendLineToEnd = (gEditLinesIndex == gEditLines.length-1);
-	var retObject = enterKey_InsertOrAppendNewLine(pCurpos, pCurrentWordLength, appendLineToEnd);
-	retObj.x = retObject.x;
-	retObj.y = retObject.y;
-	retObj.currentWordLength = retObject.currentWordLength;
+   var appendLineToEnd = (gEditLinesIndex == gEditLines.length-1);
+   var retObject = enterKey_InsertOrAppendNewLine(pCurpos, pCurrentWordLength, appendLineToEnd);
+   retObj.x = retObject.x;
+   retObj.y = retObject.y;
+   retObj.currentWordLength = retObject.currentWordLength;
 
    // If a line was added to gEditLines, then set the hardNewlineEnd property
    // to true for both lines.
@@ -2033,18 +2127,23 @@ function doEnterKey(pCurpos, pCurrentWordLength)
       gEditLines[gEditLinesIndex].hardNewlineEnd = true;
    }
 
-	// Refresh the message text on the screen if that wasn't done by
-	// enterKey_InsertOrAppendNewLine().
-	if (!retObject.displayedEditlines)
+   // Refresh the message text on the screen if that wasn't done by
+   // enterKey_InsertOrAppendNewLine().
+   if (!retObject.displayedEditlines)
       displayEditLines(initialScreenLine, initialEditLinesIndex, gEditBottom, true, true);
 
-	console.gotoxy(retObj.x, retObj.y);
+   // Note: cursorHorizDiff is set if a word was replaced and the line was
+   // wrapped because the new word wastoo long to fit on the end of the line.
+   retObj.x += cursorHorizDiff;
+   gTextLineIndex += cursorHorizDiff;
 
-	return retObj;
+   console.gotoxy(retObj.x, retObj.y);
+
+   return retObj;
 }
 
 // Helper function for doEnterKey(): Appends/inserts a line to gEditLines
-// and returns the position of where the cursor shoul dbe.
+// and returns the position of where the cursor should be.
 //
 // Parameters:
 //  pCurpos: An object containing x and y values representing the
@@ -2068,117 +2167,117 @@ function enterKey_InsertOrAppendNewLine(pCurpos, pCurrentWordLength, pAppendLine
    returnObject.addedATextLine = false;
    returnObject.addedTextLineBelow = false;
 
-	// If we're at the end of the line, then we can simply
-	// add a new blank line & set the cursor there.
-	// Otherwise, we need to split the current line, and
-	// the text to the right of the cursor will go on the new line.
-	if (gTextLineIndex == gEditLines[gEditLinesIndex].length())
-	{
-		if (pAppendLine)
-		{
-			// Add a new blank line to the end of the message, and set
-			// the cursor there.
-			gEditLines.push(new TextLine());
-			++gEditLinesIndex;
-			returnObject.addedATextLine = true;
+   // If we're at the end of the line, then we can simply
+   // add a new blank line & set the cursor there.
+   // Otherwise, we need to split the current line, and
+   // the text to the right of the cursor will go on the new line.
+   if (gTextLineIndex == gEditLines[gEditLinesIndex].length())
+   {
+      if (pAppendLine)
+      {
+         // Add a new blank line to the end of the message, and set
+         // the cursor there.
+         gEditLines.push(new TextLine());
+         ++gEditLinesIndex;
+         returnObject.addedATextLine = true;
          returnObject.addedTextLineBelow = true;
-		}
-		else
-		{
-			// Increment gEditLinesIndex and add a new line there.
-			++gEditLinesIndex;
-			gEditLines.splice(gEditLinesIndex, 0, new TextLine());
+      }
+      else
+      {
+         // Increment gEditLinesIndex and add a new line there.
+         ++gEditLinesIndex;
+         gEditLines.splice(gEditLinesIndex, 0, new TextLine());
          returnObject.addedATextLine = true;
-		}
-
-		gTextLineIndex = 0;
-		pCurrentWordLength = 0;
-		pCurpos.x = gEditLeft;
-		// Update the vertical cursor position.
-		// If the cursor is at the bottom row, then we need
-		// to scroll the message down by 1 line.  Otherwise,
-		// we can simply increment pCurpos.y.
-		if (pCurpos.y == gEditBottom)
-		{
-			displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
-			                 gEditBottom, true, true);
+      }
+
+      gTextLineIndex = 0;
+      pCurrentWordLength = 0;
+      pCurpos.x = gEditLeft;
+      // Update the vertical cursor position.
+      // If the cursor is at the bottom row, then we need
+      // to scroll the message down by 1 line.  Otherwise,
+      // we can simply increment pCurpos.y.
+      if (pCurpos.y == gEditBottom)
+      {
+         displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+                          gEditBottom, true, true);
          returnObject.displayedEditlines = true;
       }
-		else
-			++pCurpos.y;
-	}
-	else
-	{
-		// We're in the middle of the line.
-		// Get the text to the end of the current line.
-		var lineEndText = gEditLines[gEditLinesIndex].text.substr(gTextLineIndex);
-		// Remove that text from the current line.
-		gEditLines[gEditLinesIndex].text = gEditLines[gEditLinesIndex].text.substr(0, gTextLineIndex);
-
-		if (pAppendLine)
-		{
-			// Create a new line containing lineEndText and append it to
-			// gEditLines.  Then place the cursor at the start of that line.
-			var newTextLine = new TextLine();
-			newTextLine.text = lineEndText;
-			newTextLine.hardNewlineEnd = gEditLines[gEditLinesIndex].hardNewlineEnd;
-			newTextLine.isQuoteLine = gEditLines[gEditLinesIndex].isQuoteLine;
-			gEditLines.push(newTextLine);
-			++gEditLinesIndex;
-			returnObject.addedATextLine = true;
+      else
+         ++pCurpos.y;
+   }
+   else
+   {
+      // We're in the middle of the line.
+      // Get the text to the end of the current line.
+      var lineEndText = gEditLines[gEditLinesIndex].text.substr(gTextLineIndex);
+      // Remove that text from the current line.
+      gEditLines[gEditLinesIndex].text = gEditLines[gEditLinesIndex].text.substr(0, gTextLineIndex);
+
+      if (pAppendLine)
+      {
+         // Create a new line containing lineEndText and append it to
+         // gEditLines.  Then place the cursor at the start of that line.
+         var newTextLine = new TextLine();
+         newTextLine.text = lineEndText;
+         newTextLine.hardNewlineEnd = gEditLines[gEditLinesIndex].hardNewlineEnd;
+         newTextLine.isQuoteLine = gEditLines[gEditLinesIndex].isQuoteLine;
+         gEditLines.push(newTextLine);
+         ++gEditLinesIndex;
+         returnObject.addedATextLine = true;
          returnObject.addedTextLineBelow = true;
-		}
-		else
-		{
-			// Create a new line containing lineEndText and splice it into
-			// gEditLines on the next line.  Then place the cursor at the
-			// start of that line.
-			var oldIndex = gEditLinesIndex++;
-			var newTextLine = new TextLine();
-			newTextLine.text = lineEndText;
-			newTextLine.hardNewlineEnd = gEditLines[oldIndex].hardNewlineEnd;
-			newTextLine.isQuoteLine = gEditLines[oldIndex].isQuoteLine;
-			// If the user pressed enter at the beginning of a line, then a new
-			// blank line will be inserted above, so we want to make sure its
-			// isQuoteLine property is set to false.
-			if (gTextLineIndex == 0)
+      }
+      else
+      {
+         // Create a new line containing lineEndText and splice it into
+         // gEditLines on the next line.  Then place the cursor at the
+         // start of that line.
+         var oldIndex = gEditLinesIndex++;
+         var newTextLine = new TextLine();
+         newTextLine.text = lineEndText;
+         newTextLine.hardNewlineEnd = gEditLines[oldIndex].hardNewlineEnd;
+         newTextLine.isQuoteLine = gEditLines[oldIndex].isQuoteLine;
+         // If the user pressed enter at the beginning of a line, then a new
+         // blank line will be inserted above, so we want to make sure its
+         // isQuoteLine property is set to false.
+         if (gTextLineIndex == 0)
             gEditLines[oldIndex].isQuoteLine = false;
          // Splice the new text line into gEditLines at gEditLinesIndex.
-			gEditLines.splice(gEditLinesIndex, 0, newTextLine);
+         gEditLines.splice(gEditLinesIndex, 0, newTextLine);
          returnObject.addedATextLine = true;
-		}
-
-		gTextLineIndex = 0;
-		pCurpos.x = gEditLeft;
-		// Update the vertical cursor position.
-		// If the cursor is at the bottom row, then we need
-		// to scroll the message down by 1 line.  Otherwise,
-		// we can simply increment pCurpos.y.
-		if (pCurpos.y == gEditBottom)
-		{
-			displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
-			                 gEditBottom, true, true);
-			returnObject.displayedEditlines = true;
       }
-		else
-			++pCurpos.y;
-		// Figure out the current word length.
-		// Look for a space in lineEndText.  If a space is found,
-		// the word length is the length of the word up until the
-		// space.  If a space is not found, then the word length
-		// is the entire length of lineEndText.
-		var spacePos = lineEndText.indexOf(" ");
-		if (spacePos > -1)
-			pCurrentWordLength = spacePos;
-		else
-			pCurrentWordLength = lineEndText.length;
-	}
-
-	// Set some stuff in the return object, and return it.
-	returnObject.x = pCurpos.x;
-	returnObject.y = pCurpos.y;
-	returnObject.currentWordLength = pCurrentWordLength;
-	return returnObject;
+
+      gTextLineIndex = 0;
+      pCurpos.x = gEditLeft;
+      // Update the vertical cursor position.
+      // If the cursor is at the bottom row, then we need
+      // to scroll the message down by 1 line.  Otherwise,
+      // we can simply increment pCurpos.y.
+      if (pCurpos.y == gEditBottom)
+      {
+         displayEditLines(gEditTop, gEditLinesIndex-(gEditBottom-gEditTop),
+                          gEditBottom, true, true);
+         returnObject.displayedEditlines = true;
+      }
+      else
+         ++pCurpos.y;
+      // Figure out the current word length.
+      // Look for a space in lineEndText.  If a space is found,
+      // the word length is the length of the word up until the
+      // space.  If a space is not found, then the word length
+      // is the entire length of lineEndText.
+      var spacePos = lineEndText.indexOf(" ");
+      if (spacePos > -1)
+         pCurrentWordLength = spacePos;
+      else
+         pCurrentWordLength = lineEndText.length;
+   }
+
+   // Set some stuff in the return object, and return it.
+   returnObject.x = pCurpos.x;
+   returnObject.y = pCurpos.y;
+   returnObject.currentWordLength = pCurrentWordLength;
+   return returnObject;
 }
 
 // This function handles quote selection and is called by doEditLoop().
@@ -2238,19 +2337,19 @@ function doQuoteSelection(pCurpos, pCurrentWordLength)
    var continueOn = true;
    while (continueOn)
    {
-		// Get a key, and time out after 1 minute.
-		userInput = console.inkey(0, 100000);
-		if (userInput == "")
-		{
-			// The input timeout was reached.  Abort.
-			retObj.timedOut = true;
-			continueOn = false;
-			break;
-		}
-
-		// If we got here, that means the user input didn't time out.
-		switch (userInput)
-		{
+      // Get a key, and time out after 1 minute.
+      userInput = console.inkey(0, 100000);
+      if (userInput == "")
+      {
+         // The input timeout was reached.  Abort.
+         retObj.timedOut = true;
+         continueOn = false;
+         break;
+      }
+
+      // If we got here, that means the user input didn't time out.
+      switch (userInput)
+      {
          case KEY_UP:
             // Go up 1 quote line
             if (gQuoteLinesIndex > 0)
@@ -2362,7 +2461,7 @@ function doQuoteSelection(pCurpos, pCurrentWordLength)
             // Quit out of the input loop (get out of quote mode).
             continueOn = false;
             break;
-		}
+      }
    }
 
    // We've exited quote mode.  Refresh the message text on the screen.  Note:
@@ -2384,9 +2483,9 @@ function doQuoteSelection(pCurpos, pCurrentWordLength)
    console.gotoxy(curpos);
 
    // Set the settings in the return object, and return it.
-	retObj.x = curpos.x;
-	retObj.y = curpos.y;
-	return retObj;
+   retObj.x = curpos.x;
+   retObj.y = curpos.y;
+   return retObj;
 }
 
 // Helper for doQuoteSelection(): This function moves the quote selection
@@ -2656,18 +2755,18 @@ function displayEditLines(pStartScreenRow, pArrayIndex, pEndScreenRow, pClearRem
    // pEndScreenRow or gEditBottom.
    var endScreenRow = (pEndScreenRow != null ? pEndScreenRow : gEditBottom);
 
-	// Display the message lines
-	console.print("n" + gTextAttrs);
-	var screenLine = pStartScreenRow;
-	var arrayIndex = pArrayIndex;
-	while ((screenLine <= endScreenRow) && (arrayIndex < gEditLines.length))
-	{
-		// Print the text from the current line in gEditLines.  Note: Lines starting
-		// with " >" are assumed to be quote lines - Display those lines with cyan
-		// color and the normal lines with gTextAttrs.
-		var color = gTextAttrs;
-		// Note: gEditAreaBuffer is also used in clearMsgAreaToBottom().
-		if ((gEditAreaBuffer[screenLine] != gEditLines[arrayIndex].text) || pIgnoreEditAreaBuffer)
+   // Display the message lines
+   console.print("n" + gTextAttrs);
+   var screenLine = pStartScreenRow;
+   var arrayIndex = pArrayIndex;
+   while ((screenLine <= endScreenRow) && (arrayIndex < gEditLines.length))
+   {
+      // Print the text from the current line in gEditLines.  Note: Lines starting
+      // with " >" are assumed to be quote lines - Display those lines with cyan
+      // color and the normal lines with gTextAttrs.
+      var color = gTextAttrs;
+      // Note: gEditAreaBuffer is also used in clearMsgAreaToBottom().
+      if ((gEditAreaBuffer[screenLine] != gEditLines[arrayIndex].text) || pIgnoreEditAreaBuffer)
       {
          // Choose the quote line color or the normal color for the line, then
          // display the line on the screen.
@@ -2677,21 +2776,21 @@ function displayEditLines(pStartScreenRow, pArrayIndex, pEndScreenRow, pClearRem
          gEditAreaBuffer[screenLine] = gEditLines[arrayIndex].text;
       }
 
-		++screenLine;
-		++arrayIndex;
-	}
-	if (arrayIndex > 0)
-		--arrayIndex;
-	// incrementLineBeforeClearRemaining stores whether or not we
-	// should increment screenLine before clearing the remaining
-	// lines in the edit area.
-	var incrementLineBeforeClearRemaining = true;
-	// If the array index is valid, and if the current line is shorter
-	// than the edit area width, then place the cursor after the last
-	// character in the line.
-	if ((arrayIndex >= 0) && (arrayIndex < gEditLines.length) &&
-	    (gEditLines[arrayIndex] != undefined) && (gEditLines[arrayIndex].text != undefined))
-	{
+      ++screenLine;
+      ++arrayIndex;
+   }
+   if (arrayIndex > 0)
+      --arrayIndex;
+   // incrementLineBeforeClearRemaining stores whether or not we
+   // should increment screenLine before clearing the remaining
+   // lines in the edit area.
+   var incrementLineBeforeClearRemaining = true;
+   // If the array index is valid, and if the current line is shorter
+   // than the edit area width, then place the cursor after the last
+   // character in the line.
+   if ((arrayIndex >= 0) && (arrayIndex < gEditLines.length) &&
+       (gEditLines[arrayIndex] != undefined) && (gEditLines[arrayIndex].text != undefined))
+   {
       var lineLength = gEditLines[arrayIndex].length();
       if (lineLength < gEditWidth)
       {
@@ -2700,28 +2799,28 @@ function displayEditLines(pStartScreenRow, pArrayIndex, pEndScreenRow, pClearRem
       }
       else if ((lineLength == gEditWidth) || (lineLength == 0))
          incrementLineBeforeClearRemaining = false;
-	}
-	else
+   }
+   else
       incrementLineBeforeClearRemaining = false;
 
-	// Edge case: If the current screen line is below the last line, then
-	// clear the lines up until that point.
-	var clearRemainingScreenLines = (pClearRemainingScreenRows != null ? pClearRemainingScreenRows : true);
-	if (clearRemainingScreenLines && (screenLine <= endScreenRow))
-	{
-		console.print("n" + gTextAttrs);
-		var screenLineBackup = screenLine; // So we can move the cursor back
-		clearMsgAreaToBottom(incrementLineBeforeClearRemaining ? screenLine+1 : screenLine,
-		                     pIgnoreEditAreaBuffer);
-		// Move the cursor back to the end of the current text line.
-		if (typeof(gEditLines[arrayIndex]) != "undefined")
+   // Edge case: If the current screen line is below the last line, then
+   // clear the lines up until that point.
+   var clearRemainingScreenLines = (pClearRemainingScreenRows != null ? pClearRemainingScreenRows : true);
+   if (clearRemainingScreenLines && (screenLine <= endScreenRow))
+   {
+      console.print("n" + gTextAttrs);
+      var screenLineBackup = screenLine; // So we can move the cursor back
+      clearMsgAreaToBottom(incrementLineBeforeClearRemaining ? screenLine+1 : screenLine,
+                           pIgnoreEditAreaBuffer);
+      // Move the cursor back to the end of the current text line.
+      if (typeof(gEditLines[arrayIndex]) != "undefined")
          console.gotoxy(gEditLeft + gEditLines[arrayIndex].length(), screenLineBackup);
       else
          console.gotoxy(gEditLeft, screenLineBackup);
-	}
+   }
 
-	// Make sure the correct color is set for the current line.
-	console.print(chooseEditColor());
+   // Make sure the correct color is set for the current line.
+   console.print(chooseEditColor());
 }
 
 // Clears the lines in the message area from a given line to the bottom.
@@ -2735,7 +2834,7 @@ function clearMsgAreaToBottom(pStartLine, pIgnoreEditAreaBuffer)
 {
    for (var screenLine = pStartLine; screenLine <= gEditBottom; ++screenLine)
    {
-		// Note: gEditAreaBuffer is also used in displayEditLines().
+      // Note: gEditAreaBuffer is also used in displayEditLines().
       if ((gEditAreaBuffer[screenLine].length > 0) || pIgnoreEditAreaBuffer)
       {
          console.gotoxy(gEditLeft, screenLine);
@@ -2749,18 +2848,18 @@ function clearMsgAreaToBottom(pStartLine, pIgnoreEditAreaBuffer)
 // it, and this tests to see if they are all empty).
 function messageIsEmpty()
 {
-	var msgEmpty = true;
-	
-	for (var i = 0; i < gEditLines.length; ++i)
-	{
-		if (gEditLines[i].length() > 0)
-		{
-			msgEmpty = false;
-			break;
-		}
-	}
-
-	return msgEmpty;
+   var msgEmpty = true;
+   
+   for (var i = 0; i < gEditLines.length; ++i)
+   {
+      if (gEditLines[i].length() > 0)
+      {
+         msgEmpty = false;
+         break;
+      }
+   }
+
+   return msgEmpty;
 }
 
 // Displays a part of the message text in a rectangle on the screen.  This
@@ -2918,11 +3017,11 @@ function handleDCTESCMenu(pCurpos, pCurrentWordLength)
    // Command List
    else if ((menuChoice == "O") || (menuChoice == DCTMENU_HELP_COMMAND_LIST))
    {
-      displayCommandList(true, true, true, gCanCrossPost, gIsSysop);
+      displayCommandList(true, true, true, gCanCrossPost, gIsSysop, gConfigSettings.enableTextReplacements);
       clearEditAreaBuffer();
       fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
-		               gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
-		               displayEditLines);
+                     gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
+                     displayEditLines);
    }
    // General help
    else if ((menuChoice == "G") || (menuChoice == DCTMENU_HELP_GENERAL))
@@ -2930,17 +3029,17 @@ function handleDCTESCMenu(pCurpos, pCurrentWordLength)
       displayGeneralHelp(true, true, true);
       clearEditAreaBuffer();
       fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
-		               gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
-		               displayEditLines);
+                     gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
+                     displayEditLines);
    }
    // Program info
    else if ((menuChoice == "P") || (menuChoice == DCTMENU_HELP_PROGRAM_INFO))
    {
-      displayProgramInfo(true, true, true);
+      displayProgramInfo(true, true);
       clearEditAreaBuffer();
       fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
-		               gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
-		               displayEditLines);
+                     gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
+                     displayEditLines);
    }
    // Export the message
    else if ((menuChoice == "X") || (menuChoice == DCTMENU_SYSOP_EXPORT_FILE))
@@ -3017,8 +3116,8 @@ function handleIceESCMenu(pCurpos, pCurrentWordLength)
          // Nothing needs to be done for this option.
          break;
       case ICE_ESC_MENU_HELP:
-         displayProgramInfo(true, true, false);
-         displayCommandList(false, false, true, gCanCrossPost, gIsSysop);
+         displayProgramInfo(true, false);
+         displayCommandList(false, false, true, gCanCrossPost, gIsSysop, gConfigSettings.enableTextReplacements);
          clearEditAreaBuffer();
          fpRedrawScreen(gEditLeft, gEditRight, gEditTop, gEditBottom, gTextAttrs,
                         gInsertMode, gUseQuotes, gEditLinesIndex-(pCurpos.y-gEditTop),
@@ -3529,26 +3628,26 @@ function calcBottomUpdateRow(pY, pTopIndex)
 // the cursor back to where it was.
 function updateTime()
 {
-	if (typeof(updateTime.timeStr) == "undefined")
-		updateTime.timeStr = getCurrentTimeStr();
-
-	// If the current time has changed since the last time this
-	// function was called, then update the time on the screen.
-	var currentTime = getCurrentTimeStr();
-	if (currentTime != updateTime.timeStr)
-	{
-		// Get the current cursor position so we can move
-		// the cursor back there when we're done.
-		var curpos = console.getxy();
-		// Display the current time on the screen
-		fpDisplayTime(currentTime);
-		// Make sure the edit color attribute is set.
-		console.print("n" + gTextAttrs);
-		// Move the cursor back to where it was
-		console.gotoxy(curpos);
-		// Update this function's time variable
-		updateTime.timeStr = currentTime;
-	}
+   if (typeof(updateTime.timeStr) == "undefined")
+      updateTime.timeStr = getCurrentTimeStr();
+
+   // If the current time has changed since the last time this
+   // function was called, then update the time on the screen.
+   var currentTime = getCurrentTimeStr();
+   if (currentTime != updateTime.timeStr)
+   {
+      // Get the current cursor position so we can move
+      // the cursor back there when we're done.
+      var curpos = console.getxy();
+      // Display the current time on the screen
+      fpDisplayTime(currentTime);
+      // Make sure the edit color attribute is set.
+      console.print("n" + gTextAttrs);
+      // Move the cursor back to where it was
+      console.gotoxy(curpos);
+      // Update this function's time variable
+      updateTime.timeStr = currentTime;
+   }
 }
 
 // This function lets the user change the text color and is called by doEditLoop().
@@ -3566,10 +3665,10 @@ function doColorSelection(pCurpos, pCurrentWordLength)
 {
    // Create the return object
    var retObj = new Object();
-	retObj.x = pCurpos.x;
-	retObj.y = pCurpos.y;
-	retObj.timedOut = false;
-	retObj.currentWordLength = pCurrentWordLength;
+   retObj.x = pCurpos.x;
+   retObj.y = pCurpos.y;
+   retObj.timedOut = false;
+   retObj.currentWordLength = pCurrentWordLength;
 
    // Note: The current text color is stored in gTextAttrs
 
@@ -3683,19 +3782,19 @@ function doColorSelection(pCurpos, pCurrentWordLength)
    var continueOn = true;
    while (continueOn)
    {
-		// Get a key, and time out after 1 minute.
-		userInput = console.inkey(0, 100000);
-		if (userInput == "")
-		{
-			// The input timeout was reached.  Abort.
-			retObj.timedOut = true;
-			continueOn = false;
-			break;
-		}
-
-		// If we got here, that means the user input didn't time out.
-		switch (userInput)
-		{
+      // Get a key, and time out after 1 minute.
+      userInput = console.inkey(0, 100000);
+      if (userInput == "")
+      {
+         // The input timeout was reached.  Abort.
+         retObj.timedOut = true;
+         continueOn = false;
+         break;
+      }
+
+      // If we got here, that means the user input didn't time out.
+      switch (userInput)
+      {
          case KEY_UP:
             // Go up 1 quote line
             if (gQuoteLinesIndex > 0)
@@ -3807,7 +3906,7 @@ function doColorSelection(pCurpos, pCurrentWordLength)
             // Quit out of the input loop (get out of quote mode).
             continueOn = false;
             break;
-		}
+      }
    }
 
    // We've exited quote mode.  Refresh the message text on the screen.  Note:
@@ -3829,9 +3928,9 @@ function doColorSelection(pCurpos, pCurrentWordLength)
    console.gotoxy(curpos);
 */
    // Set the settings in the return object, and return it.
-	retObj.x = curpos.x;
-	retObj.y = curpos.y;
-	return retObj;
+   retObj.x = curpos.x;
+   retObj.y = curpos.y;
+   return retObj;
 }
 
 // For the cross-posting UI: Draws the initial top border of
@@ -3987,13 +4086,13 @@ function doCrossPosting(pOriginalCurpos)
     // Position the cursor after the "Cross-posting: " text in the border and
     // write the "Choose group" text
     console.gotoxy(pSelBoxUpperLeft.x+17, pSelBoxUpperLeft.y);
-    console.print("n" + gConfigSettings.genColors.crossPostBorderTxt + "Choose group");
+    console.print("n" + gConfigSettings.genColors.listBoxBorderText + "Choose group");
     //Choose group
     //Areas in <xxxxx>
     // Re-write the border characters to overwrite the message group name
     grpDesc = msg_area.grp_list[pGrpIndex].description.substr(0, pSelBoxInnerWidth-25);
     // Write the updated border character(s)
-    console.print("n" + gConfigSettings.genColors.crossPostBorder + LEFT_T_SINGLE);
+    console.print("n" + gConfigSettings.genColors.listBoxBorder + LEFT_T_SINGLE);
     if (grpDesc.length > 3)
     {
       var numChars = grpDesc.length - 3;
@@ -4056,8 +4155,8 @@ function doCrossPosting(pOriginalCurpos)
   // Draw the selection box borders
   // Top border
   drawInitialCrossPostSelBoxTopBorder(selBoxUpperLeft, selBoxWidth,
-                                      gConfigSettings.genColors.crossPostBorder,
-                                      gConfigSettings.genColors.crossPostBorderTxt);
+                                      gConfigSettings.genColors.listBoxBorder,
+                                      gConfigSettings.genColors.listBoxBorderText);
   // Side borders
   console.print(UPPER_RIGHT_SINGLE);
   for (var row = selBoxUpperLeft.y+1; row < selBoxLowerRight.y; ++row)
@@ -4069,7 +4168,7 @@ function doCrossPosting(pOriginalCurpos)
   }
   // Bottom border
   drawInitialCrossPostSelBoxBottomBorder({ x: selBoxUpperLeft.x, y: selBoxLowerRight.y },
-                                         selBoxWidth, gConfigSettings.genColors.crossPostBorder,
+                                         selBoxWidth, gConfigSettings.genColors.listBoxBorder,
                                          false);
 
   // Write the message groups
@@ -4330,13 +4429,13 @@ function doCrossPosting(pOriginalCurpos)
             if (writePromptText)
             {
               drawInitialCrossPostSelBoxBottomBorder({ x: selBoxUpperLeft.x, y: selBoxLowerRight.y },
-                                                     selBoxWidth, gConfigSettings.genColors.crossPostBorder,
+                                                     selBoxWidth, gConfigSettings.genColors.listBoxBorder,
                                                      false);
             }
             else
             {
               console.gotoxy(selBoxUpperLeft.x+1, selBoxLowerRight.y);
-              console.print(gConfigSettings.genColors.crossPostBorder + RIGHT_T_SINGLE);
+              console.print(gConfigSettings.genColors.listBoxBorder + RIGHT_T_SINGLE);
             }
 
             // If the user made a selection, then let them choose a
@@ -4601,7 +4700,7 @@ function crossPosting_selectSubBoardInGrp(pGrpIndex, pSelBoxUpperLeft, pSelBoxLo
     mswait(pPauseMS);
     // Refresh the bottom border of the selection box
     drawInitialCrossPostSelBoxBottomBorder({ x: pX, y: pY }, pSelBoxWidth,
-                                           gConfigSettings.genColors.crossPostBorder, true);
+                                           gConfigSettings.genColors.listBoxBorder, true);
     console.gotoxy(pCurpos);
   }
 
@@ -4611,10 +4710,10 @@ function crossPosting_selectSubBoardInGrp(pGrpIndex, pSelBoxUpperLeft, pSelBoxLo
   //�Cross-posting: Areas in 
   console.gotoxy(pSelBoxUpperLeft.x+17, pSelBoxUpperLeft.y);
   var grpDesc = msg_area.grp_list[pGrpIndex].description.substr(0, pSelBoxInnerWidth-25);
-  console.print("n" + gConfigSettings.genColors.crossPostBorderTxt + "Areas in " +
+  console.print("n" + gConfigSettings.genColors.listBoxBorderText + "Areas in " +
                 grpDesc);
   // Write the updated border character(s)
-  console.print("n" + gConfigSettings.genColors.crossPostBorder);
+  console.print("n" + gConfigSettings.genColors.listBoxBorder);
   // If the length of the group description is shorter than the remaining text
   // the selection box border, then draw horizontal lines to fill in the gap.
   if (grpDesc.length < 3)
@@ -4923,13 +5022,13 @@ function crossPosting_selectSubBoardInGrp(pGrpIndex, pSelBoxUpperLeft, pSelBoxLo
             if (writePromptText)
             {
               drawInitialCrossPostSelBoxBottomBorder({ x: pSelBoxUpperLeft.x, y: pSelBoxLowerRight.y },
-                                                     pSelBoxWidth, gConfigSettings.genColors.crossPostBorder,
+                                                     pSelBoxWidth, gConfigSettings.genColors.listBoxBorder,
                                                      true);
             }
             else
             {
               console.gotoxy(pSelBoxUpperLeft.x+1, pSelBoxLowerRight.y);
-              console.print(gConfigSettings.genColors.crossPostBorder + RIGHT_T_SINGLE);
+              console.print(gConfigSettings.genColors.listBoxBorder + RIGHT_T_SINGLE);
             }
 
             // If the user made a selection, then toggle it on/off.
@@ -5205,4 +5304,210 @@ function printEditLine(pIndex, pUseColors, pStart, pLength)
       }
    }
    return lengthWritten;
+}
+
+// Lists the text replacements configured in SlyEdit using a scrollable list box.
+function listTextReplacements()
+{
+   if (gNumTxtReplacements == 0)
+   {
+      writeMsgOntBtmHelpLineWithPause("nhyThere are no text replacements.", 1500);
+      return;
+   }
+
+   // Calculate the text width for each column, which will then be used to
+   // calculate the width of the box.  For the width of the box, we need to
+   // subtract at least 3 from the edit area with to accomodate the box's side
+   // borders and the space between the text columns.
+   var txtWidth = Math.floor((gEditWidth - 10)/2);
+
+   // In order to be able to navigate forward and backwards through the text
+   // replacements, we need to copy them into an array, since gTxtReplacements
+   // is an object and not navigable both ways.  This will also allow us to easily
+   // know how many text replacements there are (using the .length property of
+   // the array).
+   // For speed, create this only once.
+   if (typeof(listTextReplacements.txtReplacementArr) == "undefined")
+   {
+      var txtReplacementObj = null;
+      listTextReplacements.txtReplacementArr = new Array();
+      for (var prop in gTxtReplacements)
+      {
+         txtReplacementObj = new Object();
+         txtReplacementObj.originalText = prop;
+         txtReplacementObj.replacement = gTxtReplacements[prop];
+         listTextReplacements.txtReplacementArr.push(txtReplacementObj);
+      }
+   }
+
+   // We'll want to have an object with the box dimensions.
+   var boxInfo = new Object();
+
+   // Construct the top & bottom border strings if they don't exist already.
+   if (typeof(listTextReplacements.topBorder) == "undefined")
+   {
+      listTextReplacements.topBorder = "n" + gConfigSettings.genColors.listBoxBorder
+        + UPPER_LEFT_SINGLE + "n" + gConfigSettings.genColors.listBoxBorderText + "Text"
+        + "n" + gConfigSettings.genColors.listBoxBorder;
+      for (var i = 0; i < (txtWidth-3); ++i)
+         listTextReplacements.topBorder += HORIZONTAL_SINGLE;
+      listTextReplacements.topBorder += "n" + gConfigSettings.genColors.listBoxBorderText
+        + "Replacement" + "n" + gConfigSettings.genColors.listBoxBorder;
+      for (var i = 0; i < (txtWidth-11); ++i)
+         listTextReplacements.topBorder += HORIZONTAL_SINGLE;
+      listTextReplacements.topBorder += UPPER_RIGHT_SINGLE;
+   }
+   boxInfo.width = strip_ctrl(listTextReplacements.topBorder).length;
+   if (typeof(listTextReplacements.bottomBorder) == "undefined")
+   {
+      var numReplacementsStr = "Total: " + listTextReplacements.txtReplacementArr.length;
+      listTextReplacements.bottomBorder = "n" + gConfigSettings.genColors.listBoxBorder
+        + LOWER_LEFT_SINGLE + "n" + gConfigSettings.genColors.listBoxBorderText
+        + UP_ARROW + ", " + DOWN_ARROW + ", ESC/Ctrl-T/C=Close" + "n"
+        + gConfigSettings.genColors.listBoxBorder;
+      var maxNumChars = boxInfo.width - numReplacementsStr.length - 28;
+      for (var i = 0; i < maxNumChars; ++i)
+         listTextReplacements.bottomBorder += HORIZONTAL_SINGLE;
+      listTextReplacements.bottomBorder += RIGHT_T_SINGLE + "n"
+        + gConfigSettings.genColors.listBoxBorderText + numReplacementsStr + "n"
+        + gConfigSettings.genColors.listBoxBorder + LEFT_T_SINGLE;
+      listTextReplacements.bottomBorder += LOWER_RIGHT_SINGLE;
+   }
+   // printf format strings for the list
+   if (typeof(listTextReplacements.listFormatStr) == "undefined")
+   {
+      listTextReplacements.listFormatStr = "n" + gConfigSettings.genColors.txtReplacementList
+        + "%-" + txtWidth + "s %-" + txtWidth + "s";
+   }
+   if (typeof(listTextReplacements.listFormatStrNormalAttr) == "undefined")
+      listTextReplacements.listFormatStrNormalAttr = "n%-" + txtWidth + "s %-" + txtWidth + "s";
+
+   // Limit the box height to up to 12 lines.
+   boxInfo.height = gNumTxtReplacements + 2;
+   if (boxInfo.height > 12)
+      boxInfo.height = 12;
+   boxInfo.topLeftX = gEditLeft + Math.floor((gEditWidth/2) - (boxInfo.width/2));
+   boxInfo.topLeftY = gEditTop + Math.floor((gEditHeight/2) - (boxInfo.height/2));
+
+   // Draw the top & bottom box borders for the list of text replacements
+   var originalCurpos = console.getxy();
+   console.gotoxy(boxInfo.topLeftX, boxInfo.topLeftY);
+   console.print(listTextReplacements.topBorder);
+   console.gotoxy(boxInfo.topLeftX, boxInfo.topLeftY+boxInfo.height-1);
+   console.print(listTextReplacements.bottomBorder);
+
+   // Set up some variables for the user input loop
+   const numItemsPerPage = boxInfo.height - 2;
+   const numPages = Math.ceil(listTextReplacements.txtReplacementArr.length / numItemsPerPage);
+   // For the horizontal location of the page number text for the box border:
+   // Based on the fact that there can be up to 9999 text replacements and 10
+   // per page, there will be up to 1000 pages of replacements.  To write the
+   // text, we'll want to be 20 characters to the left of the end of the border
+   // of the box.
+   const pageNumTxtStartX = boxInfo.topLeftX + boxInfo.width - 20;
+   var pageNum = 0;
+   var startArrIndex = 0;
+   var endArrIndex = 0; // One past the last array item
+   var screenY = 0;
+   // User input loop (also drawing the list of items)
+   var continueOn = true;
+   var refreshList = true; // For screen redraw optimizations
+   while (continueOn)
+   {
+      if (refreshList)
+      {
+         // Write the list of items for the current page
+         startArrIndex = pageNum * numItemsPerPage;
+         endArrIndex = startArrIndex + numItemsPerPage;
+         if (endArrIndex > listTextReplacements.txtReplacementArr.length)
+            endArrIndex = listTextReplacements.txtReplacementArr.length;
+         screenY = boxInfo.topLeftY + 1;
+         for (var i = startArrIndex; i < endArrIndex; ++i)
+         {
+            console.gotoxy(boxInfo.topLeftX, screenY);
+            console.print("n" + gConfigSettings.genColors.listBoxBorder + VERTICAL_SINGLE);
+            printf(listTextReplacements.listFormatStr,
+                   listTextReplacements.txtReplacementArr[i].originalText.substr(0, txtWidth),
+                   listTextReplacements.txtReplacementArr[i].replacement.substr(0, txtWidth));
+            console.print("n" + gConfigSettings.genColors.listBoxBorder + VERTICAL_SINGLE);
+            ++screenY;
+         }
+         // If the current screen row is below the bottom row inside the box,
+         // continue and write blank lines to the bottom of the inside of the box
+         // to blank out any text that might still be there.
+         //if (screenY < boxInfo.topLeftY+boxInfo.height)
+         while (screenY < boxInfo.topLeftY+boxInfo.height-1)
+         {
+            console.gotoxy(boxInfo.topLeftX, screenY);
+            console.print("n" + gConfigSettings.genColors.listBoxBorder + VERTICAL_SINGLE);
+            printf(listTextReplacements.listFormatStrNormalAttr, "", "");
+            console.print("n" + gConfigSettings.genColors.listBoxBorder + VERTICAL_SINGLE);
+            ++screenY;
+         }
+
+         // Update the page number in the top border of the box.
+         console.gotoxy(pageNumTxtStartX, boxInfo.topLeftY);
+         console.print("n" + gConfigSettings.genColors.listBoxBorder + RIGHT_T_SINGLE);
+         printf("n" + gConfigSettings.genColors.listBoxBorderText + "Page %4d of %4d", pageNum+1, numPages);
+         console.print("n" + gConfigSettings.genColors.listBoxBorder + LEFT_T_SINGLE);
+
+         // Just for sane appearance: Move the cursor to the first character of
+         // the first row and make it the color for the text replacements.
+         console.gotoxy(boxInfo.topLeftX+1, boxInfo.topLeftY+1);
+         console.print(gConfigSettings.genColors.txtReplacementList);
+      }
+
+      // Get a key from the user (upper-case) and take action based upon it.
+      userInput = console.getkey(K_UPPER | K_NOCRLF);
+      switch (userInput)
+      {
+         case KEY_UP:
+            // Go up one page
+            refreshList = (pageNum > 0);
+            if (refreshList)
+               --pageNum;
+            break;
+         case KEY_DOWN:
+            // Go down one page
+            refreshList = (pageNum < numPages-1);
+            if (refreshList)
+               ++pageNum;
+            break;
+         // Quit for ESC, Ctrl-T, Ctrl-A, and 'C' (close).
+         case KEY_ESC:
+         case CTRL_T:
+         case CTRL_A:
+         case 'C':
+            refreshList = false;
+            continueOn = false;
+            break;
+         default:
+            // Unrecognized command.  Don't refresh the list of the screen.
+            refreshList = false;
+            break;
+      }
+   }
+
+   // We're done listing the text replacements.
+   // Erase the list box rectangle by re-drawing the message text.  Then, move
+   // the cursor back to where it was originally.
+   var editLineIndexAtSelBoxTopRow = gEditLinesIndex - (originalCurpos.y-boxInfo.topLeftY);
+   displayMessageRectangle(boxInfo.topLeftX, boxInfo.topLeftY, boxInfo.width,
+                           boxInfo.height, editLineIndexAtSelBoxTopRow, true);
+   console.gotoxy(originalCurpos);
+   console.print(chooseEditColor());
+}
+
+// Writes some text over the bottom help line, with a pause before erasing the
+// text and refreshing the bottom help line.
+//
+// Parameters:
+//  pMsg: The text to write
+//  pPauseMS: The pause (in milliseconds) to wait while displaying the message
+function writeMsgOntBtmHelpLineWithPause(pMsg, pPauseMS)
+{
+   // Write the message with the pause, then refresh the help line on the
+   // bottom of the screen.
+   writeWithPause(1, console.screen_rows, pMsg, pPauseMS);
+   fpDisplayBottomHelpLine(console.screen_rows, gUseQuotes);
 }
\ No newline at end of file
diff --git a/exec/SlyEdit_DCTStuff.js b/exec/SlyEdit_DCTStuff.js
index 2183d77703771d1ba59cc95f24287726b0375c59..aed6ffb05af5689cd85029e9ca9cc60e602ea072 100644
--- a/exec/SlyEdit_DCTStuff.js
+++ b/exec/SlyEdit_DCTStuff.js
@@ -41,6 +41,11 @@
  *                              for cross-posting on the File menu.
  * 2013-08-23 Eric Oulashin     Updated readColorConfig() with the new general color
  *                              configuration settings.
+ * 2013-08-28 Eric Oulashin     Simplified readColorConfig() by having it call
+ *                              moveGenColorsToGenSettings() (defined in
+ *                              SlyEdit_Misc.js) to move the general colors
+ *                              into the genColors array in the configuration
+ *                              object.
  */
 
 load("sbbsdefs.js");
@@ -79,63 +84,7 @@ function readColorConfig(pFilename)
       gConfigSettings.DCTColors = colors;
       // Move the general color settings into gConfigSettings.genColors.*
       if (EDITOR_STYLE == "DCT")
-      {
-        if (gConfigSettings.DCTColors.hasOwnProperty("crossPostBorder"))
-           gConfigSettings.genColors.crossPostBorder = gConfigSettings.DCTColors.crossPostBorder;
-        if (gConfigSettings.DCTColors.hasOwnProperty("crossPostBorderText"))
-           gConfigSettings.genColors.crossPostBorderTxt = gConfigSettings.DCTColors.crossPostBorderText;
-        if (gConfigSettings.DCTColors.hasOwnProperty("crossPostMsgAreaNum"))
-           gConfigSettings.genColors.crossPostMsgAreaNum = gConfigSettings.DCTColors.crossPostMsgAreaNum;
-        if (gConfigSettings.DCTColors.hasOwnProperty("crossPostMsgAreaNumHighlight"))
-           gConfigSettings.genColors.crossPostMsgAreaNumHighlight = gConfigSettings.DCTColors.crossPostMsgAreaNumHighlight;
-        if (gConfigSettings.DCTColors.hasOwnProperty("crossPostMsgAreaDesc"))
-           gConfigSettings.genColors.crossPostMsgAreaDesc = gConfigSettings.DCTColors.crossPostMsgAreaDesc;
-        if (gConfigSettings.DCTColors.hasOwnProperty("crossPostMsgAreaDescHighlight"))
-           gConfigSettings.genColors.crossPostMsgAreaDescHighlight = gConfigSettings.DCTColors.crossPostMsgAreaDescHighlight;
-        if (gConfigSettings.DCTColors.hasOwnProperty("crossPostChk"))
-           gConfigSettings.genColors.crossPostChk = gConfigSettings.DCTColors.crossPostChk;
-        if (gConfigSettings.DCTColors.hasOwnProperty("crossPostChkHighlight"))
-           gConfigSettings.genColors.crossPostChkHighlight = gConfigSettings.DCTColors.crossPostChkHighlight;
-        if (gConfigSettings.DCTColors.hasOwnProperty("crossPostMsgGrpMark"))
-           gConfigSettings.genColors.crossPostMsgGrpMark = gConfigSettings.DCTColors.crossPostMsgGrpMark;
-        if (gConfigSettings.DCTColors.hasOwnProperty("crossPostMsgGrpMarkHighlight"))
-           gConfigSettings.genColors.crossPostMsgGrpMarkHighlight = gConfigSettings.DCTColors.crossPostMsgGrpMarkHighlight;
-        if (gConfigSettings.DCTColors.hasOwnProperty("msgWillBePostedHdr"))
-           gConfigSettings.genColors.msgWillBePostedHdr = gConfigSettings.DCTColors.msgWillBePostedHdr;
-        if (gConfigSettings.DCTColors.hasOwnProperty("msgPostedGrpHdr"))
-           gConfigSettings.genColors.msgPostedGrpHdr = gConfigSettings.DCTColors.msgPostedGrpHdr;
-        if (gConfigSettings.DCTColors.hasOwnProperty("msgPostedSubBoardName"))
-           gConfigSettings.genColors.msgPostedSubBoardName = gConfigSettings.DCTColors.msgPostedSubBoardName;
-        if (gConfigSettings.DCTColors.hasOwnProperty("msgPostedOriginalAreaText"))
-           gConfigSettings.genColors.msgPostedOriginalAreaText = gConfigSettings.DCTColors.msgPostedOriginalAreaText;
-        if (gConfigSettings.DCTColors.hasOwnProperty("msgHasBeenSavedText"))
-           gConfigSettings.genColors.msgHasBeenSavedText = gConfigSettings.DCTColors.msgHasBeenSavedText;
-        if (gConfigSettings.DCTColors.hasOwnProperty("msgAbortedText"))
-           gConfigSettings.genColors.msgAbortedText = gConfigSettings.DCTColors.msgAbortedText;
-        if (gConfigSettings.DCTColors.hasOwnProperty("emptyMsgNotSentText"))
-           gConfigSettings.genColors.emptyMsgNotSentText = gConfigSettings.DCTColors.emptyMsgNotSentText;
-        if (gConfigSettings.DCTColors.hasOwnProperty("genMsgErrorText"))
-           gConfigSettings.genColors.genMsgErrorText = gConfigSettings.DCTColors.genMsgErrorText;
-
-        delete gConfigSettings.DCTColors.crossPostBorder;
-        delete gConfigSettings.DCTColors.crossPostBorderText;
-        delete gConfigSettings.DCTColors.crossPostMsgAreaNum;
-        delete gConfigSettings.DCTColors.crossPostMsgAreaNumHighlight;
-        delete gConfigSettings.DCTColors.crossPostMsgAreaDesc;
-        delete gConfigSettings.DCTColors.crossPostMsgAreaDescHighlight;
-        delete gConfigSettings.DCTColors.crossPostChk;
-        delete gConfigSettings.DCTColors.crossPostChkHighlight;
-        delete gConfigSettings.DCTColors.crossPostMsgGrpMark;
-        delete gConfigSettings.DCTColors.crossPostMsgGrpMarkHighlight;
-        delete gConfigSettings.DCTColors.msgWillBePostedHdr;
-        delete gConfigSettings.DCTColors.msgPostedGrpHdr;
-        delete gConfigSettings.DCTColors.msgPostedSubBoardName;
-        delete gConfigSettings.DCTColors.msgPostedOriginalAreaText;
-        delete gConfigSettings.DCTColors.msgHasBeenSavedText;
-        delete gConfigSettings.DCTColors.msgAbortedText;
-        delete gConfigSettings.DCTColors.emptyMsgNotSentText;
-        delete gConfigSettings.DCTColors.genMsgErrorText;
-      }
+        moveGenColorsToGenSettings(gConfigSettings.DCTColors, gConfigSettings);
    }
 }
 
diff --git a/exec/SlyEdit_IceStuff.js b/exec/SlyEdit_IceStuff.js
index 0eeff41452790a55bb840e28e553c358ebbb102e..0210190e18d58737196b91a284558005532a0cf9 100644
--- a/exec/SlyEdit_IceStuff.js
+++ b/exec/SlyEdit_IceStuff.js
@@ -27,6 +27,11 @@
  *                              for cross-posting, when allowed.
  * 2013-08-23 Eric Oulashin     Updated readColorConfig() with the new general color
  *                              configuration settings.
+ * 2013-08-28 Eric Oulashin     Simplified readColorConfig() by having it call
+ *                              moveGenColorsToGenSettings() (defined in
+ *                              SlyEdit_Misc.js) to move the general colors
+ *                              into the genColors array in the configuration
+ *                              object.
  */
 
 load("sbbsdefs.js");
@@ -58,63 +63,7 @@ function readColorConfig(pFilename)
       gConfigSettings.iceColors = colors;
       // Move the general color settings into gConfigSettings.genColors.*
       if (EDITOR_STYLE == "ICE")
-      {
-        if (gConfigSettings.iceColors.hasOwnProperty("crossPostBorder"))
-           gConfigSettings.genColors.crossPostBorder = gConfigSettings.iceColors.crossPostBorder;
-        if (gConfigSettings.iceColors.hasOwnProperty("crossPostBorderText"))
-           gConfigSettings.genColors.crossPostBorderTxt = gConfigSettings.iceColors.crossPostBorderText;
-        if (gConfigSettings.iceColors.hasOwnProperty("crossPostMsgAreaNum"))
-           gConfigSettings.genColors.crossPostMsgAreaNum = gConfigSettings.iceColors.crossPostMsgAreaNum;
-        if (gConfigSettings.iceColors.hasOwnProperty("crossPostMsgAreaNumHighlight"))
-           gConfigSettings.genColors.crossPostMsgAreaNumHighlight = gConfigSettings.iceColors.crossPostMsgAreaNumHighlight;
-        if (gConfigSettings.iceColors.hasOwnProperty("crossPostMsgAreaDesc"))
-           gConfigSettings.genColors.crossPostMsgAreaDesc = gConfigSettings.iceColors.crossPostMsgAreaDesc;
-        if (gConfigSettings.iceColors.hasOwnProperty("crossPostMsgAreaDescHighlight"))
-           gConfigSettings.genColors.crossPostMsgAreaDescHighlight = gConfigSettings.iceColors.crossPostMsgAreaDescHighlight;
-        if (gConfigSettings.iceColors.hasOwnProperty("crossPostChk"))
-           gConfigSettings.genColors.crossPostChk = gConfigSettings.iceColors.crossPostChk;
-        if (gConfigSettings.iceColors.hasOwnProperty("crossPostChkHighlight"))
-           gConfigSettings.genColors.crossPostChkHighlight = gConfigSettings.iceColors.crossPostChkHighlight;
-        if (gConfigSettings.iceColors.hasOwnProperty("crossPostMsgGrpMark"))
-           gConfigSettings.genColors.crossPostMsgGrpMark = gConfigSettings.iceColors.crossPostMsgGrpMark;
-        if (gConfigSettings.iceColors.hasOwnProperty("crossPostMsgGrpMarkHighlight"))
-           gConfigSettings.genColors.crossPostMsgGrpMarkHighlight = gConfigSettings.iceColors.crossPostMsgGrpMarkHighlight;
-        if (gConfigSettings.iceColors.hasOwnProperty("msgWillBePostedHdr"))
-           gConfigSettings.genColors.msgWillBePostedHdr = gConfigSettings.iceColors.msgWillBePostedHdr;
-        if (gConfigSettings.iceColors.hasOwnProperty("msgPostedGrpHdr"))
-           gConfigSettings.genColors.msgPostedGrpHdr = gConfigSettings.iceColors.msgPostedGrpHdr;
-        if (gConfigSettings.iceColors.hasOwnProperty("msgPostedSubBoardName"))
-           gConfigSettings.genColors.msgPostedSubBoardName = gConfigSettings.iceColors.msgPostedSubBoardName;
-        if (gConfigSettings.iceColors.hasOwnProperty("msgPostedOriginalAreaText"))
-           gConfigSettings.genColors.msgPostedOriginalAreaText = gConfigSettings.iceColors.msgPostedOriginalAreaText;
-        if (gConfigSettings.iceColors.hasOwnProperty("msgHasBeenSavedText"))
-           gConfigSettings.genColors.msgHasBeenSavedText = gConfigSettings.iceColors.msgHasBeenSavedText;
-        if (gConfigSettings.iceColors.hasOwnProperty("msgAbortedText"))
-           gConfigSettings.genColors.msgAbortedText = gConfigSettings.iceColors.msgAbortedText;
-        if (gConfigSettings.iceColors.hasOwnProperty("emptyMsgNotSentText"))
-           gConfigSettings.genColors.emptyMsgNotSentText = gConfigSettings.iceColors.emptyMsgNotSentText;
-        if (gConfigSettings.iceColors.hasOwnProperty("genMsgErrorText"))
-           gConfigSettings.genColors.genMsgErrorText = gConfigSettings.iceColors.genMsgErrorText;
-
-        delete gConfigSettings.iceColors.crossPostBorder;
-        delete gConfigSettings.iceColors.crossPostBorderText;
-        delete gConfigSettings.iceColors.crossPostMsgAreaNum;
-        delete gConfigSettings.iceColors.crossPostMsgAreaNumHighlight;
-        delete gConfigSettings.iceColors.crossPostMsgAreaDesc;
-        delete gConfigSettings.iceColors.crossPostMsgAreaDescHighlight;
-        delete gConfigSettings.iceColors.crossPostChk;
-        delete gConfigSettings.iceColors.crossPostChkHighlight;
-        delete gConfigSettings.iceColors.crossPostMsgGrpMark;
-        delete gConfigSettings.iceColors.crossPostMsgGrpMarkHighlight;
-        delete gConfigSettings.iceColors.msgWillBePostedHdr;
-        delete gConfigSettings.iceColors.msgPostedGrpHdr;
-        delete gConfigSettings.iceColors.msgPostedSubBoardName;
-        delete gConfigSettings.iceColors.msgPostedOriginalAreaText;
-        delete gConfigSettings.iceColors.msgHasBeenSavedText;
-        delete gConfigSettings.iceColors.msgAbortedText;
-        delete gConfigSettings.iceColors.emptyMsgNotSentText;
-        delete gConfigSettings.iceColors.genMsgErrorText;
-      }
+         moveGenColorsToGenSettings(gConfigSettings.iceColors, gConfigSettings);
    }
 }
 
diff --git a/exec/SlyEdit_Misc.js b/exec/SlyEdit_Misc.js
index 81b3780104edd94c03d55b962a6f7e405ba845ba..90e8b59355c557298da04989992b59cd63e70560 100644
--- a/exec/SlyEdit_Misc.js
+++ b/exec/SlyEdit_Misc.js
@@ -12,107 +12,28 @@
  * 2009-08-22 Eric Oulashin     Version 1.00
  *                              Initial public release
  * ....Removed some comments...
- * 2012-12-21 Eric Oulashin     Updated to check for the .cfg files in the
- *                              sbbs/ctrl directory first, and if they aren't
- *                              there, assume they're in the same directory as
- *                              the .js file.
- * 2012-12-23 Eric Oulashin     Worked on updating wrapQuoteLines() and
- *                              firstNonQuoteTxtIndex() to support putting the
- *                              "To" user's initials before the > in quote lines.
- * 2012-12-25 Eric Oulashin     Updated wrapQuoteLines() to insert a > in quote
- *                              lines right after the leading quote characters
- *                              (if any), without a space afteward, to indicate
- *                              an additional level of quoting.
- * 2012-12-27 Eric Oulashin     Bug fix in wrapQuoteLines(): When prefixing
- *                              quote lines with author initials, if wrapping
- *                              resulted in additional quote lines, it wasn't
- *                              adding a > character to the additional line(s).
- * 2012-12-28 Eric Oulashin     Updated firstNonQuoteTxtIndex() to more
- *                              intelligently find the first non-quote index
- *                              when using author initials in quote lines.
- *                              I.e., dealing with multiply-quoted lines that
- *                              start like this:
- *                              > AD> 
- *                              > AD>>
- *                              etc..
- * 2012-12-30 Eric Oulashin     Added the getCurMsgInfo() and
- *                              getFromNameForCurMsg() functions.
- *                              getCurMsgInfo() reads DDML_SyncSMBInfo.txt,
- *                              which is written to the node directory by
- *                              the Digital Distortion Message Lister and
- *                              contains information about the current
- *                              message being read so that SlyEdit can
- *                              read it for getting the sender name from
- *                              the message header.  That was necessary
- *                              because the information about that in the
- *                              bbs object (provided by Synchronet) can't
- *                              be modified.
- * 2013-01-02 Eric Oulashin     Fixed a bug in getFromNameForCurMsg() where
- *                              reading low-numbered messages in a sub-board
- *                              would result in getting the incorrect
- *                              original author name.  Updated
- *                              getFromNameForCurMsg() to just use the
- *                              sub-board code and message offset to get
- *                              the header for the current message being
- *                              read.
- * 2013-01-13 Eric Oulashin     Added calcPageNum().
- * 2013-01-18 Eric Oulashin     Updated ReadSlyEditConfigFile() to include
- *                              border color & border text color settings
- *                              for choosing message areas for cross-posting.
- * 2013-01-19 Eric Oulashin     Added postingInMsgSubBoard().  Updated
- *                              ReadSlyEditConfigFile() to read a new setting,
- *                              allowCrossPosting, from the config file.  Added
- *                              CHECK_CHAR.
- * 2013-01-20 Eric Oulashin     Added numObjProperties().  Also added
- *                              postMsgToSubBoard(), for cross-posting support.
- * 2013-01-22 Eric Oulashin     Updated displayProgramExitInfo(): Removed the
- *                              SlyEdit block text so that the overall message
- *                              it displays is shorter.
- * 2013-01-24 Eric Oulashin     Updated ReadSlyEditConfigFile() to check the
- *                              following directories, in order, for the
- *                              configuration files:
- *                              1. Mods directory
- *                              2. Ctrl directory
- *                              3. Current directory (where SlyEdit is located)
- * 2013-02-03 Eric Oulashin     Added readUserSigFile().
- * 2013-02-13 Eric Oulashin     Updated getCurMsgInfo() to get the first
- *                              postable message sub-board if the user has no
- *                              current sub-board (i.e., a new user is applying
- *                              for access).  Also, updated ReadSlyEditConfigFile()
- *                              to default indentQuoteLinesWithInitials to true.
- * 2013-05-14 Eric Oulashin     Updated getCurMsgInfo() and getFromNameForCurMsg()
- *                              to use the absolute message number (bbs.msg_number)
- *                              rather than messages indexes so that it gets
- *                              the correct message header in all cases, including
- *                              when replying to messages during "Scan for messages
- *                              to you".
- * 2013-05-16 Eric Oulashin     Added a function that returns whether the
- *                              Synchronet compile date is at least May 12, 2013.
- *                              That was when Digital Man's change to make
- *                              bbs.msg_number work when a script is running
- *                              first went into the Synchronet daily builds.
- * 2013-05-18 Eric Oulashin     Speed optimization (hopefully) for the
- *                              aforementioned function: Made the return
- *                              value a function property so that it only has
- *                              to be figured out once, and eliminates the
- *                              need for a global variable to store it for
- *                              speed optimization purposes.
- * 2013-05-23 Eric Oulashin     Simplified the decision of whether to use
- *                              bbs.msg_number or bbs.smb_curmsg by checking
- *                              whether bbs.msg_number is > 0 (if it is, then
- *                              it's valid, so use it).  This is simpler than
- *                              checking the Synchronet version & build date.
- * 2013-05-24 Eric Oulashin     Updated getCurMsgInfo() to add one more
- *                              data member to the return object:
- *                              msgNumIsOffset, which stores whether or not the
- *                              message number is an offset.  If not, then it's
- *                              the aboslute message number (i.e., bbs.msg_number).
- *                              That simplified getFromNameForCurMsg(), which can
- *                              pass that to msgBase.get_msg_header().
  * 2013-08-24 Eric Oulashin     Bug fix in wrapQuoteLines(): Off-by-one bug toward
  *                              the end where there might be more quote lines
  *                              than lineInfo objects, so it wouldn't quote the
  *                              last line when using author initials.
+ * 2013-08-28 Eric Oulashin     Updated ReadSlyEditConfigFile() to read and
+ *                              set the enableTextReplacements setting.  It
+ *                              defaults to false.  Also added populateTxtReplacements().
+ *                              Added moveGenColorsToGenSettings(), which
+ *                              can be called by JavaScripts for different
+ *                              UI styles to move the general color settings
+ *                              from their own color array into the genColors
+ *                              array in the configuration object.
+ * 2013-08-31 Eric Oulashin     Added the function getWordFromEditLine().
+ * 2013-09-02 Eric Oulashin     Worked on the new function doMacroTxtReplacementInEditLine(),
+ *                              which performs text replacement (AKA macros) on
+ *                              one of the message edit lines.  Added
+ *                              genFullPathCfgFilename() so that the logic for finding
+ *                              the configuration files is all in one place.  Added
+ *                              getFirstLetterFromStr() and firstLetterIsUppercase(),
+ *                              which are helpers for doMacroTxtReplacementInEditLine()
+ *                              for checking & fixing first-letter capitalization
+ *                              after doing a regex replace.
  *                              
  */
 
@@ -360,8 +281,6 @@ function getCurrentTimeStr()
 function isPrintableChar(pText)
 {
    // Make sure pText is valid and is a string.
-   if ((pText == null) || (pText == undefined))
-      return false;
    if (typeof(pText) != "string")
       return false;
    if (pText.length == 0)
@@ -460,7 +379,8 @@ function displayHelpHeader()
 //  pPause: Whether or not to pause at the end
 //  pCanCrossPost: Whether or not cross-posting is enabled
 //  pIsSysop: Whether or not the user is the sysop.
-function displayCommandList(pDisplayHeader, pClear, pPause, pCanCrossPost, pIsSysop)
+//  pTxtReplacments: Whether or not the text replacements feature is enabled
+function displayCommandList(pDisplayHeader, pClear, pPause, pCanCrossPost, pIsSysop, pTxtReplacments)
 {
    if (pClear)
       console.clear("n");
@@ -506,6 +426,8 @@ function displayCommandList(pDisplayHeader, pClear, pPause, pCanCrossPost, pIsSy
    displayCmdKeyFormattedDouble("Ctrl-G", "General help", "/A", "Abort", true);
    displayCmdKeyFormattedDouble("Ctrl-P", "Command key help", "/S", "Save", true);
    displayCmdKeyFormattedDouble("Ctrl-R", "Program information", "/Q", "Quote message", true);
+   if (pTxtReplacments)
+      displayCmdKeyFormattedDouble("Ctrl-T", "List text replacements", "/T", "List text replacements", true);
    if (pCanCrossPost)
       displayCmdKeyFormattedDouble("", "", "/C", "Cross-post selection", true);
    printf(" ch%-7sg  nc%s", "", "", "/?", "Show help");
@@ -553,61 +475,23 @@ function displayGeneralHelp(pDisplayHeader, pClear, pPause)
       console.pause();
 }
 
-// Displays the text to display above program info screens.
-function displayProgInfoHeader()
-{
-   // Construct the header text lines only once.
-   if (typeof(displayProgInfoHeader.headerLines) == "undefined")
-   {
-      displayProgInfoHeader.headerLines = new Array();
-
-      var progNameLen = strip_ctrl(EDITOR_PROGRAM_NAME).length;
-
-      // Top border
-      var headerTextStr = "nhc" + UPPER_LEFT_SINGLE;
-      for (var i = 0; i < progNameLen + 2; ++i)
-         headerTextStr += HORIZONTAL_SINGLE;
-      headerTextStr += UPPER_RIGHT_SINGLE;
-      displayProgInfoHeader.headerLines.push(headerTextStr);
-
-      // Middle line: Header text string
-      headerTextStr = VERTICAL_SINGLE + "4y " + EDITOR_PROGRAM_NAME + " nhc"
-                    + VERTICAL_SINGLE;
-      displayProgInfoHeader.headerLines.push(headerTextStr);
-
-      // Lower border
-      headerTextStr = LOWER_LEFT_SINGLE;
-      for (var i = 0; i < progNameLen + 2; ++i)
-         headerTextStr += HORIZONTAL_SINGLE;
-      headerTextStr += LOWER_RIGHT_SINGLE;
-      displayProgInfoHeader.headerLines.push(headerTextStr);
-   }
-
-   // Print the header strings
-   for (var index in displayProgInfoHeader.headerLines)
-      console.center(displayProgInfoHeader.headerLines[index]);
-}
-
 // Displays program information.
 //
 // Parameters:
-//  pDisplayHeader: Whether or not to display the help header.
 //  pClear: Whether or not to clear the screen first
 //  pPause: Whether or not to pause at the end
-function displayProgramInfo(pDisplayHeader, pClear, pPause)
+function displayProgramInfo(pClear, pPause)
 {
    if (pClear)
       console.clear("n");
-   if (pDisplayHeader)
-      displayProgInfoHeader();
 
    // Print the program information
-   console.center("ncVersion g" + EDITOR_VERSION + " wh(b" +
-                  EDITOR_VER_DATE + "w)");
+   console.center("nhc" + EDITOR_PROGRAM_NAME + "n cVersion g" +
+                  EDITOR_VERSION + " wh(b" + EDITOR_VER_DATE + "w)");
    console.center("ncby Eric Oulashin");
    console.crlf();
-   console.print("ncSlyEdit is a full-screen message editor written for Synchronet that mimics\r\n");
-   console.print("the look & feel of IceEdit or DCT Edit.");
+   console.print("ncSlyEdit is a full-screen message editor for Synchronet that mimics the look &\r\n");
+   console.print("feel of IceEdit or DCT Edit.");
    console.crlf();
    if (pPause)
       console.pause();
@@ -736,12 +620,17 @@ function ReadSlyEditConfigFile()
    // initials.
    cfgObj.indentQuoteLinesWithInitials = true;
    cfgObj.allowCrossPosting = true;
+   cfgObj.enableTextReplacements = false;
+   cfgObj.textReplacementsUseRegex = false;
 
    // General SlyEdit color settings
    cfgObj.genColors = new Object();
    // Cross-posting UI element colors
-   cfgObj.genColors.crossPostBorder = "ng";
-   cfgObj.genColors.crossPostBorderTxt = "nbh";
+   // Deprecated colors:
+   //cfgObj.genColors.crossPostBorder = "ng";
+   //cfgObj.genColors.crossPostBorderTxt = "nbh";
+   cfgObj.genColors.listBoxBorder = "ng";
+   cfgObj.genColors.listBoxBorderText = "nbh";
    cfgObj.genColors.crossPostMsgAreaNum = "nhw";
    cfgObj.genColors.crossPostMsgAreaNumHighlight = "n4hw";
    cfgObj.genColors.crossPostMsgAreaDesc = "nc";
@@ -763,11 +652,7 @@ function ReadSlyEditConfigFile()
    // Default Ice-style colors
    cfgObj.iceColors = new Object();
    // Ice color theme file
-   cfgObj.iceColors.ThemeFilename = system.mods_dir + "SlyIceColors_BlueIce.cfg";
-   if (!file_exists(cfgObj.iceColors.ThemeFilename))
-      cfgObj.iceColors.ThemeFilename = system.ctrl_dir + "SlyIceColors_BlueIce.cfg";
-   if (!file_exists(cfgObj.iceColors.ThemeFilename))
-      cfgObj.iceColors.ThemeFilename = gStartupPath + "SlyIceColors_BlueIce.cfg";
+   cfgObj.iceColors.ThemeFilename = genFullPathCfgFilename("SlyIceColors_BlueIce.cfg", gStartupPath);
    // Text edit color
    cfgObj.iceColors.TextEditColor = "nw";
    // Quote line color
@@ -798,11 +683,7 @@ function ReadSlyEditConfigFile()
    // Default DCT-style colors
    cfgObj.DCTColors = new Object();
    // DCT color theme file
-   cfgObj.DCTColors.ThemeFilename = system.mods_dir + "SlyDCTColors_Default.cfg";
-   if (!file_exists(cfgObj.DCTColors.ThemeFilename))
-      cfgObj.DCTColors.ThemeFilename = system.ctrl_dir + "SlyDCTColors_Default.cfg";
-   if (!file_exists(cfgObj.DCTColors.ThemeFilename))
-      cfgObj.DCTColors.ThemeFilename = gStartupPath + "SlyDCTColors_Default.cfg";
+   cfgObj.DCTColors.ThemeFilename = genFullPathCfgFilename("SlyDCTColors_Default.cfg", gStartupPath);
    // Text edit color
    cfgObj.DCTColors.TextEditColor = "nw";
    // Quote line color
@@ -861,11 +742,7 @@ function ReadSlyEditConfigFile()
    cfgObj.DCTColors.MenuHotkeys = "nwh7";
 
    // Open the SlyEdit configuration file
-   var slyEdCfgFileName = system.mods_dir + "SlyEdit.cfg";
-   if (!file_exists(slyEdCfgFileName))
-      slyEdCfgFileName = system.ctrl_dir + "SlyEdit.cfg";
-   if (!file_exists(slyEdCfgFileName))
-      slyEdCfgFileName = gStartupPath + "SlyEdit.cfg";
+   var slyEdCfgFileName = genFullPathCfgFilename("SlyEdit.cfg", gStartupPath);
    var cfgFile = new File(slyEdCfgFileName);
    if (cfgFile.open("r"))
    {
@@ -954,28 +831,29 @@ function ReadSlyEditConfigFile()
                   cfgObj.runJSOnExit.push(value);
                else if (settingUpper == "ALLOWCROSSPOSTING")
                   cfgObj.allowCrossPosting = (valueUpper == "TRUE");
+               else if (settingUpper == "ENABLETEXTREPLACEMENTS")
+               {
+                  // The enableTxtReplacements setting in the config file can
+                  // be regex, true, or false:
+                  //  - regex: Text replacement enabled using regular expressions
+                  //  - true: Text replacement enabled using exact match
+                  //  - false: Text replacement disabled
+                  cfgObj.textReplacementsUseRegex = (valueUpper == "REGEX");
+                  if (cfgObj.textReplacementsUseRegex)
+                     cfgObj.enableTextReplacements = true;
+                  else
+                     cfgObj.enableTextReplacements = (valueUpper == "TRUE");
+               }
             }
             else if (settingsMode == "ICEColors")
             {
                if (settingUpper == "THEMEFILENAME")
-               {
-                  cfgObj.iceColors.ThemeFilename = system.mods_dir + value;
-                  if (!file_exists(cfgObj.iceColors.ThemeFilename))
-                     cfgObj.iceColors.ThemeFilename = system.ctrl_dir + value;
-                  if (!file_exists(cfgObj.iceColors.ThemeFilename))
-                     cfgObj.iceColors.ThemeFilename = gStartupPath + value;
-               }
+                  cfgObj.iceColors.ThemeFilename = genFullPathCfgFilename(value, gStartupPath);
             }
             else if (settingsMode == "DCTColors")
             {
                if (settingUpper == "THEMEFILENAME")
-               {
-                  cfgObj.DCTColors.ThemeFilename = system.mods_dir + value;
-                  if (!file_exists(cfgObj.DCTColors.ThemeFilename))
-                     cfgObj.DCTColors.ThemeFilename = system.ctrl_dir + value;
-                  if (!file_exists(cfgObj.DCTColors.ThemeFilename))
-                     cfgObj.DCTColors.ThemeFilename = gStartupPath + value;
-               }
+                  cfgObj.DCTColors.ThemeFilename = genFullPathCfgFilename(value, gStartupPath);
             }
          }
       }
@@ -2388,6 +2266,410 @@ function getFirstPostableSubInfo()
   return retObj;
 }
 
+// Reads SlyEdit_TextReplacements.cfg (from sbbs/mods, sbbs/ctrl, or the
+// script's directory) and populates an associative array with the WORD=text
+// pairs.  When not using regular expressions, the key will be in all uppercase
+// and the value in lowercase.  This function will read up to 9999 replacements.
+//
+// Parameters:
+//  pArray: The array to populate.  Must be created as "new Array()".
+//  pRegex: Whether or not the text replace feature is configured to use regular
+//          expressions.  If so, then the search words in the array will not
+//          be converted to uppercase and the replacement text will not be
+//          converted to lowercase.
+//
+// Return value: The number of text replacements added to the array.
+function populateTxtReplacements(pArray, pRegex)
+{
+   var numTxtReplacements = 0;
+
+   // Note: Limited to words without spaces.
+   // Open the word replacements configuration file
+   var wordReplacementsFilename = genFullPathCfgFilename("SlyEdit_TextReplacements.cfg", gStartupPath);
+   var arrayPopulated = false;
+   var wordFile = new File(wordReplacementsFilename);
+   if (wordFile.open("r"))
+   {
+      var fileLine = null;      // A line read from the file
+      var equalsPos = 0;        // Position of a = in the line
+      var wordToSearch = null; // A word to be replaced
+      var substWord = null;    // The word to substitue
+      // This tests numTxtReplacements < 9999 so that the 9999th one is the last
+      // one read.
+      while (!wordFile.eof && (numTxtReplacements < 9999))
+      {
+         // Read the next line from the config file.
+         fileLine = wordFile.readln(2048);
+
+         // fileLine should be a string, but I've seen some cases
+         // where for some reason it isn't.  If it's not a string,
+         // then continue onto the next line.
+         if (typeof(fileLine) != "string")
+            continue;
+         // If the line starts with with a semicolon (the comment
+         // character) or is blank, then skip it.
+         if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
+            continue;
+
+         // Look for an equals sign, and if found, separate the line
+         // into the setting name (before the =) and the value (after the
+         // equals sign).
+         equalsPos = fileLine.indexOf("=");
+         if (equalsPos <= 0)
+            continue; // = not found or is at the beginning, so go on to the next line
+
+         // Extract the word to search and substitution word from the line.  If
+         // not using regular expressions, then convert the word to search to
+         // all uppercase (for case-insensitive searching) and the substitution
+         // word to all lowercase (to make sure it looks good).
+         wordToSearch = trimSpaces(fileLine.substr(0, equalsPos), true, false, true);
+         substWord = trimSpaces(fileLine.substr(equalsPos+1), true, false, true);
+         // Make sure substWord only contains printable characters.  If not, then
+         // skip this one.
+         var substIsPrintable = true;
+         for (var i = 0; (i < substWord.length) && substIsPrintable; ++i)
+            substIsPrintable = isPrintableChar(substWord.charAt(i));
+         if (!substIsPrintable)
+            continue;
+
+         // And add the search word and replacement text to pArray.
+         if (pRegex)
+         {
+            if (wordToSearch.toUpperCase() != substWord.toUpperCase())
+            {
+               pArray[wordToSearch] = substWord;
+               ++numTxtReplacements;
+            }
+         }
+         else
+         {
+            wordToSearch = wordToSearch.toUpperCase();
+            substWord = substWord.toLowerCase();
+            if (wordToSearch != substWord.toUpperCase())
+            {
+               pArray[wordToSearch] = substWord;
+               ++numTxtReplacements;
+            }
+         }
+      }
+
+      wordFile.close();
+   }
+
+   return numTxtReplacements;
+}
+
+function moveGenColorsToGenSettings(pColorsArray, pCfgObj)
+{
+   // Set up an array of color setting names 
+   var colorSettingStrings = new Array();
+   colorSettingStrings.push("crossPostBorder"); // Deprecated
+   colorSettingStrings.push("crossPostBorderText"); // Deprecated
+   colorSettingStrings.push("listBoxBorder");
+   colorSettingStrings.push("listBoxBorderText");
+   colorSettingStrings.push("crossPostMsgAreaNum");
+   colorSettingStrings.push("crossPostMsgAreaNumHighlight");
+   colorSettingStrings.push("crossPostMsgAreaDesc");
+   colorSettingStrings.push("crossPostMsgAreaDescHighlight");
+   colorSettingStrings.push("crossPostChk");
+   colorSettingStrings.push("crossPostChkHighlight");
+   colorSettingStrings.push("crossPostMsgGrpMark");
+   colorSettingStrings.push("crossPostMsgGrpMarkHighlight");
+   colorSettingStrings.push("msgWillBePostedHdr");
+   colorSettingStrings.push("msgPostedGrpHdr");
+   colorSettingStrings.push("msgPostedSubBoardName");
+   colorSettingStrings.push("msgPostedOriginalAreaText");
+   colorSettingStrings.push("msgHasBeenSavedText");
+   colorSettingStrings.push("msgAbortedText");
+   colorSettingStrings.push("emptyMsgNotSentText");
+   colorSettingStrings.push("genMsgErrorText");
+   colorSettingStrings.push("txtReplacementList");
+
+   var colorName = "";
+   for (var i = 0; i < colorSettingStrings.length; ++i)
+   {
+      colorName = colorSettingStrings[i];
+      if (pColorsArray.hasOwnProperty(colorName))
+      {
+         pCfgObj.genColors[colorName] = pColorsArray[colorName];
+         delete pColorsArray[colorName];
+      }
+   }
+   // If listBoxBorder and listBoxBorderText exist in the general colors settings,
+   // then remove crossPostBorder and crossPostBorderText if they exist.
+   if (pCfgObj.genColors.hasOwnProperty["listBoxBorder"] && pCfgObj.genColors.hasOwnProperty["crossPostBorder"])
+   {
+      // Favor crossPostBorder to preserve backwards compatibility.
+      pCfgObj.genColors["listBoxBorder"] = pCfgObj.genColors["crossPostBorder"];
+      delete pCfgObj.genColors["crossPostBorder"];
+   }
+   if (pCfgObj.genColors.hasOwnProperty["listBoxBorderText"] && pCfgObj.genColors.hasOwnProperty["crossPostBorderText"])
+   {
+      // Favor crossPostBorderText to preserve backwards compatibility.
+      pCfgObj.genColors["listBoxBorderText"] = pCfgObj.genColors["crossPostBorderText"];
+      delete pCfgObj.genColors["crossPostBorderText"];
+   }
+}
+
+// Returns whether or not a character is a letter.
+//
+// Parameters:
+//  pChar: The character to test
+//
+// Return value: Boolean - Whether or not the character is a letter
+function charIsLetter(pChar)
+{
+   return /^[ABCDEFGHIJKLMNOPQRSTUVWXYZ�������������������������������������������������������������]$/.test(pChar.toUpperCase());
+}
+
+// Returns the word in a text line at a given index.  If the index
+// is at a space, then this function will return the word before
+// (to the left of) the space.
+//
+// Parameters:
+//  pEditLinesIndex: The index of the line to look at (0-based)
+//  pCharIndex: The character index in the text line (0-based)
+//
+// Return value: An object containing the following properties:
+//               foundWord: Whether or not a word was found (boolean)
+//               word: The word in the edit line at the given indexes (text)
+//               editLineIndex: The index of the edit line (integer)
+//               startIdx: The index of the first character of the word (integer)
+//               endIndex: The index of the last character of the word (integer)
+function getWordFromEditLine(pEditLinesIndex, pCharIndex)
+{
+   var retObj = new Object();
+   retObj.foundWord = false;
+   retObj.word = "";
+   retObj.editLineIndex = pEditLinesIndex;
+   retObj.startIdx = 0;
+   retObj.endIndex = 0;
+
+   // Parameter checking
+   if ((pEditLinesIndex < 0) || (pEditLinesIndex >= gEditLines.length))
+   {
+      retObj.editLineIndex = 0;
+      return retObj;
+   }
+   if ((pCharIndex < 0) || (pCharIndex >= gEditLines[pEditLinesIndex].text.length))
+   {
+      //displayDebugText(1, 1, "pCharIndex: " + pCharIndex, null, true, false); // Temporary
+      //displayDebugText(1, 2, "Line len: " + gEditLines[pEditLinesIndex].text.length, console.getxy(), true, false); // Temporary
+      return retObj;
+   }
+
+   // If pCharIndex specifies the index of a space, then look for a non-space
+   // character before it.
+   var charIndex = pCharIndex;
+   while (gEditLines[pEditLinesIndex].text.charAt(charIndex) == " ")
+      --charIndex;
+   // Look for the start & end of the word based on the indexes of a space
+   // before and at/after the given character index.
+   var wordStartIdx = charIndex;
+   var wordEndIdx = charIndex;
+   while ((gEditLines[pEditLinesIndex].text.charAt(wordStartIdx) != " ") && (wordStartIdx >= 0))
+      --wordStartIdx;
+   ++wordStartIdx;
+   while ((gEditLines[pEditLinesIndex].text.charAt(wordEndIdx) != " ") && (wordEndIdx < gEditLines[pEditLinesIndex].text.length))
+      ++wordEndIdx;
+   --wordEndIdx;
+
+   retObj.foundWord = true;
+   retObj.startIdx = wordStartIdx;
+   retObj.endIndex = wordEndIdx;
+   retObj.word = gEditLines[pEditLinesIndex].text.substring(wordStartIdx, wordEndIdx+1);
+   return retObj;
+}
+
+// Performs text replacement (AKA macro replacement) in an edit line.
+//
+// Parameters:
+//  pTxtReplacements: An associative array of text to be replaced (i.e.,
+//                    gTxtReplacements)
+//  pEditLinesIndex: The index of the line in gEditLines
+//  pCharIndex: The current character index in the text line
+//  pUseRegex: Whether or not to treat the text replacement search string as a
+//             regular expression.
+//
+// Return value: An object containing the following properties:
+//               textLineIndex: The updated text line index (integer)
+//               wordLenDiff: The change in length of the word that
+//                            was replaced (integer)
+//               wordStartIdx: The index of the first character in the word.
+//                             Only valid if a word was found.  Otherwise, this
+//                             will be 0.
+//               newTextEndIdx: The index of the last character in the new
+//                              text.  Only valid if a word was replaced.
+//                              Otherwise, this will be 0.
+//               newTextLen: The length of the new text in the string.  Will be
+//                           the length of the existing word if the word wasn't
+//                           replaced or 0 if no word was found.
+//               madeTxtReplacement: Whether or not a text replacement was made
+//                                   (boolean)
+function doMacroTxtReplacementInEditLine(pTxtReplacements, pEditLinesIndex, pCharIndex, pUseRegex)
+{
+   var retObj = new Object();
+   retObj.textLineIndex = pCharIndex;
+   retObj.wordLenDiff = 0;
+   retObj.wordStartIdx = 0;
+   retObj.newTextEndIdx = 0;
+   retObj.newTextLen = 0;
+   retObj.madeTxtReplacement = false;
+
+   var wordObj = getWordFromEditLine(pEditLinesIndex, retObj.textLineIndex);
+   if (wordObj.foundWord)
+   {
+      retObj.wordStartIdx = wordObj.startIdx;
+      retObj.newTextLen = wordObj.word.length;
+
+      // See if the word starts with a capital letter; if so, we'll capitalize
+      // the replacement word.
+      //var firstCharUpper = (wordObj.word.charAt(0) == wordObj.word.charAt(0).toUpperCase());
+      var firstCharUpper = false;
+      var txtReplacement = "";
+      if (pUseRegex)
+      {
+         // Since a regular expression might have more characters in addition
+         // to the actual word, we need to go through all the replacement strings
+         // in pTxtReplacements and use the first one that changes the text.
+         for (var prop in pTxtReplacements)
+         {
+            if (pTxtReplacements.hasOwnProperty(prop))
+            {
+               var regex = new RegExp(prop);
+               txtReplacement = wordObj.word.replace(regex, pTxtReplacements[prop]);
+               retObj.madeTxtReplacement = (txtReplacement != wordObj.word);
+               // If a text replacement was made, then check and see if the first
+               // letter in the original text was uppercase, and if so, make the
+               // first letter in the new text (txtReplacement) uppercase.
+               if (retObj.madeTxtReplacement)
+               {
+                  if (firstLetterIsUppercase(wordObj.word))
+                  {
+                     var letterInfo = getFirstLetterFromStr(txtReplacement);
+                     if (letterInfo.idx > -1)
+                     {
+                        txtReplacement = txtReplacement.substr(0, letterInfo.idx)
+                                       + letterInfo.letter.toUpperCase()
+                                       + txtReplacement.substr(letterInfo.idx+1);
+                     }
+                  }
+                  // Now that we've made a text replacement, stop going through
+                  // pTxtReplacements looking for a matching regex.
+                  break;
+               }
+            }
+         }
+      }
+      else
+      {
+         // Not using a regular expression.
+         firstCharUpper = (wordObj.word.charAt(0) == wordObj.word.charAt(0).toUpperCase());
+         // Convert the word to all uppercase to do the case-insensitive lookup
+         // in pTxtReplacements.
+         wordObj.word = wordObj.word.toUpperCase();
+         if (pTxtReplacements.hasOwnProperty(wordObj.word))
+         {
+            txtReplacement = pTxtReplacements[wordObj.word].toLowerCase();
+            retObj.madeTxtReplacement = true;
+         }
+      }
+      if (retObj.madeTxtReplacement)
+      {
+         if (firstCharUpper)
+            txtReplacement = txtReplacement.charAt(0).toUpperCase() + txtReplacement.substr(1);
+         gEditLines[pEditLinesIndex].text = gEditLines[pEditLinesIndex].text.substr(0, wordObj.startIdx)
+                                          + txtReplacement
+                                          + gEditLines[pEditLinesIndex].text.substr(wordObj.endIndex+1);
+         // Based on the difference in word length, update the data that
+         // matters (retObj.textLineIndex, which keeps track of the index of the current line).
+         // Note: The horizontal cursor position variable should be replaced after calling this
+         // function.
+         retObj.wordLenDiff = txtReplacement.length - wordObj.word.length;
+         retObj.textLineIndex += retObj.wordLenDiff;
+         retObj.newTextEndIdx = wordObj.endIndex + retObj.wordLenDiff;
+         retObj.newTextLen = txtReplacement.length;
+      }
+   }
+
+   return retObj;
+}
+
+// For configuration files, this function returns a fully-pathed filename.
+// This function first checks to see if the file exists in the sbbs/mods
+// directory, then the sbbs/ctrl directory, and if the file is not found there,
+// this function defaults to the given default path.
+//
+// Parameters:
+//  pFilename: The name of the file to look for
+//  pDefaultPath: The default directory (must have a trailing separator character)
+function genFullPathCfgFilename(pFilename, pDefaultPath)
+{
+   var fullyPathedFilename = system.mods_dir + pFilename;
+   if (!file_exists(fullyPathedFilename))
+      fullyPathedFilename = system.ctrl_dir + pFilename;
+   if (!file_exists(fullyPathedFilename))
+   {
+      if (typeof(pDefaultPath) == "string")
+      {
+         // Make sure the default path has a trailing path separator
+         var defaultPath = backslash(pDefaultPath);
+         fullyPathedFilename = defaultPath + pFilename;
+      }
+      else
+         fullyPathedFilename = pFilename;
+   }
+   return fullyPathedFilename;
+}
+
+// Returns the first letter found in a string and its index.  If a letter is
+// not found, the string returned will be blank, and the index will be -1.
+//
+// Parameters:
+//  pString: The string to search
+//
+// Return value: An object with the following properties:
+//               letter: The first letter found in the string, or a blank string if none was found
+//               idx: The index of the first letter found, or -1 if none was found
+function getFirstLetterFromStr(pString)
+{
+   var retObj = new Object;
+   retObj.letter = "";
+   retObj.idx = -1;
+
+   var theChar = "";
+   for (var i = 0; (i < pString.length) && (retObj.idx == -1); ++i)
+   {
+      theChar = pString.charAt(i);
+      if (charIsLetter(theChar))
+      {
+         retObj.idx = i;
+         retObj.letter = theChar;
+      }
+   }
+
+   return retObj;
+}
+
+// Returns whether or not the first letter in a string is uppercase.  If the
+// string doesn't contain any letters, then this function will return false.
+//
+// Parameters:
+//  pString: The string to search
+//
+// Return value: Boolean - Whether or not the first letter in the string is uppercase
+function firstLetterIsUppercase(pString)
+{
+   var firstIsUpper = false;
+   var letterObj = getFirstLetterFromStr(pString);
+   if (letterObj.idx > -1)
+   {
+      var theLetter = pString.charAt(letterObj.idx);
+      firstIsUpper = (theLetter == theLetter.toUpperCase());
+   }
+   return firstIsUpper;
+}
+
 // This function displays debug text at a given location on the screen, then
 // moves the cursor back to a given location.
 //