@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Generate PHP function documentation in Diviner

Summary:
Ref T988. Various improvements:

- Generate function documentation, mostly correctly.
- Raise some warnings about bad documentation.
- Allow `.book` files to exclude paths from generation.
- Add a book for technical docs.
- Exclude "ghosts" from common queries (atoms which used to exist, but no longer do, but which we want to keep the PHIDs around for in case they come back later).

This is a bit rough still, but puts us much closer to being able to get rid of the old Diviner.

Test Plan: See screenshots.

Reviewers: btrahan, chad

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T988

Differential Revision: https://secure.phabricator.com/D6812

+590 -13
+9
src/__celerity_resource_map__.php
··· 1147 1147 ), 1148 1148 'disk' => '/rsrc/css/application/diffusion/diffusion-source.css', 1149 1149 ), 1150 + 'diviner-shared-css' => 1151 + array( 1152 + 'uri' => '/res/f462d51d/rsrc/css/diviner/diviner-shared.css', 1153 + 'type' => 'css', 1154 + 'requires' => 1155 + array( 1156 + ), 1157 + 'disk' => '/rsrc/css/diviner/diviner-shared.css', 1158 + ), 1150 1159 'global-drag-and-drop-css' => 1151 1160 array( 1152 1161 'uri' => '/res/4e24cb65/rsrc/css/application/files/global-drag-and-drop.css',
+6
src/__phutil_library_map__.php
··· 534 534 'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.php', 535 535 'DivinerPHIDTypeAtom' => 'applications/diviner/phid/DivinerPHIDTypeAtom.php', 536 536 'DivinerPHIDTypeBook' => 'applications/diviner/phid/DivinerPHIDTypeBook.php', 537 + 'DivinerPHPAtomizer' => 'applications/diviner/atomizer/DivinerPHPAtomizer.php', 538 + 'DivinerParameterTableView' => 'applications/diviner/view/DivinerParameterTableView.php', 537 539 'DivinerPublishCache' => 'applications/diviner/cache/DivinerPublishCache.php', 538 540 'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php', 539 541 'DivinerRemarkupRuleSymbol' => 'applications/diviner/markup/DivinerRemarkupRuleSymbol.php', 540 542 'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php', 543 + 'DivinerReturnTableView' => 'applications/diviner/view/DivinerReturnTableView.php', 541 544 'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php', 542 545 'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php', 543 546 'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php', ··· 2559 2562 ), 2560 2563 'DivinerPHIDTypeAtom' => 'PhabricatorPHIDType', 2561 2564 'DivinerPHIDTypeBook' => 'PhabricatorPHIDType', 2565 + 'DivinerPHPAtomizer' => 'DivinerAtomizer', 2566 + 'DivinerParameterTableView' => 'AphrontTagView', 2562 2567 'DivinerPublishCache' => 'DivinerDiskCache', 2563 2568 'DivinerRemarkupRuleSymbol' => 'PhutilRemarkupRule', 2569 + 'DivinerReturnTableView' => 'AphrontTagView', 2564 2570 'DivinerStaticPublisher' => 'DivinerPublisher', 2565 2571 'DivinerWorkflow' => 'PhutilArgumentWorkflow', 2566 2572 'DoorkeeperBridge' => 'Phobject',
+39 -2
src/applications/diviner/atom/DivinerAtom.php
··· 23 23 private $extends = array(); 24 24 private $links = array(); 25 25 private $book; 26 + private $properties = array(); 26 27 27 28 /** 28 29 * Returns a sorting key which imposes an unambiguous, stable order on atoms. ··· 59 60 } 60 61 61 62 public static function getAtomSerializationVersion() { 62 - return 1; 63 + return 2; 63 64 } 64 65 65 66 public function addWarning($warning) { ··· 194 195 $this->getLanguage(), 195 196 $this->getContentRaw(), 196 197 $this->getDocblockRaw(), 198 + $this->getProperties(), 197 199 mpull($this->extends, 'toHash'), 198 200 mpull($this->links, 'toHash'), 199 201 ); ··· 280 282 'extends' => $this->getExtendsDictionaries(), 281 283 'links' => $this->getLinkDictionaries(), 282 284 'ref' => $this->getRef()->toDictionary(), 285 + 'properties' => $this->getProperties(), 283 286 ); 284 287 } 285 288 ··· 312 315 ->setContext(idx($dictionary, 'context')) 313 316 ->setLanguage(idx($dictionary, 'language')) 314 317 ->setParentHash(idx($dictionary, 'parentHash')) 315 - ->setDocblockRaw(idx($dictionary, 'docblockRaw')); 318 + ->setDocblockRaw(idx($dictionary, 'docblockRaw')) 319 + ->setProperties(idx($dictionary, 'properties')); 316 320 317 321 foreach (idx($dictionary, 'warnings', array()) as $warning) { 318 322 $atom->addWarning($warning); ··· 323 327 } 324 328 325 329 return $atom; 330 + } 331 + 332 + public function getProperty($key, $default = null) { 333 + return idx($this->properties, $key, $default); 334 + } 335 + 336 + public function setProperty($key, $value) { 337 + $this->properties[$key] = $value; 338 + } 339 + 340 + public function getProperties() { 341 + return $this->properties; 342 + } 343 + 344 + public function setProperties(array $properties) { 345 + $this->properties = $properties; 346 + return $this; 347 + } 348 + 349 + public static function getThisAtomIsNotDocumentedString($type) { 350 + switch ($type) { 351 + case 'function': 352 + return pht('This function is not documented.'); 353 + case 'class': 354 + return pht('This class is not documented.'); 355 + case 'article': 356 + return pht('This article is not documented.'); 357 + case 'method': 358 + return pht('This method is not documented.'); 359 + default: 360 + phlog("Need translation for '{$type}'."); 361 + return pht('This %s is not documented.', $type); 362 + } 326 363 } 327 364 328 365 }
+229
src/applications/diviner/atomizer/DivinerPHPAtomizer.php
··· 1 + <?php 2 + 3 + final class DivinerPHPAtomizer extends DivinerAtomizer { 4 + 5 + protected function executeAtomize($file_name, $file_data) { 6 + $future = xhpast_get_parser_future($file_data); 7 + $tree = XHPASTTree::newFromDataAndResolvedExecFuture( 8 + $file_data, 9 + $future->resolve()); 10 + 11 + $atoms = array(); 12 + 13 + $root = $tree->getRootNode(); 14 + 15 + $func_decl = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); 16 + foreach ($func_decl as $func) { 17 + 18 + $name = $func->getChildByIndex(2); 19 + 20 + $atom = id(new DivinerAtom()) 21 + ->setType('function') 22 + ->setName($name->getConcreteString()) 23 + ->setLine($func->getLineNumber()) 24 + ->setFile($file_name); 25 + 26 + $this->findAtomDocblock($atom, $func); 27 + 28 + $this->parseParams($atom, $func); 29 + $this->parseReturnType($atom, $func); 30 + 31 + $atoms[] = $atom; 32 + } 33 + 34 + return $atoms; 35 + } 36 + 37 + private function parseParams(DivinerAtom $atom, AASTNode $func) { 38 + $params = $func 39 + ->getChildByIndex(3, 'n_DECLARATAION_PARAMETER_LIST') 40 + ->selectDescendantsOfType('n_DECLARATION_PARAMETER'); 41 + 42 + $param_spec = array(); 43 + 44 + if ($atom->getDocblockRaw()) { 45 + $metadata = $atom->getDocblockMeta(); 46 + } else { 47 + $metadata = array(); 48 + } 49 + 50 + $docs = idx($metadata, 'param'); 51 + if ($docs) { 52 + $docs = explode("\n", $docs); 53 + $docs = array_filter($docs); 54 + } else { 55 + $docs = array(); 56 + } 57 + 58 + if (count($docs)) { 59 + if (count($docs) < count($params)) { 60 + $atom->addWarning( 61 + pht( 62 + 'This call takes %d parameters, but only %d are documented.', 63 + count($params), 64 + count($docs))); 65 + } 66 + } 67 + 68 + foreach ($params as $param) { 69 + $name = $param->getChildByIndex(1)->getConcreteString(); 70 + $dict = array( 71 + 'type' => $param->getChildByIndex(0)->getConcreteString(), 72 + 'default' => $param->getChildByIndex(2)->getConcreteString(), 73 + ); 74 + 75 + if ($docs) { 76 + $doc = array_shift($docs); 77 + if ($doc) { 78 + $dict += $this->parseParamDoc($atom, $doc, $name); 79 + } 80 + } 81 + 82 + $param_spec[] = array( 83 + 'name' => $name, 84 + ) + $dict; 85 + } 86 + 87 + if ($docs) { 88 + foreach ($docs as $doc) { 89 + if ($doc) { 90 + $param_spec[] = $this->parseParamDoc($atom, $doc, null); 91 + } 92 + } 93 + } 94 + 95 + // TODO: Find `assert_instances_of()` calls in the function body and 96 + // add their type information here. See T1089. 97 + 98 + $atom->setProperty('parameters', $param_spec); 99 + } 100 + 101 + 102 + private function findAtomDocblock(DivinerAtom $atom, XHPASTNode $node) { 103 + $token = $node->getDocblockToken(); 104 + if ($token) { 105 + $atom->setDocblockRaw($token->getValue()); 106 + return true; 107 + } else { 108 + $tokens = $node->getTokens(); 109 + if ($tokens) { 110 + $prev = head($tokens); 111 + while ($prev = $prev->getPrevToken()) { 112 + if ($prev->isAnyWhitespace()) { 113 + continue; 114 + } 115 + break; 116 + } 117 + 118 + if ($prev && $prev->isComment()) { 119 + $value = $prev->getValue(); 120 + $matches = null; 121 + if (preg_match('/@(return|param|task|author)/', $value, $matches)) { 122 + $atom->addWarning( 123 + pht( 124 + 'Atom "%s" is preceded by a comment containing "@%s", but the '. 125 + 'comment is not a documentation comment. Documentation '. 126 + 'comments must begin with "/**", followed by a newline. Did '. 127 + 'you mean to use a documentation comment? (As the comment is '. 128 + 'not a documentation comment, it will be ignored.)', 129 + $atom->getName(), 130 + $matches[1])); 131 + } 132 + } 133 + } 134 + 135 + $atom->setDocblockRaw(''); 136 + return false; 137 + } 138 + } 139 + 140 + protected function parseParamDoc(DivinerAtom $atom, $doc, $name) { 141 + $dict = array(); 142 + $split = preg_split('/\s+/', trim($doc), $limit = 2); 143 + if (!empty($split[0])) { 144 + $dict['doctype'] = $split[0]; 145 + } 146 + 147 + if (!empty($split[1])) { 148 + $docs = $split[1]; 149 + 150 + // If the parameter is documented like "@param int $num Blah blah ..", 151 + // get rid of the `$num` part (which Diviner considers optional). If it 152 + // is present and different from the declared name, raise a warning. 153 + $matches = null; 154 + if (preg_match('/^(\\$\S+)\s+/', $docs, $matches)) { 155 + if ($name !== null) { 156 + if ($matches[1] !== $name) { 157 + $atom->addWarning( 158 + pht( 159 + 'Parameter "%s" is named "%s" in the documentation. The '. 160 + 'documentation may be out of date.', 161 + $name, 162 + $matches[1])); 163 + } 164 + } 165 + $docs = substr($docs, strlen($matches[0])); 166 + } 167 + 168 + $dict['docs'] = $docs; 169 + } 170 + 171 + return $dict; 172 + } 173 + 174 + private function parseReturnType(DivinerAtom $atom, XHPASTNode $decl) { 175 + $return_spec = array(); 176 + 177 + $metadata = $atom->getDocblockMeta(); 178 + $return = idx($metadata, 'return'); 179 + 180 + if (!$return) { 181 + $return = idx($metadata, 'returns'); 182 + if ($return) { 183 + $atom->addWarning( 184 + pht('Documentation uses `@returns`, but should use `@return`.')); 185 + } 186 + } 187 + 188 + if ($atom->getName() == '__construct' && $atom->getType() == 'method') { 189 + $return_spec = array( 190 + 'doctype' => 'this', 191 + 'docs' => '//Implicit.//', 192 + ); 193 + 194 + if ($return) { 195 + $atom->addWarning( 196 + 'Method __construct() has explicitly documented @return. The '. 197 + '__construct() method always returns $this. Diviner documents '. 198 + 'this implicitly.'); 199 + } 200 + } else if ($return) { 201 + $split = preg_split('/\s+/', trim($return), $limit = 2); 202 + if (!empty($split[0])) { 203 + $type = $split[0]; 204 + } 205 + 206 + if ($decl->getChildByIndex(1)->getTypeName() == 'n_REFERENCE') { 207 + $type = $type.' &'; 208 + } 209 + 210 + $docs = null; 211 + if (!empty($split[1])) { 212 + $docs = $split[1]; 213 + } 214 + 215 + $return_spec = array( 216 + 'doctype' => $type, 217 + 'docs' => $docs, 218 + ); 219 + } else { 220 + $return_spec = array( 221 + 'type' => 'wild', 222 + ); 223 + } 224 + 225 + $atom->setProperty('return', $return_spec); 226 + } 227 + 228 + } 229 +
+53 -9
src/applications/diviner/controller/DivinerAtomController.php
··· 33 33 return new Aphront404Response(); 34 34 } 35 35 36 + // TODO: This query won't load ghosts, because they'll fail `needAtoms()`. 37 + // Instead, we might want to load ghosts and render a message like 38 + // "this thing existed in an older version, but no longer does", especially 39 + // if we add content like comments. 40 + 36 41 $symbol = id(new DivinerAtomQuery()) 37 42 ->setViewer($viewer) 38 43 ->withBookPHIDs(array($book->getPHID())) ··· 85 90 pht('Defined'), 86 91 $atom->getFile().':'.$atom->getLine()); 87 92 93 + $warnings = $atom->getWarnings(); 94 + if ($warnings) { 95 + $warnings = id(new AphrontErrorView()) 96 + ->setErrors($warnings) 97 + ->setTitle(pht('Documentation Warnings')) 98 + ->setSeverity(AphrontErrorView::SEVERITY_WARNING); 99 + } 100 + 88 101 $field = 'default'; 89 102 $engine = id(new PhabricatorMarkupEngine()) 90 103 ->setViewer($viewer) ··· 93 106 94 107 $content = $engine->getOutput($symbol, $field); 95 108 109 + if (strlen(trim($symbol->getMarkupText($field)))) { 110 + $content = phutil_tag( 111 + 'div', 112 + array( 113 + 'class' => 'phabricator-remarkup', 114 + ), 115 + array( 116 + $content, 117 + )); 118 + } else { 119 + $undoc = DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType()); 120 + $content = id(new AphrontErrorView()) 121 + ->appendChild($undoc) 122 + ->setSeverity(AphrontErrorView::SEVERITY_NODATA); 123 + } 124 + 125 + 96 126 $toc = $engine->getEngineMetadata( 97 127 $symbol, 98 128 $field, ··· 103 133 ->setBook($book->getTitle(), $group_name) 104 134 ->setHeader($header) 105 135 ->appendChild($properties) 106 - ->appendChild( 107 - phutil_tag( 108 - 'div', 109 - array( 110 - 'class' => 'phabricator-remarkup', 111 - ), 112 - array( 113 - $content, 114 - ))); 136 + ->appendChild($warnings) 137 + ->appendChild($content); 138 + 139 + $parameters = $atom->getProperty('parameters'); 140 + if ($parameters !== null) { 141 + $document->appendChild( 142 + id(new PhabricatorHeaderView()) 143 + ->setHeader(pht('Parameters'))); 144 + 145 + $document->appendChild( 146 + id(new DivinerParameterTableView()) 147 + ->setParameters($parameters)); 148 + } 149 + 150 + $return = $atom->getProperty('return'); 151 + if ($return !== null) { 152 + $document->appendChild( 153 + id(new PhabricatorHeaderView()) 154 + ->setHeader(pht('Return'))); 155 + $document->appendChild( 156 + id(new DivinerReturnTableView()) 157 + ->setReturn($return)); 158 + } 115 159 116 160 if ($toc) { 117 161 $side = new PHUIListView();
+11
src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php
··· 114 114 )); 115 115 } 116 116 117 + // TODO: This probably is not the best place to do this. Move it somewhere 118 + // better when it becomes more clear where it should actually go. 119 + if ($ref) { 120 + switch ($ref->getType()) { 121 + case 'function': 122 + case 'method': 123 + $title = $title.'()'; 124 + break; 125 + } 126 + } 127 + 117 128 if ($this->getEngine()->isTextMode()) { 118 129 if ($href) { 119 130 $link = $title.' <'.PhabricatorEnv::getProductionURI($href).'>';
+1
src/applications/diviner/publisher/DivinerLivePublisher.php
··· 34 34 ->withContexts(array($atom->getContext())) 35 35 ->withIndexes(array($this->getAtomSimilarIndex($atom))) 36 36 ->withIncludeUndocumentable(true) 37 + ->withIncludeGhosts(true) 37 38 ->executeOne(); 38 39 39 40 if ($symbol) {
+30
src/applications/diviner/query/DivinerAtomQuery.php
··· 11 11 private $contexts; 12 12 private $indexes; 13 13 private $includeUndocumentable; 14 + private $includeGhosts; 14 15 15 16 private $needAtoms; 16 17 ··· 51 52 52 53 public function needAtoms($need) { 53 54 $this->needAtoms = $need; 55 + return $this; 56 + } 57 + 58 + /** 59 + * Include "ghosts", which are symbols which used to exist but do not exist 60 + * currently (for example, a function which existed in an older version of 61 + * the codebase but was deleted). 62 + * 63 + * These symbols had PHIDs assigned to them, and may have other sorts of 64 + * metadata that we don't want to lose (like comments or flags), so we don't 65 + * delete them outright. They might also come back in the future: the change 66 + * which deleted the symbol might be reverted, or the documentation might 67 + * have been generated incorrectly by accident. In these cases, we can 68 + * restore the original data. 69 + * 70 + * However, most callers are not interested in these symbols, so they are 71 + * excluded by default. You can use this method to include them in results. 72 + * 73 + * @param bool True to include ghosts. 74 + * @return this 75 + */ 76 + public function withIncludeGhosts($include) { 77 + $this->includeGhosts = $include; 54 78 return $this; 55 79 } 56 80 ··· 188 212 $where[] = qsprintf( 189 213 $conn_r, 190 214 'isDocumentable = 1'); 215 + } 216 + 217 + if (!$this->includeGhosts) { 218 + $where[] = qsprintf( 219 + $conn_r, 220 + 'graphHash IS NOT NULL'); 191 221 } 192 222 193 223 $where[] = $this->buildPagingClause($conn_r);
+64
src/applications/diviner/view/DivinerParameterTableView.php
··· 1 + <?php 2 + 3 + final class DivinerParameterTableView extends AphrontTagView { 4 + 5 + private $parameters; 6 + 7 + public function setParameters(array $parameters) { 8 + $this->parameters = $parameters; 9 + return $this; 10 + } 11 + 12 + public function getTagName() { 13 + return 'table'; 14 + } 15 + 16 + public function getTagAttributes() { 17 + return array( 18 + 'class' => 'diviner-parameter-table-view', 19 + ); 20 + } 21 + 22 + public function getTagContent() { 23 + require_celerity_resource('diviner-shared-css'); 24 + 25 + $rows = array(); 26 + foreach ($this->parameters as $param) { 27 + $cells = array(); 28 + 29 + $type = idx($param, 'doctype'); 30 + if (!$type) { 31 + $type = idx($param, 'type'); 32 + } 33 + 34 + $name = idx($param, 'name'); 35 + $docs = idx($param, 'docs'); 36 + 37 + $cells[] = phutil_tag( 38 + 'td', 39 + array( 40 + 'class' => 'diviner-parameter-table-type diviner-monospace', 41 + ), 42 + $type); 43 + 44 + $cells[] = phutil_tag( 45 + 'td', 46 + array( 47 + 'class' => 'diviner-parameter-table-name diviner-monospace', 48 + ), 49 + $name); 50 + 51 + $cells[] = phutil_tag( 52 + 'td', 53 + array( 54 + 'class' => 'diviner-parameter-table-docs', 55 + ), 56 + $docs); 57 + 58 + $rows[] = phutil_tag('tr', array(), $cells); 59 + } 60 + 61 + return $rows; 62 + } 63 + 64 + }
+53
src/applications/diviner/view/DivinerReturnTableView.php
··· 1 + <?php 2 + 3 + final class DivinerReturnTableView extends AphrontTagView { 4 + 5 + private $return; 6 + 7 + public function setReturn(array $return) { 8 + $this->return = $return; 9 + return $this; 10 + } 11 + 12 + public function getTagName() { 13 + return 'table'; 14 + } 15 + 16 + public function getTagAttributes() { 17 + return array( 18 + 'class' => 'diviner-return-table-view', 19 + ); 20 + } 21 + 22 + public function getTagContent() { 23 + require_celerity_resource('diviner-shared-css'); 24 + 25 + $return = $this->return; 26 + 27 + $type = idx($return, 'doctype'); 28 + if (!$type) { 29 + $type = idx($return, 'type'); 30 + } 31 + 32 + $docs = idx($return, 'docs'); 33 + 34 + $cells = array(); 35 + 36 + $cells[] = phutil_tag( 37 + 'td', 38 + array( 39 + 'class' => 'diviner-return-table-type diviner-monospace', 40 + ), 41 + $type); 42 + 43 + $cells[] = phutil_tag( 44 + 'td', 45 + array( 46 + 'class' => 'diviner-return-table-docs', 47 + ), 48 + $docs); 49 + 50 + return phutil_tag('tr', array(), $cells); 51 + } 52 + 53 + }
+32 -2
src/applications/diviner/workflow/DivinerGenerateWorkflow.php
··· 170 170 171 171 private function getAtomizersForFiles(array $files) { 172 172 $rules = $this->getRules(); 173 + $exclude = $this->getExclude(); 173 174 174 175 $atomizers = array(); 175 176 176 177 foreach ($files as $file) { 178 + foreach ($exclude as $pattern) { 179 + if (preg_match($pattern, $file)) { 180 + continue 2; 181 + } 182 + } 183 + 177 184 foreach ($rules as $rule => $atomizer) { 178 185 $ok = preg_match($rule, $file); 179 186 if ($ok === false) { ··· 191 198 } 192 199 193 200 private function getRules() { 194 - return $this->getConfig('rules', array()) + array( 201 + $rules = $this->getConfig('rules', array( 195 202 '/\\.diviner$/' => 'DivinerArticleAtomizer', 196 - ); 203 + '/\\.php$/' => 'DivinerPHPAtomizer', 204 + )); 205 + 206 + foreach ($rules as $rule => $atomizer) { 207 + if (@preg_match($rule, '') === false) { 208 + throw new Exception( 209 + "Rule '{$rule}' is not a valid regular expression!"); 210 + } 211 + } 212 + 213 + return $rules; 214 + } 215 + 216 + private function getExclude() { 217 + $exclude = $this->getConfig('exclude', array()); 218 + 219 + foreach ($exclude as $rule) { 220 + if (@preg_match($rule, '') === false) { 221 + throw new Exception( 222 + "Exclude rule '{$rule}' is not a valid regular expression!"); 223 + } 224 + } 225 + 226 + return $exclude; 197 227 } 198 228 199 229
+17
src/docs/book/phabricator.book
··· 1 + { 2 + "name" : "phabdev", 3 + "title" : "Phabricator Technical Documentation", 4 + "short" : "Phabricator Tech Docs", 5 + "root" : "../../../", 6 + "rules" : { 7 + "(\\.php$)" : "DivinerPHPAtomizer" 8 + }, 9 + "exclude" : [ 10 + "(^externals/)", 11 + "(^scripts/)", 12 + "(^support/)", 13 + "(^resources/)" 14 + ], 15 + "groups" : { 16 + } 17 + }
+8
src/docs/book/user.book
··· 3 3 "title" : "Phabricator User Documentation", 4 4 "short" : "Phabricator User Docs", 5 5 "root" : "../../../", 6 + "rules" : { 7 + "(\\.diviner$)" : "DivinerArticleAtomizer" 8 + }, 9 + "exclude" : [ 10 + "(^externals/)", 11 + "(^scripts/)", 12 + "(^support/)" 13 + ], 6 14 "groups" : { 7 15 "intro" : { 8 16 "name" : "Introduction"
+38
webroot/rsrc/css/diviner/diviner-shared.css
··· 1 + /** 2 + * @provides diviner-shared-css 3 + */ 4 + 5 + .diviner-monospace { 6 + font-family: monospace; 7 + font-size: 13px; 8 + } 9 + 10 + .diviner-return-table-view, 11 + .diviner-parameter-table-view { 12 + width: 100%; 13 + margin: 0 0 16px; 14 + background: #f6f6f6; 15 + border-bottom: 1px solid #c0c5d1; 16 + width: 100%; 17 + } 18 + 19 + .diviner-return-table-type, 20 + .diviner-parameter-table-type { 21 + padding: 4px 8px 4px 12px; 22 + white-space: nowrap; 23 + text-align: right; 24 + color: #666666; 25 + width: 20%; 26 + } 27 + 28 + .diviner-parameter-table-name { 29 + padding: 4px 8px; 30 + white-space: nowrap; 31 + font-weight: bold; 32 + } 33 + 34 + .diviner-return-table-docs, 35 + .diviner-parameter-table-docs { 36 + padding: 4px 12px 4px 8px; 37 + width: 80%; 38 + }