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); }