@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.

Improve Diviner linking

Summary:
Do this somewhat reasonably:

- For links to the same documentation book (the common case), go look up that the thing you're linking to actualy exists. If it doesn't, render a <span> which we can make have a red background and warn about later.
- For links to some other book, just generate a link and hope it hits something. We can improve and augment this later.
- For non-documentation links (links in comments, e.g.) just generate a query link into the Diviner app. We'll do a query and figure out where to send the user after they click the link. We could pre-resolve these later.

Test Plan: Generated documentation, saw it build mostly-correct links when objects were referenced correctly. Used preview to generate various `@{x:y|z}` things and made sure they ended up reasonable-looking.

Reviewers: chad

Reviewed By: chad

CC: aran

Maniphest Tasks: T988

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

+276 -24
+3
src/applications/diviner/atom/DivinerAtom.php
··· 285 285 286 286 public function getRef() { 287 287 $group = null; 288 + $title = null; 288 289 if ($this->docblockMeta) { 289 290 $group = $this->getDocblockMetaValue('group'); 291 + $title = $this->getDocblockMetaValue('title'); 290 292 } 291 293 292 294 return id(new DivinerAtomRef()) ··· 294 296 ->setContext($this->getContext()) 295 297 ->setType($this->getType()) 296 298 ->setName($this->getName()) 299 + ->setTitle($title) 297 300 ->setGroup($group); 298 301 } 299 302
+25 -8
src/applications/diviner/atom/DivinerAtomRef.php
··· 9 9 private $group; 10 10 private $summary; 11 11 private $index; 12 + private $title; 12 13 13 14 public function getSortKey() { 14 15 return implode( ··· 47 48 "Atom names must not be in the form '/@\d+/'. This pattern is ". 48 49 "reserved for disambiguating atoms with similar names."); 49 50 } 50 - $this->name = $name; 51 + $this->name = $normal_name; 51 52 return $this; 52 53 } 53 54 ··· 78 79 } 79 80 80 81 public function setBook($book) { 81 - $this->book = self::normalizeString($book); 82 + if ($book === null) { 83 + $this->book = $book; 84 + } else { 85 + $this->book = self::normalizeString($book); 86 + } 82 87 return $this; 83 88 } 84 89 ··· 95 100 return $this->group; 96 101 } 97 102 103 + public function setTitle($title) { 104 + $this->title = $title; 105 + return $this; 106 + } 107 + 108 + public function getTitle() { 109 + return $this->title; 110 + } 111 + 98 112 public function toDictionary() { 99 113 return array( 100 114 'book' => $this->getBook(), ··· 104 118 'group' => $this->getGroup(), 105 119 'index' => $this->getIndex(), 106 120 'summary' => $this->getSummary(), 121 + 'title' => $this->getTitle(), 107 122 ); 108 123 } 109 124 ··· 113 128 unset($dict['group']); 114 129 unset($dict['index']); 115 130 unset($dict['summary']); 131 + unset($dict['title']); 116 132 117 133 ksort($dict); 118 134 return md5(serialize($dict)).'S'; ··· 120 136 121 137 public static function newFromDictionary(array $dict) { 122 138 $obj = new DivinerAtomRef(); 123 - $obj->book = idx($dict, 'book'); 124 - $obj->context = idx($dict, 'context'); 125 - $obj->type = idx($dict, 'type'); 126 - $obj->name = idx($dict, 'name'); 139 + $obj->setBook(idx($dict, 'book')); 140 + $obj->setContext(idx($dict, 'context')); 141 + $obj->setType(idx($dict, 'type')); 142 + $obj->setName(idx($dict, 'name')); 127 143 $obj->group = idx($dict, 'group'); 128 144 $obj->index = idx($dict, 'index'); 129 145 $obj->summary = idx($dict, 'summary'); 146 + $obj->title = idx($dict, 'title'); 130 147 return $obj; 131 148 } 132 149 ··· 163 180 // Replace all spaces with underscores. 164 181 $str = preg_replace('/ +/', '_', $str); 165 182 166 - // Replace control characters with "@". 167 - $str = preg_replace('/[\x00-\x19]/', '@', $str); 183 + // Replace control characters with "X". 184 + $str = preg_replace('/[\x00-\x19]/', 'X', $str); 168 185 169 186 // Replace specific problematic names with alternative names. 170 187 $alternates = array(
+108 -13
src/applications/diviner/markup/DivinerRemarkupRuleSymbol.php
··· 2 2 3 3 final class DivinerRemarkupRuleSymbol extends PhutilRemarkupRule { 4 4 5 + const KEY_RULE_ATOM_REF = 'rule.diviner.atomref'; 6 + 5 7 public function apply($text) { 8 + // Grammar here is: 9 + // 10 + // rule = '@{' maybe_type name maybe_title '}' 11 + // maybe_type = null | type ':' | type '@' book ':' 12 + // name = name | name '@' context 13 + // maybe_title = null | '|' title 14 + // 15 + // So these are all valid: 16 + // 17 + // @{name} 18 + // @{type : name} 19 + // @{name | title} 20 + // @{type @ book : name @ context | title} 21 + 6 22 return $this->replaceHTML( 7 - '/(?:^|\B)@{(?:(?P<type>[^:]+?):)?(?P<name>[^}]+?)}/', 23 + '/(?:^|\B)@{'. 24 + '(?:(?P<type>[^:]+?):)?'. 25 + '(?P<name>[^}|]+?)'. 26 + '(?:[|](?P<title>[^}]+))?'. 27 + '}/', 8 28 array($this, 'markupSymbol'), 9 29 $text); 10 30 } 11 31 12 32 public function markupSymbol($matches) { 13 - $type = $matches['type']; 14 - $name = $matches['name']; 33 + $type = (string)idx($matches, 'type'); 34 + $name = (string)$matches['name']; 35 + $title = (string)idx($matches, 'title'); 15 36 16 37 // Collapse sequences of whitespace into a single space. 17 - $name = preg_replace('/\s+/', ' ', $name); 38 + $type = preg_replace('/\s+/', ' ', trim($type)); 39 + $name = preg_replace('/\s+/', ' ', trim($name)); 40 + $title = preg_replace('/\s+/', ' ', trim($title)); 41 + 42 + $ref = array(); 18 43 19 - $book = null; 20 44 if (strpos($type, '@') !== false) { 21 45 list($type, $book) = explode('@', $type, 2); 46 + $ref['type'] = trim($type); 47 + $ref['book'] = trim($book); 48 + } else { 49 + $ref['type'] = $type; 50 + } 51 + 52 + if (strpos($name, '@') !== false) { 53 + list($name, $context) = explode('@', $name, 2); 54 + $ref['name'] = trim($name); 55 + $ref['context'] = trim($context); 56 + } else { 57 + $ref['name'] = $name; 22 58 } 23 59 24 - // TODO: This doesn't actually do anything useful yet. 60 + $ref['title'] = $title; 61 + 62 + foreach ($ref as $key => $value) { 63 + if ($value === '') { 64 + unset($ref[$key]); 65 + } 66 + } 67 + 68 + $engine = $this->getEngine(); 69 + $token = $engine->storeText(''); 70 + 71 + $key = self::KEY_RULE_ATOM_REF; 72 + $data = $engine->getTextMetadata($key, array()); 73 + $data[$token] = $ref; 74 + $engine->setTextMetadata($key, $data); 25 75 26 - $link = phutil_tag( 27 - 'a', 28 - array( 29 - 'href' => '#', 30 - ), 31 - $name); 76 + return $token; 77 + } 78 + 79 + public function didMarkupText() { 80 + $engine = $this->getEngine(); 32 81 33 - return $this->getEngine()->storeText($link); 82 + $key = self::KEY_RULE_ATOM_REF; 83 + $data = $engine->getTextMetadata($key, array()); 84 + 85 + $renderer = $engine->getConfig('diviner.renderer'); 86 + 87 + foreach ($data as $token => $ref_dict) { 88 + $ref = DivinerAtomRef::newFromDictionary($ref_dict); 89 + $title = nonempty($ref->getTitle(), $ref->getName()); 90 + 91 + $href = null; 92 + if ($renderer) { 93 + // Here, we're generating documentation. If possible, we want to find 94 + // the real atom ref so we can render the correct default title and 95 + // render invalid links in an alternate style. 96 + 97 + $ref = $renderer->normalizeAtomRef($ref); 98 + if ($ref) { 99 + $title = nonempty($ref->getTitle(), $ref->getName()); 100 + $href = $renderer->getHrefForAtomRef($ref); 101 + } 102 + } else { 103 + // Here, we're generating commment text or something like that. Just 104 + // link to Diviner and let it sort things out. 105 + 106 + $href = id(new PhutilURI('/diviner/find/')) 107 + ->setQueryParams($ref_dict + array('jump' => true)); 108 + } 109 + 110 + if ($href) { 111 + $link = phutil_tag( 112 + 'a', 113 + array( 114 + 'class' => 'atom-ref', 115 + 'href' => $href, 116 + ), 117 + $title); 118 + } else { 119 + $link = phutil_tag( 120 + 'span', 121 + array( 122 + 'class' => 'atom-ref-invalid', 123 + ), 124 + $title); 125 + } 126 + 127 + $engine->overwriteStoredText($token, $link); 128 + } 34 129 } 35 130 36 131 }
+2
src/applications/diviner/publisher/DivinerPublisher.php
··· 10 10 private $symbolReverseMap; 11 11 12 12 public function setRenderer(DivinerRenderer $renderer) { 13 + $renderer->setPublisher($this); 13 14 $this->renderer = $renderer; 14 15 return $this; 15 16 } ··· 103 104 abstract protected function loadAllPublishedHashes(); 104 105 abstract protected function deleteDocumentsByHash(array $hashes); 105 106 abstract protected function createDocumentsByHash(array $hashes); 107 + abstract public function findAtomByRef(DivinerAtomRef $ref); 106 108 107 109 final public function publishAtoms(array $hashes) { 108 110 $existing = $this->loadAllPublishedHashes();
+43
src/applications/diviner/publisher/DivinerStaticPublisher.php
··· 3 3 final class DivinerStaticPublisher extends DivinerPublisher { 4 4 5 5 private $publishCache; 6 + private $atomNameMap; 6 7 7 8 private function getPublishCache() { 8 9 if (!$this->publishCache) { ··· 113 114 )); 114 115 115 116 Filesystem::writeFile($path, $content); 117 + } 118 + 119 + public function findAtomByRef(DivinerAtomRef $ref) { 120 + if ($ref->getBook() != $this->getConfig('name')) { 121 + return null; 122 + } 123 + 124 + if ($this->atomNameMap === null) { 125 + $name_map = array(); 126 + foreach ($this->getPublishCache()->getIndex() as $hash => $dict) { 127 + $name_map[$dict['name']][$hash] = $dict; 128 + } 129 + $this->atomNameMap = $name_map; 130 + } 131 + 132 + $name = $ref->getName(); 133 + if (empty($this->atomNameMap[$name])) { 134 + return null; 135 + } 136 + 137 + $candidates = $this->atomNameMap[$name]; 138 + foreach ($candidates as $key => $dict) { 139 + $candidates[$key] = DivinerAtomRef::newFromDict($dict); 140 + if ($ref->getType()) { 141 + if ($candidates[$key]->getType() != $ref->getType()) { 142 + unset($candidates[$key]); 143 + } 144 + } 145 + 146 + if ($ref->getContext()) { 147 + if ($candidates[$key]->getContext() != $ref->getContext()) { 148 + unset($candidates[$key]); 149 + } 150 + } 151 + } 152 + 153 + // If we have exactly one uniquely identifiable atom, return it. 154 + if (count($candidates) == 1) { 155 + return $this->getAtomFromNodeHash(last_key($candidates)); 156 + } 157 + 158 + return null; 116 159 } 117 160 118 161 private function addAtomToIndex($hash, DivinerAtom $atom) {
+64 -3
src/applications/diviner/renderer/DivinerDefaultRenderer.php
··· 91 91 protected function renderAtomDescription(DivinerAtom $atom) { 92 92 $text = $this->getAtomDescription($atom); 93 93 $engine = $this->getBlockMarkupEngine(); 94 + 95 + $this->pushAtomStack($atom); 96 + $description = $engine->markupText($text); 97 + $this->popAtomStack($atom); 98 + 94 99 return phutil_tag( 95 100 'div', 96 101 array( 97 102 'class' => 'atom-description', 98 103 ), 99 - $engine->markupText($text)); 104 + $description); 100 105 } 101 106 102 107 protected function getAtomDescription(DivinerAtom $atom) { ··· 106 111 public function renderAtomSummary(DivinerAtom $atom) { 107 112 $text = $this->getAtomSummary($atom); 108 113 $engine = $this->getInlineMarkupEngine(); 114 + 115 + $this->pushAtomStack($atom); 116 + $summary = $engine->markupText($text); 117 + $this->popAtomStack(); 118 + 109 119 return phutil_tag( 110 120 'span', 111 121 array( 112 122 'class' => 'atom-summary', 113 123 ), 114 - $engine->markupText($text)); 124 + $summary); 115 125 } 116 126 117 127 protected function getAtomSummary(DivinerAtom $atom) { ··· 172 182 } 173 183 174 184 protected function getBlockMarkupEngine() { 175 - return PhabricatorMarkupEngine::newMarkupEngine( 185 + $engine = PhabricatorMarkupEngine::newMarkupEngine( 176 186 array( 177 187 'preserve-linebreaks' => false, 178 188 )); 189 + $engine->setConfig('diviner.renderer', $this); 190 + return $engine; 179 191 } 180 192 181 193 protected function getInlineMarkupEngine() { 182 194 return $this->getBlockMarkupEngine(); 183 195 } 184 196 197 + public function normalizeAtomRef(DivinerAtomRef $ref) { 198 + if (!strlen($ref->getBook())) { 199 + $ref->setBook($this->getConfig('name')); 200 + } 201 + 202 + if ($ref->getBook() != $this->getConfig('name')) { 203 + // If the ref is from a different book, we can't normalize it. Just return 204 + // it as-is if it has enough information to resolve. 205 + if ($ref->getName() && $ref->getType()) { 206 + return $ref; 207 + } else { 208 + return null; 209 + } 210 + } 211 + 212 + $atom = $this->getPublisher()->findAtomByRef($ref); 213 + if ($atom) { 214 + return $atom->getRef(); 215 + } 216 + 217 + return null; 218 + } 219 + 220 + protected function getAtomHrefDepth(DivinerAtom $atom) { 221 + if ($atom->getContext()) { 222 + return 4; 223 + } else { 224 + return 3; 225 + } 226 + } 227 + 228 + public function getHrefForAtomRef(DivinerAtomRef $ref) { 229 + $atom = $this->peekAtomStack(); 230 + $depth = $this->getAtomHrefDepth($atom); 231 + $href = str_repeat('../', $depth); 232 + 233 + $book = $ref->getBook(); 234 + $type = $ref->getType(); 235 + $name = $ref->getName(); 236 + $context = $ref->getContext(); 237 + 238 + $href .= $book.'/'.$type.'/'; 239 + if ($context !== null) { 240 + $href .= $context.'/'; 241 + } 242 + $href .= $name.'/'; 243 + 244 + return $href; 245 + } 185 246 186 247 }
+31
src/applications/diviner/renderer/DivinerRenderer.php
··· 2 2 3 3 abstract class DivinerRenderer { 4 4 5 + private $publisher; 6 + private $atomStack; 7 + 8 + public function setPublisher($publisher) { 9 + $this->publisher = $publisher; 10 + return $this; 11 + } 12 + 13 + public function getPublisher() { 14 + return $this->publisher; 15 + } 16 + 17 + public function getConfig($key, $default = null) { 18 + return $this->getPublisher()->getConfig($key, $default); 19 + } 20 + 21 + protected function pushAtomStack(DivinerAtom $atom) { 22 + $this->atomStack[] = $atom; 23 + return $this; 24 + } 25 + 26 + protected function peekAtomStack() { 27 + return end($this->atomStack); 28 + } 29 + 30 + protected function popAtomStack() { 31 + array_pop($this->atomStack); 32 + return $this; 33 + } 34 + 5 35 abstract public function renderAtom(DivinerAtom $atom); 6 36 abstract public function renderAtomSummary(DivinerAtom $atom); 7 37 abstract public function renderAtomIndex(array $refs); 38 + abstract public function getHrefForAtomRef(DivinerAtomRef $ref); 8 39 9 40 }