diff --git a/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php b/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php
index 9b988c7e8f..48a2438c1b 100644
--- a/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php
+++ b/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php
@@ -1,131 +1,131 @@
 <?php
 
 final class DivinerRemarkupRuleSymbol extends PhutilRemarkupRule {
 
   const KEY_RULE_ATOM_REF = 'rule.diviner.atomref';
 
   public function apply($text) {
     // Grammar here is:
     //
     //         rule = '@{' maybe_type name maybe_title '}'
     //   maybe_type = null | type ':' | type '@' book ':'
     //         name = name | name '@' context
     //  maybe_title = null | '|' title
     //
     // So these are all valid:
     //
     //   @{name}
     //   @{type : name}
     //   @{name | title}
     //   @{type @ book : name @ context | title}
 
-    return $this->replaceHTML(
+    return preg_replace_callback(
       '/(?:^|\B)@{'.
         '(?:(?P<type>[^:]+?):)?'.
         '(?P<name>[^}|]+?)'.
         '(?:[|](?P<title>[^}]+))?'.
       '}/',
       array($this, 'markupSymbol'),
       $text);
   }
 
   public function markupSymbol($matches) {
     $type = (string)idx($matches, 'type');
     $name = (string)$matches['name'];
     $title = (string)idx($matches, 'title');
 
     // Collapse sequences of whitespace into a single space.
     $type = preg_replace('/\s+/', ' ', trim($type));
     $name = preg_replace('/\s+/', ' ', trim($name));
     $title = preg_replace('/\s+/', ' ', trim($title));
 
     $ref = array();
 
     if (strpos($type, '@') !== false) {
       list($type, $book) = explode('@', $type, 2);
       $ref['type'] = trim($type);
       $ref['book'] = trim($book);
     } else {
       $ref['type'] = $type;
     }
 
     if (strpos($name, '@') !== false) {
       list($name, $context) = explode('@', $name, 2);
       $ref['name'] = trim($name);
       $ref['context'] = trim($context);
     } else {
       $ref['name'] = $name;
     }
 
     $ref['title'] = $title;
 
     foreach ($ref as $key => $value) {
       if ($value === '') {
         unset($ref[$key]);
       }
     }
 
     $engine = $this->getEngine();
     $token = $engine->storeText('');
 
     $key = self::KEY_RULE_ATOM_REF;
     $data = $engine->getTextMetadata($key, array());
     $data[$token] = $ref;
     $engine->setTextMetadata($key, $data);
 
     return $token;
   }
 
   public function didMarkupText() {
     $engine = $this->getEngine();
 
     $key = self::KEY_RULE_ATOM_REF;
     $data = $engine->getTextMetadata($key, array());
 
     $renderer = $engine->getConfig('diviner.renderer');
 
     foreach ($data as $token => $ref_dict) {
       $ref = DivinerAtomRef::newFromDictionary($ref_dict);
       $title = nonempty($ref->getTitle(), $ref->getName());
 
       $href = null;
       if ($renderer) {
         // Here, we're generating documentation. If possible, we want to find
         // the real atom ref so we can render the correct default title and
         // render invalid links in an alternate style.
 
         $ref = $renderer->normalizeAtomRef($ref);
         if ($ref) {
           $title = nonempty($ref->getTitle(), $ref->getName());
           $href = $renderer->getHrefForAtomRef($ref);
         }
       } else {
         // Here, we're generating commment text or something like that. Just
         // link to Diviner and let it sort things out.
 
         $href = id(new PhutilURI('/diviner/find/'))
           ->setQueryParams($ref_dict + array('jump' => true));
       }
 
       if ($href) {
         $link = phutil_tag(
           'a',
           array(
             'class' => 'atom-ref',
             'href' => $href,
           ),
           $title);
       } else {
         $link = phutil_tag(
           'span',
           array(
             'class' => 'atom-ref-invalid',
           ),
           $title);
       }
 
       $engine->overwriteStoredText($token, $link);
     }
   }
 
 }
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php
index 3356aed8e2..0fbe87ebfc 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php
@@ -1,71 +1,71 @@
 <?php
 
 /**
  * @group markup
  */
 final class PhabricatorRemarkupRuleCountdown extends PhutilRemarkupRule {
 
   const KEY_RULE_COUNTDOWN = 'rule.countdown';
 
   public function apply($text) {
-    return $this->replaceHTML(
+    return preg_replace_callback(
       "@\B{C(\d+)}\B@",
       array($this, 'markupCountdown'),
       $text);
   }
 
   protected function markupCountdown($matches) {
     $countdown = id(new PhabricatorTimer())->load($matches[1]);
     if (!$countdown) {
       return $matches[0];
     }
     $id = celerity_generate_unique_node_id();
 
     $engine = $this->getEngine();
     $token = $engine->storeText('');
 
     $metadata_key = self::KEY_RULE_COUNTDOWN;
     $metadata = $engine->getTextMetadata($metadata_key, array());
     $metadata[$id] = array($countdown->getDatepoint(), $token);
     $engine->setTextMetadata($metadata_key, $metadata);
 
     return $token;
   }
 
   public function didMarkupText() {
     $engine = $this->getEngine();
 
     $metadata_key = self::KEY_RULE_COUNTDOWN;
     $metadata = $engine->getTextMetadata($metadata_key, array());
 
     if (!$metadata) {
       return;
     }
 
     require_celerity_resource('javelin-behavior-countdown-timer');
 
     foreach ($metadata as $id => $info) {
       list($time, $token) = $info;
       $prefix = 'phabricator-timer-';
       $count = phutil_tag(
         'span',
         array(
           'id' => $id,
         ),
         array(
           javelin_tag('span', array('sigil' => $prefix.'days'), ''), 'd',
           javelin_tag('span', array('sigil' => $prefix.'hours'), ''), 'h',
           javelin_tag('span', array('sigil' => $prefix.'minutes'), ''), 'm',
           javelin_tag('span', array('sigil' => $prefix.'seconds'), ''), 's',
         ));
       Javelin::initBehavior('countdown-timer', array(
         'timestamp' => $time,
         'container' => $id,
       ));
       $engine->overwriteStoredText($token, $count);
     }
 
     $engine->setTextMetadata($metadata_key, array());
   }
 
 }
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php
index e7c25dd1fe..9ef0ac8ad9 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php
@@ -1,187 +1,187 @@
 <?php
 
 /**
  * @group markup
  */
 final class PhabricatorRemarkupRuleEmbedFile
   extends PhutilRemarkupRule {
 
   const KEY_RULE_EMBED_FILE = 'rule.embed.file';
   const KEY_EMBED_FILE_PHIDS = 'phabricator.embedded-file-phids';
 
   public function apply($text) {
-    return $this->replaceHTML(
+    return preg_replace_callback(
       "@{F(\d+)([^}]+?)?}@",
       array($this, 'markupEmbedFile'),
       $text);
   }
 
   public function markupEmbedFile($matches) {
 
     $file = null;
     if ($matches[1]) {
       // TODO: This is pretty inefficient if there are a bunch of files.
       $file = id(new PhabricatorFile())->load($matches[1]);
     }
 
     if (!$file) {
       return $matches[0];
     }
     $phid = $file->getPHID();
 
     $engine = $this->getEngine();
     $token = $engine->storeText('');
     $metadata_key = self::KEY_RULE_EMBED_FILE;
     $metadata = $engine->getTextMetadata($metadata_key, array());
     $bundle = array('token' => $token);
 
     $options = array(
       'size'    => 'thumb',
       'layout'  => 'left',
       'float'   => false,
       'name'    => null,
     );
 
     if (!empty($matches[2])) {
       $matches[2] = trim($matches[2], ', ');
       $parser = new PhutilSimpleOptions();
       $options = $parser->parse($matches[2]) + $options;
     }
     $file_name = coalesce($options['name'], $file->getName());
     $options['name'] = $file_name;
 
     $is_viewable_image = $file->isViewableImage();
 
     $attrs = array();
     if ($is_viewable_image) {
       switch ((string)$options['size']) {
         case 'full':
           $attrs['src'] = $file->getBestURI();
           $options['image_class'] = null;
           $file_data = $file->getMetadata();
           $height = idx($file_data, PhabricatorFile::METADATA_IMAGE_HEIGHT);
           if ($height) {
             $attrs['height'] = $height;
           }
           $width = idx($file_data, PhabricatorFile::METADATA_IMAGE_WIDTH);
           if ($width) {
             $attrs['width'] = $width;
           }
           break;
         case 'thumb':
         default:
           $attrs['src'] = $file->getPreview220URI();
           $dimensions =
             PhabricatorImageTransformer::getPreviewDimensions($file, 220);
           $attrs['width'] = $dimensions['sdx'];
           $attrs['height'] = $dimensions['sdy'];
           $options['image_class'] = 'phabricator-remarkup-embed-image';
           break;
       }
     }
     $bundle['attrs'] = $attrs;
     $bundle['options'] = $options;
 
     $bundle['meta'] = array(
       'phid'     => $file->getPHID(),
       'viewable' => $is_viewable_image,
       'uri'      => $file->getBestURI(),
       'dUri'     => $file->getDownloadURI(),
       'name'     => $options['name'],
     );
     $metadata[$phid][] = $bundle;
     $engine->setTextMetadata($metadata_key, $metadata);
 
     return $token;
   }
 
   public function didMarkupText() {
     $engine = $this->getEngine();
 
     $metadata_key = self::KEY_RULE_EMBED_FILE;
     $metadata = $engine->getTextMetadata($metadata_key, array());
 
     if (!$metadata) {
       return;
     }
 
     $file_phids = array();
     foreach ($metadata as $phid => $bundles) {
       foreach ($bundles as $data) {
 
         $options = $data['options'];
         $meta    = $data['meta'];
 
         if (!$meta['viewable'] || $options['layout'] == 'link') {
           $link = id(new PhabricatorFileLinkView())
             ->setFilePHID($meta['phid'])
             ->setFileName($meta['name'])
             ->setFileDownloadURI($meta['dUri'])
             ->setFileViewURI($meta['uri'])
             ->setFileViewable($meta['viewable']);
           $embed = $link->render();
           $engine->overwriteStoredText($data['token'], $embed);
           continue;
         }
 
         require_celerity_resource('lightbox-attachment-css');
         $img = phutil_tag('img', $data['attrs']);
 
         $embed = javelin_tag(
           'a',
           array(
             'href'        => $meta['uri'],
             'class'       => $options['image_class'],
             'sigil'       => 'lightboxable',
             'mustcapture' => true,
             'meta'        => $meta,
           ),
           $img);
 
         $layout_class = null;
         switch ($options['layout']) {
           case 'right':
           case 'center':
           case 'inline':
           case 'left':
             $layout_class = 'phabricator-remarkup-embed-layout-'.
               $options['layout'];
             break;
           default:
             $layout_class = 'phabricator-remarkup-embed-layout-left';
             break;
         }
 
         if ($options['float']) {
           switch ($options['layout']) {
             case 'center':
             case 'inline':
               break;
             case 'right':
               $layout_class .= ' phabricator-remarkup-embed-float-right';
               break;
             case 'left':
             default:
               $layout_class .= ' phabricator-remarkup-embed-float-left';
               break;
           }
         }
 
         if ($layout_class) {
           $embed = phutil_tag(
             'div',
             array(
               'class' => $layout_class,
             ),
             $embed);
         }
 
         $engine->overwriteStoredText($data['token'], $embed);
       }
       $file_phids[] = $phid;
     }
     $engine->setTextMetadata(self::KEY_EMBED_FILE_PHIDS, $file_phids);
     $engine->setTextMetadata($metadata_key, array());
   }
 
 }
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php
index 2bf0d89fcf..7ad83ebc58 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php
@@ -1,63 +1,63 @@
 <?php
 
 /**
  * @group markup
  */
 final class PhabricatorRemarkupRuleImageMacro
   extends PhutilRemarkupRule {
 
   private $images;
 
   public function apply($text) {
-    return $this->replaceHTML(
+    return preg_replace_callback(
       '@^([a-zA-Z0-9:_\-]+)$@m',
       array($this, 'markupImageMacro'),
       $text);
   }
 
   public function markupImageMacro($matches) {
     if ($this->images === null) {
       $this->images = array();
       $rows = id(new PhabricatorFileImageMacro())->loadAllWhere(
         'isDisabled = 0');
       foreach ($rows as $row) {
         $this->images[$row->getName()] = $row->getFilePHID();
       }
     }
 
     $name = (string)$matches[1];
 
     if (array_key_exists($name, $this->images)) {
       $phid = $this->images[$name];
 
       $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $phid);
       $style = null;
       $src_uri = null;
       if ($file) {
         $src_uri = $file->getBestURI();
         $file_data = $file->getMetadata();
         $height = idx($file_data, PhabricatorFile::METADATA_IMAGE_HEIGHT);
         $width = idx($file_data, PhabricatorFile::METADATA_IMAGE_WIDTH);
         if ($height && $width) {
           $style = sprintf(
             'height: %dpx; width: %dpx;',
             $height,
             $width);
         }
       }
 
       $img = phutil_tag(
         'img',
         array(
           'src'   => $src_uri,
           'alt'   => $matches[1],
           'title' => $matches[1],
           'style' => $style,
         ));
       return $this->getEngine()->storeText($img);
     } else {
       return $matches[1];
     }
   }
 
 }
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php
index b569eb60ac..1254d044ad 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMeme.php
@@ -1,42 +1,42 @@
 <?php
 
 /**
  * @group markup
  */
 final class PhabricatorRemarkupRuleMeme
   extends PhutilRemarkupRule {
 
   private $images;
 
   public function apply($text) {
-    return $this->replaceHTML(
+    return preg_replace_callback(
       '@{meme,([^}]+)}$@m',
       array($this, 'markupMeme'),
       $text);
   }
 
   public function markupMeme($matches) {
     $options = array(
       'src' => null,
       'above' => null,
       'below' => null,
     );
 
     $parser = new PhutilSimpleOptions();
     $options = $parser->parse($matches[1]) + $options;
 
     $uri = id(new PhutilURI('/macro/meme/'))
       ->alter('macro', $options['src'])
       ->alter('uppertext', $options['above'])
       ->alter('lowertext', $options['below']);
 
     $img = phutil_tag(
       'img',
       array(
         'src' => (string)$uri,
       ));
 
     return $this->getEngine()->storeText($img);
   }
 
 }
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php
index b2a98fc9af..7f05ff95a1 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php
@@ -1,130 +1,130 @@
 <?php
 
 /**
  * @group markup
  */
 final class PhabricatorRemarkupRuleMention
   extends PhutilRemarkupRule {
 
   const KEY_RULE_MENTION          = 'rule.mention';
   const KEY_RULE_MENTION_ORIGINAL = 'rule.mention.original';
 
   const KEY_MENTIONED = 'phabricator.mentioned-user-phids';
 
 
   // NOTE: The negative lookbehind prevents matches like "mail@lists", while
   // allowing constructs like "@tomo/@mroch". Since we now allow periods in
   // usernames, we can't resonably distinguish that "@company.com" isn't a
   // username, so we'll incorrectly pick it up, but there's little to be done
   // about that. We forbid terminal periods so that we can correctly capture
   // "@joe" instead of "@joe." in "Hey, @joe.".
   const REGEX = '/(?<!\w)@([a-zA-Z0-9._-]*[a-zA-Z0-9_-])/';
 
   public function apply($text) {
-    return $this->replaceHTML(
+    return preg_replace_callback(
       self::REGEX,
       array($this, 'markupMention'),
       $text);
   }
 
   protected function markupMention($matches) {
     $engine = $this->getEngine();
     $token = $engine->storeText('');
 
     // Store the original text exactly so we can preserve casing if it doesn't
     // resolve into a username.
     $original_key = self::KEY_RULE_MENTION_ORIGINAL;
     $original = $engine->getTextMetadata($original_key, array());
     $original[$token] = $matches[1];
     $engine->setTextMetadata($original_key, $original);
 
     $metadata_key = self::KEY_RULE_MENTION;
     $metadata = $engine->getTextMetadata($metadata_key, array());
     $username = strtolower($matches[1]);
     if (empty($metadata[$username])) {
       $metadata[$username] = array();
     }
     $metadata[$username][] = $token;
     $engine->setTextMetadata($metadata_key, $metadata);
 
     return $token;
   }
 
   public function didMarkupText() {
     $engine = $this->getEngine();
 
     $metadata_key = self::KEY_RULE_MENTION;
     $metadata = $engine->getTextMetadata($metadata_key, array());
     if (empty($metadata)) {
       // No mentions, or we already processed them.
       return;
     }
 
     $original_key = self::KEY_RULE_MENTION_ORIGINAL;
     $original = $engine->getTextMetadata($original_key, array());
 
     $usernames = array_keys($metadata);
     $user_table = new PhabricatorUser();
     $real_user_names = queryfx_all(
       $user_table->establishConnection('r'),
       'SELECT username, phid, realName, isDisabled
         FROM %T
         WHERE username IN (%Ls)',
       $user_table->getTableName(),
       $usernames);
 
     $actual_users = array();
 
     $mentioned_key = self::KEY_MENTIONED;
     $mentioned = $engine->getTextMetadata($mentioned_key, array());
     foreach ($real_user_names as $row) {
       $actual_users[strtolower($row['username'])] = $row;
       $mentioned[$row['phid']] = $row['phid'];
     }
 
     $engine->setTextMetadata($mentioned_key, $mentioned);
 
     foreach ($metadata as $username => $tokens) {
       $exists = isset($actual_users[$username]);
       if (!$exists) {
         $class = 'phabricator-remarkup-mention-unknown';
       } else if ($actual_users[$username]['isDisabled']) {
         $class = 'phabricator-remarkup-mention-disabled';
       } else {
         $class = 'phabricator-remarkup-mention-exists';
       }
 
       if ($exists) {
         $tag = phutil_tag(
           'a',
           array(
             'class'   => $class,
             'href'    => '/p/'.$actual_users[$username]['username'].'/',
             'target'  => '_blank',
             'title'   => $actual_users[$username]['realName'],
           ),
           '@'.$actual_users[$username]['username']);
         foreach ($tokens as $token) {
           $engine->overwriteStoredText($token, $tag);
         }
       } else {
         // NOTE: The structure here is different from the 'exists' branch,
         // because we want to preserve the original text capitalization and it
         // may differ for each token.
         foreach ($tokens as $token) {
           $tag = phutil_tag(
             'span',
             array(
               'class' => $class,
             ),
             '@'.idx($original, $token, $username));
           $engine->overwriteStoredText($token, $tag);
         }
       }
     }
 
     // Don't re-process these mentions.
     $engine->setTextMetadata($metadata_key, array());
   }
 
 }
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php
index 8aa082b621..c47681a7f7 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php
@@ -1,66 +1,66 @@
 <?php
 
 /**
  * @group markup
  */
 abstract class PhabricatorRemarkupRuleObjectHandle
   extends PhutilRemarkupRule {
 
   const KEY_RULE_HANDLE = 'rule.handle';
 
   abstract protected function getObjectNamePrefix();
   abstract protected function loadObjectPHID($id);
 
   public function apply($text) {
     $prefix = $this->getObjectNamePrefix();
-    return $this->replaceHTML(
+    return preg_replace_callback(
       "@\B{{$prefix}(\d+)}\B@",
       array($this, 'markupObjectHandle'),
       $text);
   }
 
   protected function markupObjectHandle($matches) {
     // TODO: These are single gets but should be okay for now, they're behind
     // the cache.
     $phid = $this->loadObjectPHID($matches[1]);
     if (!$phid) {
       return $matches[0];
     }
 
     $engine = $this->getEngine();
     $token = $engine->storeText('');
 
     $metadata_key = self::KEY_RULE_HANDLE;
     $metadata = $engine->getTextMetadata($metadata_key, array());
     if (empty($metadata[$phid])) {
       $metadata[$phid] = array();
     }
     $metadata[$phid][] = $token;
     $engine->setTextMetadata($metadata_key, $metadata);
 
     return $token;
   }
 
   public function didMarkupText() {
     $engine = $this->getEngine();
 
     $metadata_key = self::KEY_RULE_HANDLE;
     $metadata = $engine->getTextMetadata($metadata_key, array());
     if (empty($metadata)) {
       return;
     }
 
     $handles = id(new PhabricatorObjectHandleData(array_keys($metadata)))
       ->loadHandles();
 
     foreach ($metadata as $phid => $tokens) {
       $link = $handles[$phid]->renderLink();
       foreach ($tokens as $token) {
         $engine->overwriteStoredText($token, $link);
       }
     }
 
     $engine->setTextMetadata($metadata_key, array());
   }
 
 }
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php
index 859bebdc74..b81c37cc2f 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php
@@ -1,51 +1,51 @@
 <?php
 
 /**
  * @group markup
  */
 abstract class PhabricatorRemarkupRuleObjectName
   extends PhutilRemarkupRule {
 
   abstract protected function getObjectNamePrefix();
 
   protected function getObjectIDPattern() {
     return '[1-9]\d*';
   }
 
   public function apply($text) {
     $prefix = $this->getObjectNamePrefix();
     $id = $this->getObjectIDPattern();
-    return $this->replaceHTML(
+    return preg_replace_callback(
       "@\b({$prefix})({$id})(?:#([-\w\d]+))?\b@",
       array($this, 'markupObjectNameLink'),
       $text);
   }
 
   public function markupObjectNameLink($matches) {
     list(, $prefix, $id) = $matches;
 
     if (isset($matches[3])) {
       $href = $matches[3];
       $text = $matches[3];
       if (preg_match('@^(?:comment-)?(\d{1,7})$@', $href, $matches)) {
         // Maximum length is 7 because 12345678 could be a file hash.
         $href = "comment-{$matches[1]}";
         $text = $matches[1];
       }
       $href = "/{$prefix}{$id}#{$href}";
       $text = "{$prefix}{$id}#{$text}";
     } else {
       $href = "/{$prefix}{$id}";
       $text = "{$prefix}{$id}";
     }
 
     return $this->getEngine()->storeText(
       phutil_tag(
         'a',
         array(
           'href' => $href,
         ),
         $text));
   }
 
 }
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php
index 56a84d0d75..e42c680a63 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php
@@ -1,45 +1,45 @@
 <?php
 
 /**
  * @group markup
  */
 final class PhabricatorRemarkupRulePhriction
   extends PhutilRemarkupRule {
 
   public function apply($text) {
-    return $this->replaceHTML(
+    return preg_replace_callback(
       '@\B\\[\\[([^|\\]]+)(?:\\|([^\\]]+))?\\]\\]\B@U',
       array($this, 'markupDocumentLink'),
       $text);
   }
 
   public function markupDocumentLink($matches) {
 
     $link = trim($matches[1]);
     $name = trim(idx($matches, 2, $link));
     $name = explode('/', trim($name, '/'));
     $name = end($name);
 
     $uri      = new PhutilURI($link);
     $slug     = $uri->getPath();
     $fragment = $uri->getFragment();
     $slug     = PhabricatorSlug::normalize($slug);
     $slug     = PhrictionDocument::getSlugURI($slug);
     $href     = (string) id(new PhutilURI($slug))->setFragment($fragment);
 
     if ($this->getEngine()->getState('toc')) {
       $text = $name;
     } else {
       $text = phutil_tag(
           'a',
           array(
             'href'  => $href,
             'class' => 'phriction-link',
           ),
           $name);
     }
 
     return $this->getEngine()->storeText($text);
   }
 
 }