Index: /trunk/lib/Prefs.inc.php
===================================================================
--- /trunk/lib/Prefs.inc.php	(revision 652)
+++ /trunk/lib/Prefs.inc.php	(revision 653)
@@ -518,5 +518,5 @@
         // User_id must not be empty.
         if ('' == $this->getParam('user_id')) {
-            $app->logMsg(sprintf('Cannot save prefs because user_id not set.', null), LOG_WARNING, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Cannot load prefs because user_id not set.', null), LOG_WARNING, __FILE__, __LINE__);
             return false;
         }
@@ -591,5 +591,5 @@
         // Skip this method if not using the db.
         if ('database' != $this->getParam('storagetype')) {
-            $app->logMsg('Prefs->save() does nothing unless using a database storagetype.', LOG_NOTICE, __FILE__, __LINE__);
+            $app->logMsg('Prefs->save() does nothing unless using a database storagetype.', LOG_DEBUG, __FILE__, __LINE__);
             return true;
         }
@@ -597,5 +597,5 @@
         // User_id must not be empty.
         if ('' == $this->getParam('user_id')) {
-            $app->logMsg(sprintf('Cannot save prefs because user_id not set.', null), LOG_WARNING, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Cannot save prefs because user_id not set.', null), LOG_DEBUG, __FILE__, __LINE__);
             return false;
         }
Index: /trunk/lib/Utilities.inc.php
===================================================================
--- /trunk/lib/Utilities.inc.php	(revision 652)
+++ /trunk/lib/Utilities.inc.php	(revision 653)
@@ -221,30 +221,40 @@
  * @return string         Cleaned text.
  */
-function fancyTxt($text)
+function fancyTxt($text, $extra_search=null, $extra_replace=null)
 {
     $search = array();
     $replace = array();
 
-    // "double quoted text"  becomes  &ldquo;double quoted text&rdquo;
-    $search['double_quotes']    = '/(^|[^\w=])(?:"|&quot;|&#34;|&#x22;|&ldquo;)([^"]+?)(?:"|&quot;|&#34;|&#x22;|&rdquo;)([^\w]|$)/ms'; // " is the same as &quot; and &#34; and &#x22;
-    $replace['double_quotes']   = '$1&ldquo;$2&rdquo;$3';
-
-    // text's apostrophes  become  text&rsquo;s apostrophes
-    $search['apostrophe']       = '/(\w)(?:\'|&#39;|&#039;)(\w)/ms';
-    $replace['apostrophe']      = '$1&rsquo;$2';
-
-    // 'single quoted text'  becomes  &lsquo;single quoted text&rsquo;
-    $search['single_quotes']    = '/(^|[^\w=])(?:\'|&#39;|&lsquo;)([^\']+?)(?:\'|&#39;|&rsquo;)([^\w]|$)/ms';
-    $replace['single_quotes']   = '$1&lsquo;$2&rsquo;$3';
-
-    // plural posessives' apostrophes become posessives&rsquo;
-    $search['apostrophes']      = '/(s)(?:\'|&#39;|&#039;)(\s)/ms';
-    $replace['apostrophes']     = '$1&rsquo;$2';
-
-    // em--dashes  become em&mdash;dashes
-    $search['em_dash']          = '/(\s*[^!<-])--([^>-]\s*)/';
-    $replace['em_dash']         = '$1&mdash;$2';
-
-    return preg_replace($search, $replace, $text);
+    // "double quoted text"  â  âdouble quoted textâ
+    $search['_double_quotes']    = '/(?<=^|[^\w=(])(?:"|&quot;|&#0?34;|&#x22;|&ldquo;)([\w\'.âŠ(ââ-][^"]*?)(?:"|&quot;|&#0?34;|&#x22;|&rdquo;)(?=[^)\w]|$)/imsu'; // " is the same as &quot; and &#34; and &#034; and &#x22;
+    $replace['_double_quotes']   = 'â$1â';
+
+    // text's apostrophes  â  textâs apostrophes (except foot marks: 6'3")
+    $search['_apostrophe']       = '/(?<=[a-z])(?:\'|&#0?39;)(?=\w)/imsu';
+    $replace['_apostrophe']      = 'â';
+
+    // 'single quoted text'  â  âsingle quoted textâ
+    $search['_single_quotes']    = '/(?<=^|[^\w=(])(?:\'|&#0?39;|&lsquo;)([\w"][^\']+?)(?:\'|&#0?39;|&rsquo;)(?=[^)\w]|$)/imsu';
+    $replace['_single_quotes']   = 'â$1â';
+
+    // plural posessives' apostrophes  â  posessivesâ  (except foot marks: 6')
+    $search['_apostrophes']      = '/(?<=s)(?:\'|&#0?39;|&rsquo;)(?=\s)/imsu';
+    $replace['_apostrophes']     = 'â';
+
+    // double--hyphens  â  en â dashes
+    $search['_em_dash']          = '/(?<=[\w\s"\'ââ)])--(?=[\w\sâââ"\'(?])/imsu';
+    $replace['_em_dash']         = ' â ';
+
+    // ...  â  âŠ
+    $search['_elipsis']          = '/(?<=^|[^.])\.\.\.(?=[^.]|$)/imsu';
+    $replace['_elipsis']         = 'âŠ';
+
+    if (is_array($extra_search) && is_array($extra_replace) && sizeof($extra_search) == sizeof($extra_replace)) {
+        // Append additional search replacements.
+        $search = array_merge($search, $extra_search);
+        $replace = array_merge($replace, $extra_replace);
+    }
+
+    return trim(preg_replace($search, $replace, $text));
 }
 
Index: /trunk/tests/EmailTest.php
===================================================================
--- /trunk/tests/EmailTest.php	(revision 652)
+++ /trunk/tests/EmailTest.php	(revision 653)
@@ -99,5 +99,5 @@
         $this->Email->setString('This is a {TEST}');
         $this->Email->replace(array(
-            'test' => 'â you guessed it â a test!'
+            'test' => 'â you guessed it â a test! Sent from ' . __FILE__
         ));
         $result   = $this->Email->send();
Index: /trunk/tests/UtilitiesTest.php
===================================================================
--- /trunk/tests/UtilitiesTest.php	(revision 652)
+++ /trunk/tests/UtilitiesTest.php	(revision 653)
@@ -35,5 +35,5 @@
     function setUp()
     {
-        define('_CLI', true);
+        // define('_CLI', true);
         require dirname(__FILE__) . '/_config.inc.php';
         require_once '../lib/Utilities.inc.php';
@@ -255,3 +255,31 @@
     }
 
+    function test_fancyTxt()
+    {
+        $strings = [
+            'Due to the <a href="https://en.wikipedia.org/wiki/Camp_Fire_(2018)">fires in "Paradise," CA</a> it\'s a bad day.' => 'â¢ Due to the <a href="https://en.wikipedia.org/wiki/Camp_Fire_(2018)">fires in âParadise,â CA</a> itâs a bad day.',
+            'Simply--"\'check back\' later"--in 6\'3"...' => 'â¢ Simply â ââcheck backâ laterâ â in 6\'3"âŠ',
+            '"Double "double \'single\'""--?' => 'â¢ "Double âdouble âsingleââ" â ?',
+            '<a href="javascript:openPopup(\'/foo/bar.php\')">Click here</a>....' => 'â¢ <a href="javascript:openPopup(\'/foo/bar.php\')">Click here</a>....',
+            'If you have any questions please <a href="/contact/">contact the ants\' mailboxes</a>.' => 'â¢ If you have any questions please <a href="/contact/">contact the antsâ mailboxes</a>.',
+            'This URL: "http://äŸå­.å·ç­çºž/?x=y&1=2#asdf"' => 'â¢ This URL: âhttp://äŸå­.å·ç­çºž/?x=y&1=2#asdfâ',
+            // '\'...\' and "..."' => 'â¢ ââŠâ and ââŠâ', // FIXME: this one doesn't work.
+            '\'1...\' and "2..."' => 'â¢ â1âŠâ and â2âŠâ',
+            '\'One\'s self...\' and "Twosome"' => 'â¢ âOneâs selfâŠâ and âTwosomeâ',
+            '"\'Well,\' said I, \'the gentleman at No. 4.\'"' => 'â¢ ââWell,â said I, âthe gentleman at No. 4.ââ',
+            '"...an office in Leadenhall Street--and--"
+"O\'Reilly\'s office?"
+"What!" he roared.
+"Yes, to-day."* She stood smiling.'
+=>
+'â¢ ââŠan office in Leadenhall Street â and â â
+âOâReillyâs office?â
+âWhat!â he roared.
+âYes, to-day.â* She stood smiling.',
+        ];
+        foreach ($strings as $input => $expected) {
+            $result = fancyTxt($input, ['/^/', '/^Â·/'], ['Â· ', 'â¢']);
+            $this->assertEquals($expected, $result, sprintf('Failed with input: %s', $input));
+        }
+    }
 }
Index: /trunk/tests/run_tests.sh
===================================================================
--- /trunk/tests/run_tests.sh	(revision 652)
+++ /trunk/tests/run_tests.sh	(revision 653)
@@ -7,4 +7,7 @@
 #  export DB_USER="test"
 #  export DB_PASS="..."
+
+# Run an individual test like this:
+# ./tests/run_tests.sh /Users/q/src/codebase/trunk/tests/UtilitiesTest.php
 
 function err {
@@ -38,4 +41,4 @@
 # Config options go in phpunit.xml
 # phpunit --tap | grep -v '^ok '
-../vendor/phpunit/phpunit/composer/bin/phpunit --stderr || err "\nSomething went wrong (code $?). If there is no output above, check the php_error_log";
+../vendor/phpunit/phpunit/composer/bin/phpunit --stderr "$@" || err "\nSomething went wrong (code $?). If there is no output above, check the php_error_log";
 
