diff --git a/src/applications/diviner/atom/DivinerAtom.php b/src/applications/diviner/atom/DivinerAtom.php
index 3aa56105f8..31a067fed5 100644
--- a/src/applications/diviner/atom/DivinerAtom.php
+++ b/src/applications/diviner/atom/DivinerAtom.php
@@ -1,325 +1,328 @@
 <?php
 
 final class DivinerAtom {
 
   const TYPE_FILE      = 'file';
   const TYPE_ARTICLE   = 'article';
 
   private $type;
   private $name;
   private $file;
   private $line;
   private $hash;
   private $contentRaw;
   private $length;
   private $language;
   private $docblockRaw;
   private $docblockText;
   private $docblockMeta;
   private $warnings = array();
   private $parentHash;
   private $childHashes = array();
   private $context;
   private $extends = array();
   private $links = array();
   private $book;
 
   /**
    * Returns a sorting key which imposes an unambiguous, stable order on atoms.
    */
   public function getSortKey() {
     return implode(
       "\0",
       array(
         $this->getBook(),
         $this->getType(),
         $this->getContext(),
         $this->getName(),
         $this->getFile(),
         sprintf('%08', $this->getLine()),
       ));
   }
 
   public function setBook($book) {
     $this->book = $book;
     return $this;
   }
 
   public function getBook() {
     return $this->book;
   }
 
   public function setContext($context) {
     $this->context = $context;
     return $this;
   }
 
   public function getContext() {
     return $this->context;
   }
 
   public static function getAtomSerializationVersion() {
     return 1;
   }
 
   public function addWarning($warning) {
     $this->warnings[] = $warning;
     return $this;
   }
 
   public function getWarnings() {
     return $this->warnings;
   }
 
   public function setDocblockRaw($docblock_raw) {
     $this->docblockRaw = $docblock_raw;
 
     $parser = new PhutilDocblockParser();
     list($text, $meta) = $parser->parse($docblock_raw);
     $this->docblockText = $text;
     $this->docblockMeta = $meta;
 
     return $this;
   }
 
   public function getDocblockRaw() {
     return $this->docblockRaw;
   }
 
   public function getDocblockText() {
     if ($this->docblockText === null) {
       throw new Exception("Call setDocblockRaw() before getDocblockText()!");
     }
     return $this->docblockText;
   }
 
   public function getDocblockMeta() {
     if ($this->docblockMeta === null) {
       throw new Exception("Call setDocblockRaw() before getDocblockMeta()!");
     }
     return $this->docblockMeta;
   }
 
   public function getDocblockMetaValue($key, $default = null) {
     $meta = $this->getDocblockMeta();
     return idx($meta, $key, $default);
   }
 
   public function setDocblockMetaValue($key, $value) {
     $meta = $this->getDocblockMeta();
     $meta[$key] = $value;
     $this->docblockMeta = $meta;
     return $this;
   }
 
   public function setType($type) {
     $this->type = $type;
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setFile($file) {
     $this->file = $file;
     return $this;
   }
 
   public function getFile() {
     return $this->file;
   }
 
   public function setLine($line) {
     $this->line = $line;
     return $this;
   }
 
   public function getLine() {
     return $this->line;
   }
 
   public function setContentRaw($content_raw) {
     $this->contentRaw = $content_raw;
     return $this;
   }
 
   public function getContentRaw() {
     return $this->contentRaw;
   }
 
   public function setHash($hash) {
     $this->hash = $hash;
     return $this;
   }
 
   public function addLink(DivinerAtomRef $ref) {
     $this->links[] = $ref;
     return $this;
   }
 
   public function addExtends(DivinerAtomRef $ref) {
     $this->extends[] = $ref;
     return $this;
   }
 
   public function getLinkDictionaries() {
     return mpull($this->links, 'toDictionary');
   }
 
   public function getExtendsDictionaries() {
     return mpull($this->extends, 'toDictionary');
   }
 
   public function getHash() {
     if ($this->hash) {
       return $this->hash;
     }
 
     $parts = array(
       $this->getType(),
       $this->getName(),
       $this->getFile(),
       $this->getLine(),
       $this->getLength(),
       $this->getLanguage(),
       $this->getContentRaw(),
       $this->getDocblockRaw(),
       mpull($this->extends, 'toHash'),
       mpull($this->links, 'toHash'),
     );
 
     return md5(serialize($parts)).'N';
   }
 
   public function setLength($length) {
     $this->length = $length;
     return $this;
   }
 
   public function getLength() {
     return $this->length;
   }
 
   public function setLanguage($language) {
     $this->language = $language;
     return $this;
   }
 
   public function getLanguage() {
     return $this->language;
   }
 
   public function addChildHash($child_hash) {
     $this->childHashes[] = $child_hash;
     return $this;
   }
 
   public function getChildHashes() {
     return $this->childHashes;
   }
 
   public function setParentHash($parent_hash) {
     if ($this->parentHash) {
       throw new Exception("Atom already has a parent!");
     }
     $this->parentHash = $parent_hash;
     return $this;
   }
 
   public function getParentHash() {
     return $this->parentHash;
   }
 
   public function addChild(DivinerAtom $atom) {
     $atom->setParentHash($this->getHash());
     $this->addChildHash($atom->getHash());
     return $this;
   }
 
   public function getURI() {
     $parts = array();
     $parts[] = phutil_escape_uri_path_component($this->getType());
     if ($this->getContext()) {
       $parts[] = phutil_escape_uri_path_component($this->getContext());
     }
     $parts[] = phutil_escape_uri_path_component($this->getName());
     $parts[] = null;
     return implode('/', $parts);
   }
 
 
   public function toDictionary() {
     // NOTE: If you change this format, bump the format version in
     // getAtomSerializationVersion().
 
     return array(
       'book'        => $this->getBook(),
       'type'        => $this->getType(),
       'name'        => $this->getName(),
       'file'        => $this->getFile(),
       'line'        => $this->getLine(),
       'hash'        => $this->getHash(),
       'uri'         => $this->getURI(),
       'length'      => $this->getLength(),
       'context'     => $this->getContext(),
       'language'    => $this->getLanguage(),
       'docblockRaw' => $this->getDocblockRaw(),
       'warnings'    => $this->getWarnings(),
       'parentHash'  => $this->getParentHash(),
       'childHashes' => $this->getChildHashes(),
       'extends'     => $this->getExtendsDictionaries(),
       'links'       => $this->getLinkDictionaries(),
       'ref'         => $this->getRef()->toDictionary(),
     );
   }
 
   public function getRef() {
     $group = null;
+    $title = null;
     if ($this->docblockMeta) {
       $group = $this->getDocblockMetaValue('group');
+      $title = $this->getDocblockMetaValue('title');
     }
 
     return id(new DivinerAtomRef())
       ->setBook($this->getBook())
       ->setContext($this->getContext())
       ->setType($this->getType())
       ->setName($this->getName())
+      ->setTitle($title)
       ->setGroup($group);
   }
 
   public static function newFromDictionary(array $dictionary) {
     $atom = id(new DivinerAtom())
       ->setBook(idx($dictionary, 'book'))
       ->setType(idx($dictionary, 'type'))
       ->setName(idx($dictionary, 'name'))
       ->setFile(idx($dictionary, 'file'))
       ->setLine(idx($dictionary, 'line'))
       ->setHash(idx($dictionary, 'hash'))
       ->setLength(idx($dictionary, 'length'))
       ->setContext(idx($dictionary, 'context'))
       ->setLanguage(idx($dictionary, 'language'))
       ->setParentHash(idx($dictionary, 'parentHash'))
       ->setDocblockRaw(idx($dictionary, 'docblockRaw'));
 
     foreach (idx($dictionary, 'warnings', array()) as $warning) {
       $atom->addWarning($warning);
     }
 
     foreach (idx($dictionary, 'childHashes', array()) as $child) {
       $atom->addChildHash($child);
     }
 
     return $atom;
   }
 
 }
diff --git a/src/applications/diviner/atom/DivinerAtomRef.php b/src/applications/diviner/atom/DivinerAtomRef.php
index b0eb3a87b0..f1bd344b75 100644
--- a/src/applications/diviner/atom/DivinerAtomRef.php
+++ b/src/applications/diviner/atom/DivinerAtomRef.php
@@ -1,179 +1,196 @@
 <?php
 
 final class DivinerAtomRef {
 
   private $book;
   private $context;
   private $type;
   private $name;
   private $group;
   private $summary;
   private $index;
+  private $title;
 
   public function getSortKey() {
     return implode(
       "\0",
       array(
         $this->getName(),
         $this->getType(),
         $this->getContext(),
         $this->getBook(),
         $this->getIndex(),
       ));
   }
 
   public function setIndex($index) {
     $this->index = $index;
     return $this;
   }
 
   public function getIndex() {
     return $this->index;
   }
 
   public function setSummary($summary) {
     $this->summary = $summary;
     return $this;
   }
 
   public function getSummary() {
     return $this->summary;
   }
 
   public function setName($name) {
     $normal_name = self::normalizeString($name);
     if (preg_match('/^@[0-9]+$/', $normal_name)) {
       throw new Exception(
         "Atom names must not be in the form '/@\d+/'. This pattern is ".
         "reserved for disambiguating atoms with similar names.");
     }
-    $this->name = $name;
+    $this->name = $normal_name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setType($type) {
     $this->type = self::normalizeString($type);
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setContext($context) {
     if ($context === null) {
       $this->context = $context;
     } else {
       $this->context = self::normalizeString($context);
     }
     return $this;
   }
 
   public function getContext() {
     return $this->context;
   }
 
   public function setBook($book) {
-    $this->book = self::normalizeString($book);
+    if ($book === null) {
+      $this->book = $book;
+    } else {
+      $this->book = self::normalizeString($book);
+    }
     return $this;
   }
 
   public function getBook() {
     return $this->book;
   }
 
   public function setGroup($group) {
     $this->group = $group;
     return $this;
   }
 
   public function getGroup() {
     return $this->group;
   }
 
+  public function setTitle($title) {
+    $this->title = $title;
+    return $this;
+  }
+
+  public function getTitle() {
+    return $this->title;
+  }
+
   public function toDictionary() {
     return array(
       'book'    => $this->getBook(),
       'context' => $this->getContext(),
       'type'    => $this->getType(),
       'name'    => $this->getName(),
       'group'   => $this->getGroup(),
       'index'   => $this->getIndex(),
       'summary' => $this->getSummary(),
+      'title'   => $this->getTitle(),
     );
   }
 
   public function toHash() {
     $dict = $this->toDictionary();
 
     unset($dict['group']);
     unset($dict['index']);
     unset($dict['summary']);
+    unset($dict['title']);
 
     ksort($dict);
     return md5(serialize($dict)).'S';
   }
 
   public static function newFromDictionary(array $dict) {
     $obj = new DivinerAtomRef();
-    $obj->book = idx($dict, 'book');
-    $obj->context = idx($dict, 'context');
-    $obj->type = idx($dict, 'type');
-    $obj->name = idx($dict, 'name');
+    $obj->setBook(idx($dict, 'book'));
+    $obj->setContext(idx($dict, 'context'));
+    $obj->setType(idx($dict, 'type'));
+    $obj->setName(idx($dict, 'name'));
     $obj->group = idx($dict, 'group');
     $obj->index = idx($dict, 'index');
     $obj->summary = idx($dict, 'summary');
+    $obj->title = idx($dict, 'title');
     return $obj;
   }
 
   public static function normalizeString($str) {
     // These characters create problems on the filesystem or in URIs. Replace
     // them with non-problematic appoximations (instead of simply removing them)
     // to keep the URIs fairly useful and avoid unnecessary collisions. These
     // approximations are selected based on some domain knowledge of common
     // languages: where a character is used as a delimiter, it is more helpful
     // to replace it with a "." or a ":" or similar, while it's better if
     // operator overloads read as, e.g., "operator_div".
 
     $map = array(
       // Hopefully not used anywhere by anything.
       '#'   => '.',
 
       // Used in Ruby methods.
       '?'   => 'Q',
 
       // Used in PHP namespaces.
       '\\'  => '.',
 
       // Used in "operator +" in C++.
       '+'   => 'plus',
 
       // Used in "operator %" in C++.
       '%'   => 'mod',
 
       // Used in "operator /" in C++.
       '/'   => 'div',
     );
     $str = str_replace(array_keys($map), array_values($map), $str);
 
     // Replace all spaces with underscores.
     $str = preg_replace('/ +/', '_', $str);
 
-    // Replace control characters with "@".
-    $str = preg_replace('/[\x00-\x19]/', '@', $str);
+    // Replace control characters with "X".
+    $str = preg_replace('/[\x00-\x19]/', 'X', $str);
 
     // Replace specific problematic names with alternative names.
     $alternates = array(
       '.'   => 'dot',
       '..'  => 'dotdot',
       ''    => 'null',
     );
 
     return idx($alternates, $str, $str);
   }
 
 }
diff --git a/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php b/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php
index bf2273b9ea..9b988c7e8f 100644
--- a/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php
+++ b/src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php
@@ -1,36 +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(
-      '/(?:^|\B)@{(?:(?P<type>[^:]+?):)?(?P<name>[^}]+?)}/',
+      '/(?:^|\B)@{'.
+        '(?:(?P<type>[^:]+?):)?'.
+        '(?P<name>[^}|]+?)'.
+        '(?:[|](?P<title>[^}]+))?'.
+      '}/',
       array($this, 'markupSymbol'),
       $text);
   }
 
   public function markupSymbol($matches) {
-    $type = $matches['type'];
-    $name = $matches['name'];
+    $type = (string)idx($matches, 'type');
+    $name = (string)$matches['name'];
+    $title = (string)idx($matches, 'title');
 
     // Collapse sequences of whitespace into a single space.
-    $name = preg_replace('/\s+/', ' ', $name);
+    $type = preg_replace('/\s+/', ' ', trim($type));
+    $name = preg_replace('/\s+/', ' ', trim($name));
+    $title = preg_replace('/\s+/', ' ', trim($title));
+
+    $ref = array();
 
-    $book = null;
     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;
     }
 
-    // TODO: This doesn't actually do anything useful yet.
+    $ref['title'] = $title;
 
-    $link = phutil_tag(
-      'a',
-      array(
-        'href' => '#',
-      ),
-      $name);
+    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;
+  }
 
-    return $this->getEngine()->storeText($link);
+  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/applications/diviner/publisher/DivinerPublisher.php b/src/applications/diviner/publisher/DivinerPublisher.php
index 98837a2422..1a63704186 100644
--- a/src/applications/diviner/publisher/DivinerPublisher.php
+++ b/src/applications/diviner/publisher/DivinerPublisher.php
@@ -1,135 +1,137 @@
 <?php
 
 abstract class DivinerPublisher {
 
   private $atomCache;
   private $atomGraphHashToNodeHashMap;
   private $atomMap = array();
   private $renderer;
   private $config;
   private $symbolReverseMap;
 
   public function setRenderer(DivinerRenderer $renderer) {
+    $renderer->setPublisher($this);
     $this->renderer = $renderer;
     return $this;
   }
 
   public function getRenderer() {
     return $this->renderer;
   }
 
   public function setConfig(array $config) {
     $this->config = $config;
     return $this;
   }
 
   public function getConfig($key, $default = null) {
     return idx($this->config, $key, $default);
   }
 
   public function setAtomCache(DivinerAtomCache $cache) {
     $this->atomCache = $cache;
     $graph_map = $this->atomCache->getGraphMap();
     $this->atomGraphHashToNodeHashMap = array_flip($graph_map);
   }
 
   protected function getAtomFromGraphHash($graph_hash) {
     if (empty($this->atomGraphHashToNodeHashMap[$graph_hash])) {
       throw new Exception("No such atom '{$graph_hash}'!");
     }
 
     return $this->getAtomFromNodeHash(
       $this->atomGraphHashToNodeHashMap[$graph_hash]);
   }
 
   protected function getAtomFromNodeHash($node_hash) {
     if (empty($this->atomMap[$node_hash])) {
       $dict = $this->atomCache->getAtom($node_hash);
       $this->atomMap[$node_hash] = DivinerAtom::newFromDictionary($dict);
     }
     return $this->atomMap[$node_hash];
   }
 
   protected function getSimilarAtoms(DivinerAtom $atom) {
     if ($this->symbolReverseMap === null) {
       $rmap = array();
       $smap = $this->atomCache->getSymbolMap();
       foreach ($smap as $nhash => $shash) {
         $rmap[$shash][$nhash] = true;
       }
       $this->symbolReverseMap = $rmap;
     }
 
     $shash = $atom->getRef()->toHash();
 
     if (empty($this->symbolReverseMap[$shash])) {
       throw new Exception("Atom has no symbol map entry!");
     }
 
     $hashes = $this->symbolReverseMap[$shash];
 
     $atoms = array();
     foreach ($hashes as $hash => $ignored) {
       $atoms[] = $this->getAtomFromNodeHash($hash);
     }
 
     $atoms = msort($atoms, 'getSortKey');
     return $atoms;
   }
 
   /**
    * If a book contains multiple definitions of some atom, like some function
    * "f()", we assign them an arbitrary (but fairly stable) order and publish
    * them as "function/f/1/", "function/f/2/", etc., or similar.
    */
   protected function getAtomSimilarIndex(DivinerAtom $atom) {
     $atoms = $this->getSimilarAtoms($atom);
     if (count($atoms) == 1) {
       return null;
     }
 
     $index = 1;
     foreach ($atoms as $similar_atom) {
       if ($atom === $similar_atom) {
         return $index;
       }
       $index++;
     }
 
     throw new Exception("Expected to find atom while disambiguating!");
   }
 
 
   abstract protected function loadAllPublishedHashes();
   abstract protected function deleteDocumentsByHash(array $hashes);
   abstract protected function createDocumentsByHash(array $hashes);
+  abstract public function findAtomByRef(DivinerAtomRef $ref);
 
   final public function publishAtoms(array $hashes) {
     $existing = $this->loadAllPublishedHashes();
 
     $existing_map = array_fill_keys($existing, true);
     $hashes_map = array_fill_keys($hashes, true);
 
     $deleted = array_diff_key($existing_map, $hashes_map);
     $created = array_diff_key($hashes_map, $existing_map);
 
     echo pht('Deleting %d documents.', count($deleted))."\n";
     $this->deleteDocumentsByHash(array_keys($deleted));
 
     echo pht('Creating %d documents.', count($created))."\n";
     $this->createDocumentsByHash(array_keys($created));
   }
 
   protected function shouldGenerateDocumentForAtom(DivinerAtom $atom) {
     switch ($atom->getType()) {
       case DivinerAtom::TYPE_FILE:
         return false;
       case DivinerAtom::TYPE_ARTICLE:
       default:
         break;
     }
 
     return true;
   }
 
 }
diff --git a/src/applications/diviner/publisher/DivinerStaticPublisher.php b/src/applications/diviner/publisher/DivinerStaticPublisher.php
index 4c03da30c0..fcfe859b4a 100644
--- a/src/applications/diviner/publisher/DivinerStaticPublisher.php
+++ b/src/applications/diviner/publisher/DivinerStaticPublisher.php
@@ -1,167 +1,210 @@
 <?php
 
 final class DivinerStaticPublisher extends DivinerPublisher {
 
   private $publishCache;
+  private $atomNameMap;
 
   private function getPublishCache() {
     if (!$this->publishCache) {
       $dir = implode(
         DIRECTORY_SEPARATOR,
         array(
           $this->getConfig('root'),
           '.divinercache',
           $this->getConfig('name'),
           'static',
         ));
       $this->publishCache = new DivinerPublishCache($dir);
     }
 
     return $this->publishCache;
   }
 
   protected function loadAllPublishedHashes() {
     return array_keys($this->getPublishCache()->getPathMap());
   }
 
   protected function deleteDocumentsByHash(array $hashes) {
     $root = $this->getConfig('root');
     $cache = $this->getPublishCache();
 
     foreach ($hashes as $hash) {
       $paths = $cache->getAtomPathsFromCache($hash);
       foreach ($paths as $path) {
         $abs = $root.DIRECTORY_SEPARATOR.$path;
         Filesystem::remove($abs);
 
         // If the parent directory is now empty, clean it up.
         $dir = dirname($abs);
         while (true) {
           if (!Filesystem::isDescendant($dir, $root)) {
             // Directory is outside of the root.
             break;
           }
           if (Filesystem::listDirectory($dir)) {
             // Directory is not empty.
             break;
           }
 
           Filesystem::remove($dir);
           $dir = dirname($dir);
         }
       }
 
       $cache->removeAtomPathsFromCache($hash);
       $cache->deleteRenderCache($hash);
       $cache->deleteAtomFromIndex($hash);
     }
   }
 
   protected function createDocumentsByHash(array $hashes) {
     $indexes = array();
 
     $cache = $this->getPublishCache();
 
     foreach ($hashes as $hash) {
       $atom = $this->getAtomFromGraphHash($hash);
 
       $paths = array();
       if ($this->shouldGenerateDocumentForAtom($atom)) {
         $content = $this->getRenderer()->renderAtom($atom);
 
         $this->writeDocument($atom, $content);
 
         $paths[] = $this->getAtomRelativePath($atom);
         if ($this->getAtomSimilarIndex($atom) !== null) {
           $index = dirname($this->getAtomRelativePath($atom)).'index.html';
           $indexes[$index] = $atom;
           $paths[] = $index;
         }
 
         $this->addAtomToIndex($hash, $atom);
       }
 
       $cache->addAtomPathsToCache($hash, $paths);
     }
 
     foreach ($indexes as $index => $atoms) {
       // TODO: Publish disambiguation pages.
     }
 
     $this->publishIndex();
 
     $cache->writePathMap();
     $cache->writeIndex();
   }
 
   private function publishIndex() {
     $index = $this->getPublishCache()->getIndex();
     $refs = array();
     foreach ($index as $hash => $dictionary) {
       $refs[$hash] = DivinerAtomRef::newFromDictionary($dictionary);
     }
 
     $content = $this->getRenderer()->renderAtomIndex($refs);
 
     $path = implode(
       DIRECTORY_SEPARATOR,
       array(
         $this->getConfig('root'),
         'docs',
         $this->getConfig('name'),
         'index.html',
       ));
 
     Filesystem::writeFile($path, $content);
   }
 
+  public function findAtomByRef(DivinerAtomRef $ref) {
+    if ($ref->getBook() != $this->getConfig('name')) {
+      return null;
+    }
+
+    if ($this->atomNameMap === null) {
+      $name_map = array();
+      foreach ($this->getPublishCache()->getIndex() as $hash => $dict) {
+        $name_map[$dict['name']][$hash] = $dict;
+      }
+      $this->atomNameMap = $name_map;
+    }
+
+    $name = $ref->getName();
+    if (empty($this->atomNameMap[$name])) {
+      return null;
+    }
+
+    $candidates = $this->atomNameMap[$name];
+    foreach ($candidates as $key => $dict) {
+      $candidates[$key] = DivinerAtomRef::newFromDict($dict);
+      if ($ref->getType()) {
+        if ($candidates[$key]->getType() != $ref->getType()) {
+          unset($candidates[$key]);
+        }
+      }
+
+      if ($ref->getContext()) {
+        if ($candidates[$key]->getContext() != $ref->getContext()) {
+          unset($candidates[$key]);
+        }
+      }
+    }
+
+    // If we have exactly one uniquely identifiable atom, return it.
+    if (count($candidates) == 1) {
+      return $this->getAtomFromNodeHash(last_key($candidates));
+    }
+
+    return null;
+  }
+
   private function addAtomToIndex($hash, DivinerAtom $atom) {
     $ref = $atom->getRef();
     $ref->setIndex($this->getAtomSimilarIndex($atom));
     $ref->setSummary($this->getRenderer()->renderAtomSummary($atom));
 
     $this->getPublishCache()->addAtomToIndex($hash, $ref->toDictionary());
   }
 
   private function writeDocument(DivinerAtom $atom, $content) {
     $root = $this->getConfig('root');
     $path = $root.DIRECTORY_SEPARATOR.$this->getAtomRelativePath($atom);
 
     if (!Filesystem::pathExists($path)) {
       Filesystem::createDirectory($path, $umask = 0755, $recursive = true);
     }
 
     Filesystem::writeFile($path.'index.html', $content);
 
     return $this;
   }
 
   private function getAtomRelativePath(DivinerAtom $atom) {
     $ref = $atom->getRef();
 
     $book = $ref->getBook();
     $type = $ref->getType();
     $context = $ref->getContext();
     $name = $ref->getName();
 
     $path = array(
       'docs',
       $book,
       $type,
     );
     if ($context !== null) {
       $path[] = $context;
     }
     $path[] = $name;
 
     $index = $this->getAtomSimilarIndex($atom);
     if ($index !== null) {
       $path[] = '@'.$index;
     }
 
     $path[] = null;
 
     return implode(DIRECTORY_SEPARATOR, $path);
   }
 
 }
diff --git a/src/applications/diviner/renderer/DivinerDefaultRenderer.php b/src/applications/diviner/renderer/DivinerDefaultRenderer.php
index e5ca5958e5..f8380156c6 100644
--- a/src/applications/diviner/renderer/DivinerDefaultRenderer.php
+++ b/src/applications/diviner/renderer/DivinerDefaultRenderer.php
@@ -1,186 +1,247 @@
 <?php
 
 final class DivinerDefaultRenderer extends DivinerRenderer {
 
   public function renderAtom(DivinerAtom $atom) {
     $out = array(
       $this->renderAtomTitle($atom),
       $this->renderAtomProperties($atom),
       $this->renderAtomDescription($atom),
     );
 
     return phutil_tag(
       'div',
       array(
         'class' => 'diviner-atom',
       ),
       $out);
   }
 
   protected function renderAtomTitle(DivinerAtom $atom) {
     $name = $this->renderAtomName($atom);
     $type = $this->renderAtomType($atom);
 
     return phutil_tag(
       'h1',
       array(
         'class' => 'atom-title',
       ),
       array($name, ' ', $type));
   }
 
   protected function renderAtomName(DivinerAtom $atom) {
     return phutil_tag(
       'div',
       array(
         'class' => 'atom-name',
       ),
       $this->getAtomName($atom));
   }
 
   protected function getAtomName(DivinerAtom $atom) {
     if ($atom->getDocblockMetaValue('title')) {
       return $atom->getDocblockMetaValue('title');
     }
 
     return $atom->getName();
   }
 
   protected function renderAtomType(DivinerAtom $atom) {
     return phutil_tag(
       'div',
       array(
         'class' => 'atom-name',
       ),
       $this->getAtomType($atom));
   }
 
   protected function getAtomType(DivinerAtom $atom) {
     return ucwords($atom->getType());
   }
 
   protected function renderAtomProperties(DivinerAtom $atom) {
     $props = $this->getAtomProperties($atom);
 
     $out = array();
     foreach ($props as $prop) {
       list($key, $value) = $prop;
 
       $out[] = phutil_tag('dt', array(), $key);
       $out[] = phutil_tag('dd', array(), $value);
     }
 
     return phutil_tag(
       'dl',
       array(
         'class' => 'atom-properties',
       ),
       $out);
   }
 
   protected function getAtomProperties(DivinerAtom $atom) {
     $properties = array();
     $properties[] = array(
       pht('Defined'),
       $atom->getFile().':'.$atom->getLine(),
     );
 
     return $properties;
   }
 
   protected function renderAtomDescription(DivinerAtom $atom) {
     $text = $this->getAtomDescription($atom);
     $engine = $this->getBlockMarkupEngine();
+
+    $this->pushAtomStack($atom);
+      $description = $engine->markupText($text);
+    $this->popAtomStack($atom);
+
     return phutil_tag(
       'div',
       array(
         'class' => 'atom-description',
       ),
-      $engine->markupText($text));
+      $description);
   }
 
   protected function getAtomDescription(DivinerAtom $atom) {
     return $atom->getDocblockText();
   }
 
   public function renderAtomSummary(DivinerAtom $atom) {
     $text = $this->getAtomSummary($atom);
     $engine = $this->getInlineMarkupEngine();
+
+    $this->pushAtomStack($atom);
+      $summary = $engine->markupText($text);
+    $this->popAtomStack();
+
     return phutil_tag(
       'span',
       array(
         'class' => 'atom-summary',
       ),
-      $engine->markupText($text));
+      $summary);
   }
 
   protected function getAtomSummary(DivinerAtom $atom) {
     if ($atom->getDocblockMetaValue('summary')) {
       return $atom->getDocblockMetaValue('summary');
     }
 
     $text = $this->getAtomDescription($atom);
     return PhabricatorMarkupEngine::summarize($text);
   }
 
   public function renderAtomIndex(array $refs) {
     $refs = msort($refs, 'getSortKey');
 
     $groups = mgroup($refs, 'getGroup');
 
     $out = array();
     foreach ($groups as $group_key => $refs) {
       $out[] = phutil_tag(
         'h1',
         array(
           'class' => 'atom-group-name',
         ),
         $this->getGroupName($group_key));
 
       $items = array();
       foreach ($refs as $ref) {
         $items[] = phutil_tag(
           'li',
           array(
             'class' => 'atom-index-item',
           ),
           array(
             $ref->getName(),
             ' - ',
             $ref->getSummary(),
           ));
       }
 
       $out[] = phutil_tag(
         'ul',
         array(
           'class' => 'atom-index-list',
         ),
         $items);
     }
 
     return phutil_tag(
       'div',
       array(
         'class' => 'atom-index',
       ),
       $out);
   }
 
   protected function getGroupName($group_key) {
     return $group_key;
   }
 
   protected function getBlockMarkupEngine() {
-    return PhabricatorMarkupEngine::newMarkupEngine(
+    $engine = PhabricatorMarkupEngine::newMarkupEngine(
       array(
         'preserve-linebreaks' => false,
       ));
+    $engine->setConfig('diviner.renderer', $this);
+    return $engine;
   }
 
   protected function getInlineMarkupEngine() {
     return $this->getBlockMarkupEngine();
   }
 
+  public function normalizeAtomRef(DivinerAtomRef $ref) {
+    if (!strlen($ref->getBook())) {
+      $ref->setBook($this->getConfig('name'));
+    }
+
+    if ($ref->getBook() != $this->getConfig('name')) {
+      // If the ref is from a different book, we can't normalize it. Just return
+      // it as-is if it has enough information to resolve.
+      if ($ref->getName() && $ref->getType()) {
+        return $ref;
+      } else {
+        return null;
+      }
+    }
+
+    $atom = $this->getPublisher()->findAtomByRef($ref);
+    if ($atom) {
+      return $atom->getRef();
+    }
+
+    return null;
+  }
+
+  protected function getAtomHrefDepth(DivinerAtom $atom) {
+    if ($atom->getContext()) {
+      return 4;
+    } else {
+      return 3;
+    }
+  }
+
+  public function getHrefForAtomRef(DivinerAtomRef $ref) {
+    $atom = $this->peekAtomStack();
+    $depth = $this->getAtomHrefDepth($atom);
+    $href = str_repeat('../', $depth);
+
+    $book = $ref->getBook();
+    $type = $ref->getType();
+    $name = $ref->getName();
+    $context = $ref->getContext();
+
+    $href .= $book.'/'.$type.'/';
+    if ($context !== null) {
+      $href .= $context.'/';
+    }
+    $href .= $name.'/';
+
+    return $href;
+  }
 
 }
diff --git a/src/applications/diviner/renderer/DivinerRenderer.php b/src/applications/diviner/renderer/DivinerRenderer.php
index 333ef07047..dc86e0fe32 100644
--- a/src/applications/diviner/renderer/DivinerRenderer.php
+++ b/src/applications/diviner/renderer/DivinerRenderer.php
@@ -1,9 +1,40 @@
 <?php
 
 abstract class DivinerRenderer {
 
+  private $publisher;
+  private $atomStack;
+
+  public function setPublisher($publisher) {
+    $this->publisher = $publisher;
+    return $this;
+  }
+
+  public function getPublisher() {
+    return $this->publisher;
+  }
+
+  public function getConfig($key, $default = null) {
+    return $this->getPublisher()->getConfig($key, $default);
+  }
+
+  protected function pushAtomStack(DivinerAtom $atom) {
+    $this->atomStack[] = $atom;
+    return $this;
+  }
+
+  protected function peekAtomStack() {
+    return end($this->atomStack);
+  }
+
+  protected function popAtomStack() {
+    array_pop($this->atomStack);
+    return $this;
+  }
+
   abstract public function renderAtom(DivinerAtom $atom);
   abstract public function renderAtomSummary(DivinerAtom $atom);
   abstract public function renderAtomIndex(array $refs);
+  abstract public function getHrefForAtomRef(DivinerAtomRef $ref);
 
 }