diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php index d381a53d03..d923d3d641 100644 --- a/src/applications/diviner/controller/DivinerAtomController.php +++ b/src/applications/diviner/controller/DivinerAtomController.php @@ -1,304 +1,424 @@ bookName = $data['book']; $this->atomType = $data['type']; $this->atomName = $data['name']; $this->atomContext = nonempty(idx($data, 'context'), null); $this->atomIndex = nonempty(idx($data, 'index'), null); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $book = id(new DivinerBookQuery()) ->setViewer($viewer) ->withNames(array($this->bookName)) ->executeOne(); if (!$book) { return new Aphront404Response(); } // TODO: This query won't load ghosts, because they'll fail `needAtoms()`. // Instead, we might want to load ghosts and render a message like // "this thing existed in an older version, but no longer does", especially // if we add content like comments. $symbol = id(new DivinerAtomQuery()) ->setViewer($viewer) ->withBookPHIDs(array($book->getPHID())) ->withTypes(array($this->atomType)) ->withNames(array($this->atomName)) ->withContexts(array($this->atomContext)) ->withIndexes(array($this->atomIndex)) ->needAtoms(true) ->needExtends(true) + ->needChildren(true) ->executeOne(); if (!$symbol) { return new Aphront404Response(); } $atom = $symbol->getAtom(); - - $extends = $atom->getExtends(); - - $child_hashes = $atom->getChildHashes(); - if ($child_hashes) { - $children = id(new DivinerAtomQuery()) - ->setViewer($viewer) - ->withIncludeUndocumentable(true) - ->withNodeHashes($child_hashes) - ->execute(); - } else { - $children = array(); - } - $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($book->getShortTitle()) ->setHref('/book/'.$book->getName().'/')); $atom_short_title = $atom->getDocblockMetaValue( 'short', $symbol->getTitle()); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($atom_short_title)); $header = id(new PhabricatorHeaderView()) ->setHeader($symbol->getTitle()) ->addTag( id(new PhabricatorTagView()) ->setType(PhabricatorTagView::TYPE_STATE) ->setBackgroundColor(PhabricatorTagView::COLOR_BLUE) ->setName(DivinerAtom::getAtomTypeNameString($atom->getType()))); $properties = id(new PhabricatorPropertyListView()); $group = $atom->getDocblockMetaValue('group'); if ($group) { $group_name = $book->getGroupName($group); } else { $group_name = null; } $this->buildDefined($properties, $symbol); $this->buildExtendsAndImplements($properties, $symbol); $warnings = $atom->getWarnings(); if ($warnings) { $warnings = id(new AphrontErrorView()) ->setErrors($warnings) ->setTitle(pht('Documentation Warnings')) ->setSeverity(AphrontErrorView::SEVERITY_WARNING); } $field = 'default'; $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer) ->addObject($symbol, $field) ->process(); $content = $engine->getOutput($symbol, $field); if (strlen(trim($symbol->getMarkupText($field)))) { $content = phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), array( $content, )); } else { $undoc = DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType()); $content = id(new AphrontErrorView()) ->appendChild($undoc) ->setSeverity(AphrontErrorView::SEVERITY_NODATA); } $toc = $engine->getEngineMetadata( $symbol, $field, PhutilRemarkupEngineRemarkupHeaderBlockRule::KEY_HEADER_TOC, array()); $document = id(new PHUIDocumentView()) ->setBook($book->getTitle(), $group_name) ->setHeader($header) ->appendChild($properties) ->appendChild($warnings) ->appendChild($content); $parameters = $atom->getProperty('parameters'); if ($parameters !== null) { $document->appendChild( id(new PhabricatorHeaderView()) ->setHeader(pht('Parameters'))); $document->appendChild( id(new DivinerParameterTableView()) ->setParameters($parameters)); } $return = $atom->getProperty('return'); if ($return !== null) { $document->appendChild( id(new PhabricatorHeaderView()) ->setHeader(pht('Return'))); $document->appendChild( id(new DivinerReturnTableView()) ->setReturn($return)); } - if ($children) { + $methods = $this->composeMethods($symbol); + if ($methods) { + + $tasks = $this->composeTasks($symbol); + + if ($tasks) { + $methods_by_task = igroup($methods, 'task'); + + $document->appendChild( + id(new PhabricatorHeaderView()) + ->setHeader(pht('Tasks'))); + + if (isset($methods_by_task[''])) { + $tasks[''] = array( + 'name' => '', + 'title' => pht('Other Methods'), + 'defined' => $symbol, + ); + } + + foreach ($tasks as $spec) { + $document->appendChild( + id(new PhabricatorHeaderView()) + ->setHeader($spec['title'])); + + $task_methods = idx($methods_by_task, $spec['name'], array()); + if ($task_methods) { + $document->appendChild(hsprintf('')); + } else { + $document->appendChild("No methods for this task."); + } + } + } + $document->appendChild( id(new PhabricatorHeaderView()) ->setHeader(pht('Methods'))); - foreach ($children as $child) { - $document->appendChild( - id(new PhabricatorHeaderView()) - ->setHeader($child->getName())); + foreach ($methods as $spec) { + $method_header = id(new PhabricatorHeaderView()) + ->setHeader(last($spec['atoms'])->getName()); + + $inherited = $spec['inherited']; + if ($inherited) { + $method_header->addTag( + id(new PhabricatorTagView()) + ->setType(PhabricatorTagView::TYPE_STATE) + ->setBackgroundColor(PhabricatorTagView::COLOR_GREY) + ->setName(pht('Inherited'))); + } + + $document->appendChild($method_header); } } if ($toc) { $side = new PHUIListView(); $side->addMenuItem( id(new PHUIListItemView()) ->setName(pht('Contents')) ->setType(PHUIListItemView::TYPE_LABEL)); foreach ($toc as $key => $entry) { $side->addMenuItem( id(new PHUIListItemView()) ->setName($entry[1]) ->setHref('#'.$key)); } $document->setSideNav($side, PHUIDocumentView::NAV_TOP); } return $this->buildApplicationPage( array( $crumbs, $document, ), array( 'title' => $symbol->getTitle(), 'device' => true, )); } private function buildExtendsAndImplements( PhabricatorPropertyListView $view, DivinerLiveSymbol $symbol) { $lineage = $this->getExtendsLineage($symbol); if ($lineage) { $lineage = mpull($lineage, 'getName'); $lineage = implode(' > ', $lineage); $view->addProperty(pht('Extends'), $lineage); } $implements = $this->getImplementsLineage($symbol); if ($implements) { $items = array(); foreach ($implements as $spec) { $via = $spec['via']; $iface = $spec['interface']; if ($via == $symbol) { $items[] = $iface->getName(); } else { $items[] = $iface->getName().' (via '.$via->getName().')'; } } $view->addProperty( pht('Implements'), phutil_implode_html(phutil_tag('br'), $items)); } } private function getExtendsLineage(DivinerLiveSymbol $symbol) { foreach ($symbol->getExtends() as $extends) { if ($extends->getType() == 'class') { $lineage = $this->getExtendsLineage($extends); $lineage[] = $extends; return $lineage; } } return array(); } private function getImplementsLineage(DivinerLiveSymbol $symbol) { $implements = array(); // Do these first so we get interfaces ordered from most to least specific. foreach ($symbol->getExtends() as $extends) { if ($extends->getType() == 'interface') { $implements[$extends->getName()] = array( 'interface' => $extends, 'via' => $symbol, ); } } // Now do parent interfaces. foreach ($symbol->getExtends() as $extends) { if ($extends->getType() == 'class') { $implements += $this->getImplementsLineage($extends); } } return $implements; } private function buildDefined( PhabricatorPropertyListView $view, DivinerLiveSymbol $symbol) { $atom = $symbol->getAtom(); $defined = $atom->getFile().':'.$atom->getLine(); $link = $symbol->getBook()->getConfig('uri.source'); if ($link) { $link = strtr( $link, array( '%%' => '%', '%f' => phutil_escape_uri($atom->getFile()), '%l' => phutil_escape_uri($atom->getLine()), )); $defined = phutil_tag( 'a', array( 'href' => $link, 'target' => '_blank', ), $defined); } $view->addProperty(pht('Defined'), $defined); } + private function composeMethods(DivinerLiveSymbol $symbol) { + $methods = $this->findMethods($symbol); + if (!$methods) { + return $methods; + } + + foreach ($methods as $name => $method) { + // Check for "@task" on each parent, to find the most recently declared + // "@task". + $task = null; + foreach ($method['atoms'] as $key => $method_symbol) { + $atom = $method_symbol->getAtom(); + if ($atom->getDocblockMetaValue('task')) { + $task = $atom->getDocblockMetaValue('task'); + } + } + $methods[$name]['task'] = $task; + + // Set 'inherited' if this atom has no implementation of the method. + if (last($method['implementations']) !== $symbol) { + $methods[$name]['inherited'] = true; + } else { + $methods[$name]['inherited'] = false; + } + } + + return $methods; + } + + private function findMethods(DivinerLiveSymbol $symbol) { + $child_specs = array(); + foreach ($symbol->getExtends() as $extends) { + if ($extends->getType() == DivinerAtom::TYPE_CLASS) { + $child_specs = $this->findMethods($extends); + } + } + + foreach ($symbol->getChildren() as $child) { + if ($child->getType() == DivinerAtom::TYPE_METHOD) { + $name = $child->getName(); + if (isset($child_specs[$name])) { + $child_specs[$name]['atoms'][] = $child; + $child_specs[$name]['implementations'][] = $symbol; + } else { + $child_specs[$name] = array( + 'atoms' => array($child), + 'defined' => $symbol, + 'implementations' => array($symbol), + ); + } + } + } + + return $child_specs; + } + + private function composeTasks(DivinerLiveSymbol $symbol) { + $extends_task_specs = array(); + foreach ($symbol->getExtends() as $extends) { + $extends_task_specs += $this->composeTasks($extends); + } + + $task_specs = array(); + + $tasks = $symbol->getAtom()->getDocblockMetaValue('task'); + if (strlen($tasks)) { + $tasks = phutil_split_lines($tasks, $retain_endings = false); + + foreach ($tasks as $task) { + list($name, $title) = explode(' ', $task, 2); + $name = trim($name); + $title = trim($title); + + $task_specs[$name] = array( + 'name' => $name, + 'title' => $title, + 'defined' => $symbol, + ); + } + } + + return $task_specs + $extends_task_specs; + } + } diff --git a/src/applications/diviner/publisher/DivinerLivePublisher.php b/src/applications/diviner/publisher/DivinerLivePublisher.php index 164aa6f548..62004da063 100644 --- a/src/applications/diviner/publisher/DivinerLivePublisher.php +++ b/src/applications/diviner/publisher/DivinerLivePublisher.php @@ -1,144 +1,151 @@ book) { $book_name = $this->getConfig('name'); $book = id(new DivinerLiveBook())->loadOneWhere( 'name = %s', $book_name); if (!$book) { $book = id(new DivinerLiveBook()) ->setName($book_name) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->save(); } $book->setConfigurationData($this->getConfigurationData())->save(); $this->book = $book; } return $this->book; } private function loadSymbolForAtom(DivinerAtom $atom) { $symbol = id(new DivinerAtomQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBookPHIDs(array($this->loadBook()->getPHID())) ->withTypes(array($atom->getType())) ->withNames(array($atom->getName())) ->withContexts(array($atom->getContext())) ->withIndexes(array($this->getAtomSimilarIndex($atom))) ->withIncludeUndocumentable(true) ->withIncludeGhosts(true) ->executeOne(); if ($symbol) { return $symbol; } return id(new DivinerLiveSymbol()) ->setBookPHID($this->loadBook()->getPHID()) ->setType($atom->getType()) ->setName($atom->getName()) ->setContext($atom->getContext()) ->setAtomIndex($this->getAtomSimilarIndex($atom)); } private function loadAtomStorageForSymbol(DivinerLiveSymbol $symbol) { $storage = id(new DivinerLiveAtom())->loadOneWhere( 'symbolPHID = %s', $symbol->getPHID()); if ($storage) { return $storage; } return id(new DivinerLiveAtom()) ->setSymbolPHID($symbol->getPHID()); } protected function loadAllPublishedHashes() { $symbols = id(new DivinerAtomQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBookPHIDs(array($this->loadBook()->getPHID())) ->withIncludeUndocumentable(true) ->execute(); return mpull($symbols, 'getGraphHash'); } protected function deleteDocumentsByHash(array $hashes) { $atom_table = new DivinerLiveAtom(); $symbol_table = new DivinerLiveSymbol(); $conn_w = $symbol_table->establishConnection('w'); $strings = array(); foreach ($hashes as $hash) { $strings[] = qsprintf($conn_w, '%s', $hash); } foreach (PhabricatorLiskDAO::chunkSQL($strings, ', ') as $chunk) { queryfx( $conn_w, 'UPDATE %T SET graphHash = NULL, nodeHash = NULL WHERE graphHash IN (%Q)', $symbol_table->getTableName(), $chunk); } queryfx( $conn_w, 'DELETE a FROM %T a LEFT JOIN %T s ON a.symbolPHID = s.phid WHERE s.graphHash IS NULL', $atom_table->getTableName(), $symbol_table->getTableName()); } protected function createDocumentsByHash(array $hashes) { foreach ($hashes as $hash) { $atom = $this->getAtomFromGraphHash($hash); $ref = $atom->getRef(); $symbol = $this->loadSymbolForAtom($atom); $is_documentable = $this->shouldGenerateDocumentForAtom($atom); $symbol ->setGraphHash($hash) ->setIsDocumentable((int)$is_documentable) ->setTitle($ref->getTitle()) ->setGroupName($ref->getGroup()) ->setNodeHash($atom->getHash()); if ($is_documentable) { $renderer = $this->getRenderer(); $summary = $renderer->renderAtomSummary($atom); $summary = (string)phutil_safe_html($summary); $symbol->setSummary($summary); } $symbol->save(); - if ($is_documentable) { - $storage = $this->loadAtomStorageForSymbol($symbol) - ->setAtomData($atom->toDictionary()) - ->setContent(null) - ->save(); - } + // TODO: We probably need a finer-grained sense of what "documentable" + // atoms are. Neither files nor methods are currently considered + // documentable, but for different reasons: files appear nowhere, while + // methods just don't appear at the top level. These are probably + // separate concepts. Since we need atoms in order to build method + // documentation, we insert them here. This also means we insert files, + // which are unnecessary and unused. Make sure this makes sense, but then + // probably introduce separate "isTopLevel" and "isDocumentable" flags? + + $storage = $this->loadAtomStorageForSymbol($symbol) + ->setAtomData($atom->toDictionary()) + ->setContent(null) + ->save(); } } public function findAtomByRef(DivinerAtomRef $ref) { // TODO: Actually implement this. return null; } } diff --git a/src/applications/diviner/query/DivinerAtomQuery.php b/src/applications/diviner/query/DivinerAtomQuery.php index 06e6fda1e3..662bc834d8 100644 --- a/src/applications/diviner/query/DivinerAtomQuery.php +++ b/src/applications/diviner/query/DivinerAtomQuery.php @@ -1,325 +1,408 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withBookPHIDs(array $phids) { $this->bookPHIDs = $phids; return $this; } public function withTypes(array $types) { $this->types = $types; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withContexts(array $contexts) { $this->contexts = $contexts; return $this; } public function withIndexes(array $indexes) { $this->indexes = $indexes; return $this; } public function withNodeHashes(array $hashes) { $this->nodeHashes = $hashes; return $this; } public function needAtoms($need) { $this->needAtoms = $need; return $this; } + public function needChildren($need) { + $this->needChildren = $need; + return $this; + } + /** * Include "ghosts", which are symbols which used to exist but do not exist * currently (for example, a function which existed in an older version of * the codebase but was deleted). * * These symbols had PHIDs assigned to them, and may have other sorts of * metadata that we don't want to lose (like comments or flags), so we don't * delete them outright. They might also come back in the future: the change * which deleted the symbol might be reverted, or the documentation might * have been generated incorrectly by accident. In these cases, we can * restore the original data. * * However, most callers are not interested in these symbols, so they are * excluded by default. You can use this method to include them in results. * * @param bool True to include ghosts. * @return this */ public function withIncludeGhosts($include) { $this->includeGhosts = $include; return $this; } public function needExtends($need) { $this->needExtends = $need; return $this; } public function withIncludeUndocumentable($include) { $this->includeUndocumentable = $include; return $this; } protected function loadPage() { $table = new DivinerLiveSymbol(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $atoms) { $books = array_unique(mpull($atoms, 'getBookPHID')); $books = id(new DivinerBookQuery()) ->setViewer($this->getViewer()) ->withPHIDs($books) ->execute(); $books = mpull($books, null, 'getPHID'); foreach ($atoms as $key => $atom) { $book = idx($books, $atom->getBookPHID()); if (!$book) { unset($atoms[$key]); continue; } $atom->attachBook($book); } - $need_atoms = $this->needAtoms; - if ($this->needAtoms) { $atom_data = id(new DivinerLiveAtom())->loadAllWhere( 'symbolPHID IN (%Ls)', mpull($atoms, 'getPHID')); $atom_data = mpull($atom_data, null, 'getSymbolPHID'); foreach ($atoms as $key => $atom) { $data = idx($atom_data, $atom->getPHID()); if (!$data) { unset($atoms[$key]); continue; } $atom->attachAtom($data); } } // Load all of the symbols this symbol extends, recursively. Commonly, // this means all the ancestor classes and interfaces it extends and // implements. if ($this->needExtends) { // First, load all the matching symbols by name. This does 99% of the // work in most cases, assuming things are named at all reasonably. $names = array(); foreach ($atoms as $atom) { foreach ($atom->getAtom()->getExtends() as $xref) { $names[] = $xref->getName(); } } if ($names) { $xatoms = id(new DivinerAtomQuery()) ->setViewer($this->getViewer()) ->withNames($names) ->needExtends(true) ->needAtoms(true) + ->needChildren($this->needChildren) ->execute(); $xatoms = mgroup($xatoms, 'getName', 'getType', 'getBookPHID'); } else { $xatoms = array(); } foreach ($atoms as $atom) { $alang = $atom->getAtom()->getLanguage(); $extends = array(); foreach ($atom->getAtom()->getExtends() as $xref) { // If there are no symbols of the matching name and type, we can't // resolve this. if (empty($xatoms[$xref->getName()][$xref->getType()])) { continue; } // If we found matches in the same documentation book, prefer them // over other matches. Otherwise, look at all the the matches. $matches = $xatoms[$xref->getName()][$xref->getType()]; if (isset($matches[$atom->getBookPHID()])) { $maybe = $matches[$atom->getBookPHID()]; } else { $maybe = array_mergev($matches); } if (!$maybe) { continue; } // Filter out matches in a different language, since, e.g., PHP // classes can not implement JS classes. $same_lang = array(); foreach ($maybe as $xatom) { if ($xatom->getAtom()->getLanguage() == $alang) { $same_lang[] = $xatom; } } if (!$same_lang) { continue; } // If we have duplicates remaining, just pick the first one. There's // nothing more we can do to figure out which is the real one. $extends[] = head($same_lang); } $atom->attachExtends($extends); } } + if ($this->needChildren) { + $child_hashes = $this->getAllChildHashes($atoms, $this->needExtends); + + if ($child_hashes) { + $children = id(new DivinerAtomQuery()) + ->setViewer($this->getViewer()) + ->withIncludeUndocumentable(true) + ->withNodeHashes($child_hashes) + ->needAtoms($this->needAtoms) + ->execute(); + + $children = mpull($children, null, 'getNodeHash'); + } else { + $children = array(); + } + + $this->attachAllChildren($atoms, $children, $this->needExtends); + } + return $atoms; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } if ($this->bookPHIDs) { $where[] = qsprintf( $conn_r, 'bookPHID IN (%Ls)', $this->bookPHIDs); } if ($this->types) { $where[] = qsprintf( $conn_r, 'type IN (%Ls)', $this->types); } if ($this->names) { $where[] = qsprintf( $conn_r, 'name IN (%Ls)', $this->names); } if ($this->contexts) { $with_null = false; $contexts = $this->contexts; foreach ($contexts as $key => $value) { if ($value === null) { unset($contexts[$key]); $with_null = true; continue; } } if ($contexts && $with_null) { $where[] = qsprintf( $conn_r, 'context IN (%Ls) OR context IS NULL', $contexts); } else if ($contexts) { $where[] = qsprintf( $conn_r, 'context IN (%Ls)', $contexts); } else if ($with_null) { $where[] = qsprintf( $conn_r, 'context IS NULL'); } } if ($this->indexes) { $where[] = qsprintf( $conn_r, 'atomIndex IN (%Ld)', $this->indexes); } if (!$this->includeUndocumentable) { $where[] = qsprintf( $conn_r, 'isDocumentable = 1'); } if (!$this->includeGhosts) { $where[] = qsprintf( $conn_r, 'graphHash IS NOT NULL'); } if ($this->nodeHashes) { $where[] = qsprintf( $conn_r, 'nodeHash IN (%Ls)', $this->nodeHashes); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } + + /** + * Walk a list of atoms and collect all the node hashes of the atoms' + * children. When recursing, also walk up the tree and collect children of + * atoms they extend. + * + * @param list List of symbols to collect child hashes of. + * @param bool True to collect children of extended atoms, + * as well. + * @return map Hashes of atoms' children. + */ + private function getAllChildHashes(array $symbols, $recurse_up) { + assert_instances_of($symbols, 'DivinerLiveSymbol'); + + $hashes = array(); + foreach ($symbols as $symbol) { + foreach ($symbol->getAtom()->getChildHashes() as $hash) { + $hashes[$hash] = $hash; + } + if ($recurse_up) { + $hashes += $this->getAllChildHashes($symbol->getExtends(), true); + } + } + + return $hashes; + } + + + /** + * Attach child atoms to existing atoms. In recursive mode, also attach child + * atoms to atoms that these atoms extend. + * + * @param list List of symbols to attach childeren to. + * @param map Map of symbols, keyed by node hash. + * @param bool True to attach children to extended atoms, as well. + * @return void + */ + private function attachAllChildren( + array $symbols, + array $children, + $recurse_up) { + + assert_instances_of($symbols, 'DivinerLiveSymbol'); + assert_instances_of($children, 'DivinerLiveSymbol'); + + foreach ($symbols as $symbol) { + $symbol_children = array(); + foreach ($symbol->getAtom()->getChildHashes() as $hash) { + if (isset($children[$hash])) { + $symbol_children[] = $children[$hash]; + } + } + $symbol->attachChildren($symbol_children); + if ($recurse_up) { + $this->attachAllChildren($symbol->getExtends(), $children, true); + } + } + } + } diff --git a/src/applications/diviner/storage/DivinerLiveSymbol.php b/src/applications/diviner/storage/DivinerLiveSymbol.php index 8a8a4bb79f..51bad9b94e 100644 --- a/src/applications/diviner/storage/DivinerLiveSymbol.php +++ b/src/applications/diviner/storage/DivinerLiveSymbol.php @@ -1,172 +1,183 @@ true, self::CONFIG_TIMESTAMPS => false, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DivinerPHIDTypeAtom::TYPECONST); } public function getBook() { return $this->assertAttached($this->book); } public function attachBook(DivinerLiveBook $book) { $this->book = $book; return $this; } public function getAtom() { return $this->assertAttached($this->atom); } public function attachAtom(DivinerLiveAtom $atom) { $this->atom = DivinerAtom::newFromDictionary($atom->getAtomData()); return $this; } public function getURI() { $parts = array( 'book', $this->getBook()->getName(), $this->getType(), ); if ($this->getContext()) { $parts[] = $this->getContext(); } $parts[] = $this->getName(); if ($this->getAtomIndex()) { $parts[] = $this->getAtomIndex(); } return '/'.implode('/', $parts).'/'; } public function getSortKey() { return $this->getTitle(); } public function save() { // NOTE: The identity hash is just a sanity check because the unique tuple // on this table is way way too long to fit into a normal UNIQUE KEY. We // don't use it directly, but its existence prevents duplicate records. if (!$this->identityHash) { $this->identityHash = PhabricatorHash::digestForIndex( serialize( array( 'bookPHID' => $this->getBookPHID(), 'context' => $this->getContext(), 'type' => $this->getType(), 'name' => $this->getName(), 'index' => $this->getAtomIndex(), ))); } return parent::save(); } public function getTitle() { $title = parent::getTitle(); if (!strlen($title)) { $title = $this->getName(); } return $title; } public function attachExtends(array $extends) { assert_instances_of($extends, 'DivinerLiveSymbol'); $this->extends = $extends; return $this; } public function getExtends() { return $this->assertAttached($this->extends); } + public function attachChildren(array $children) { + assert_instances_of($children, 'DivinerLiveSymbol'); + $this->children = $children; + return $this; + } + + public function getChildren() { + return $this->assertAttached($this->children); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return $this->getBook()->getCapabilities(); } public function getPolicy($capability) { return $this->getBook()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBook()->hasAutomaticCapability($capability, $viewer); } /* -( Markup Interface )--------------------------------------------------- */ public function getMarkupFieldKey($field) { return $this->getPHID().':'.$field.':'.$this->getGraphHash(); } public function newMarkupEngine($field) { $engine = PhabricatorMarkupEngine::newMarkupEngine(array()); $engine->setConfig('preserve-linebreaks', false); // $engine->setConfig('diviner.renderer', new DivinerDefaultRenderer()); $engine->setConfig('header.generate-toc', true); return $engine; } public function getMarkupText($field) { return $this->getAtom()->getDocblockText(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return false; } }