diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php index 21779c8a63..02de0a92d0 100644 --- a/src/applications/diviner/controller/DivinerAtomController.php +++ b/src/applications/diviner/controller/DivinerAtomController.php @@ -1,84 +1,148 @@ 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(); } - $atom = id(new DivinerAtomQuery()) + $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) ->executeOne(); - if (!$atom) { + if (!$symbol) { return new Aphront404Response(); } + $atom = $symbol->getAtom(); + $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( id(new PhabricatorCrumbView()) - ->setName($book->getName()) + ->setName($book->getShortTitle()) ->setHref('/book/'.$book->getName().'/')); + $atom_short_title = $atom->getDocblockMetaValue( + 'short', + $symbol->getTitle()); + $crumbs->addCrumb( id(new PhabricatorCrumbView()) - ->setName($atom->getName())); + ->setName($atom_short_title)); + + $header = id(new PhabricatorHeaderView()) + ->setHeader($symbol->getTitle()) + ->addTag( + id(new PhabricatorTagView()) + ->setType(PhabricatorTagView::TYPE_STATE) + ->setBackgroundColor(PhabricatorTagView::COLOR_BLUE) + ->setName($this->renderAtomTypeName($atom->getType()))); + + $properties = id(new PhabricatorPropertyListView()); + + $group = $atom->getDocblockMetaValue('group'); + if ($group) { + $group_name = $book->getGroupName($group); + } else { + $group_name = null; + } + + $properties->addProperty( + pht('Defined'), + $atom->getFile().':'.$atom->getLine()); + + $field = 'default'; + $engine = id(new PhabricatorMarkupEngine()) + ->setViewer($viewer) + ->addObject($symbol, $field) + ->process(); - $header = id(new PhabricatorHeaderView())->setHeader($atom->getName()); + $content = $engine->getOutput($symbol, $field); + + $toc = $engine->getEngineMetadata( + $symbol, + $field, + PhutilRemarkupEngineRemarkupHeaderBlockRule::KEY_HEADER_TOC, + array()); $document = id(new PHUIDocumentView()) + ->setBook($book->getTitle(), $group_name) + ->setHeader($header) + ->appendChild($properties) ->appendChild( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), - phutil_safe_html($atom->getContent()))); + array( + $content, + ))); + + 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); + } return $this->buildApplicationPage( array( $crumbs, $document, ), array( - 'title' => $atom->getName(), + 'title' => $symbol->getTitle(), 'dust' => true, 'device' => true, )); } + private function renderAtomTypeName($name) { + return phutil_utf8_ucwords($name); + } + } diff --git a/src/applications/diviner/controller/DivinerBookController.php b/src/applications/diviner/controller/DivinerBookController.php index 18bcfa3b9d..31c21c78b7 100644 --- a/src/applications/diviner/controller/DivinerBookController.php +++ b/src/applications/diviner/controller/DivinerBookController.php @@ -1,112 +1,100 @@ bookName = $data['book']; } 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(); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( id(new PhabricatorCrumbView()) - ->setName($book->getTitle()) + ->setName($book->getShortTitle()) ->setHref('/book/'.$book->getName().'/')); $header = id(new PhabricatorHeaderView())->setHeader($book->getTitle()); $properties = $this->buildPropertyList($book); $atoms = id(new DivinerAtomQuery()) ->setViewer($viewer) ->withBookPHIDs(array($book->getPHID())) ->execute(); $atoms = msort($atoms, 'getSortKey'); $group_spec = $book->getConfig('groups'); if (!is_array($group_spec)) { $group_spec = array(); } $groups = mgroup($atoms, 'getGroupName'); $groups = array_select_keys($groups, array_keys($group_spec)) + $groups; if (isset($groups[''])) { $no_group = $groups['']; unset($groups['']); $groups[''] = $no_group; } $out = array(); foreach ($groups as $group => $atoms) { - $group_info = idx($group_spec, $group); - if (!is_array($group_info)) { - $group_info = array(); - } - - $group_name = idx($group_info, 'name'); - if (!strlen($group_name)) { - if (strlen($group)) { - $group_name = $group; - } else { - $group_name = pht('Free Radicals'); - } - } + $group_name = $book->getGroupName($group); $out[] = id(new PhabricatorHeaderView()) ->setHeader($group_name); $out[] = $this->renderAtomList($atoms); } return $this->buildApplicationPage( array( $crumbs, $header, $properties, $out, ), array( 'title' => $book->getTitle(), 'dust' => true, 'device' => true, )); } private function buildPropertyList(DivinerLiveBook $book) { $user = $this->getRequest()->getUser(); $view = id(new PhabricatorPropertyListView()) ->setUser($user); $policies = PhabricatorPolicyQuery::renderPolicyDescriptions( $user, $book); $view->addProperty( pht('Visible To'), $policies[PhabricatorPolicyCapability::CAN_VIEW]); $view->addProperty( pht('Updated'), phabricator_datetime($book->getDateModified(), $user)); return $view; } } diff --git a/src/applications/diviner/publisher/DivinerLivePublisher.php b/src/applications/diviner/publisher/DivinerLivePublisher.php index 9e1c7b6e8b..317739180f 100644 --- a/src/applications/diviner/publisher/DivinerLivePublisher.php +++ b/src/applications/diviner/publisher/DivinerLivePublisher.php @@ -1,139 +1,137 @@ 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) ->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 DivinerLiveSymbol())->loadAllWhere( 'bookPHID = %s AND graphHash IS NOT NULL', $this->loadBook()->getPHID()); 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 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()); if ($is_documentable) { - $renderer = $this->getRenderer(); - $content = $renderer->renderAtom($atom); - $storage = $this->loadAtomStorageForSymbol($symbol) ->setAtomData($atom->toDictionary()) - ->setContent((string)phutil_safe_html($content)) + ->setContent(null) ->save(); + $renderer = $this->getRenderer(); $summary = $renderer->renderAtomSummary($atom); $summary = (string)phutil_safe_html($summary); $symbol->setSummary($summary); } $symbol->save(); } } public function findAtomByRef(DivinerAtomRef $ref) { // TODO: Actually implement this. return null; } } diff --git a/src/applications/diviner/renderer/DivinerDefaultRenderer.php b/src/applications/diviner/renderer/DivinerDefaultRenderer.php index 0cc129437a..c6d7b35806 100644 --- a/src/applications/diviner/renderer/DivinerDefaultRenderer.php +++ b/src/applications/diviner/renderer/DivinerDefaultRenderer.php @@ -1,263 +1,264 @@ 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', ), $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', ), $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( $this->renderAtomRefLink($ref), ' - ', $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() { - $engine = PhabricatorMarkupEngine::newMarkupEngine( - array( - 'preserve-linebreaks' => false, - )); + $engine = PhabricatorMarkupEngine::newMarkupEngine(array()); + + $engine->setConfig('preserve-linebreaks', false); $engine->setConfig('viewer', new PhabricatorUser()); $engine->setConfig('diviner.renderer', $this); + $engine->setConfig('header.generate-toc', true); + 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) { $depth = 1; $atom = $this->peekAtomStack(); if ($atom) { $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.'/index.html'; return $href; } protected function renderAtomRefLink(DivinerAtomRef $ref) { return phutil_tag( 'a', array( 'href' => $this->getHrefForAtomRef($ref), ), $ref->getTitle()); } } diff --git a/src/applications/diviner/storage/DivinerLiveAtom.php b/src/applications/diviner/storage/DivinerLiveAtom.php index f682c15d6d..933d81bcad 100644 --- a/src/applications/diviner/storage/DivinerLiveAtom.php +++ b/src/applications/diviner/storage/DivinerLiveAtom.php @@ -1,18 +1,19 @@ false, self::CONFIG_SERIALIZATION => array( + 'content' => self::SERIALIZATION_JSON, 'atomData' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } } diff --git a/src/applications/diviner/storage/DivinerLiveBook.php b/src/applications/diviner/storage/DivinerLiveBook.php index 2155a5b7e4..25e2cbaf49 100644 --- a/src/applications/diviner/storage/DivinerLiveBook.php +++ b/src/applications/diviner/storage/DivinerLiveBook.php @@ -1,54 +1,64 @@ true, self::CONFIG_SERIALIZATION => array( 'configurationData' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function getConfig($key, $default = null) { return idx($this->configurationData, $key, $default); } public function setConfig($key, $value) { $this->configurationData[$key] = $value; return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_BOOK); } public function getTitle() { return $this->getConfig('title', $this->getName()); } + public function getShortTitle() { + return $this->getConfig('short', $this->getTitle()); + } + + public function getGroupName($group) { + $groups = $this->getConfig('groups'); + $spec = idx($groups, $group, array()); + return idx($spec, 'name', pht('Free Radicals')); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->viewPolicy; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } } diff --git a/src/applications/diviner/storage/DivinerLiveSymbol.php b/src/applications/diviner/storage/DivinerLiveSymbol.php index 06dee25667..aee5aa3203 100644 --- a/src/applications/diviner/storage/DivinerLiveSymbol.php +++ b/src/applications/diviner/storage/DivinerLiveSymbol.php @@ -1,138 +1,166 @@ true, self::CONFIG_TIMESTAMPS => false, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_ATOM); } public function getBook() { if ($this->book === null) { throw new Exception("Call attachBook() before getBook()!"); } return $this->book; } public function attachBook(DivinerLiveBook $book) { $this->book = $book; return $this; } - public function getContent() { - if ($this->content === null) { - throw new Exception("Call attachAtom() before getContent()!"); - } - return $this->content; - } - public function getAtom() { if ($this->atom === null) { throw new Exception("Call attachAtom() before getAtom()!"); } return $this->atom; } public function attachAtom(DivinerLiveAtom $atom) { - $this->content = $atom->getContent(); $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; } /* -( 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 true; + } + } diff --git a/src/docs/book/user.book b/src/docs/book/user.book index a8f23f2cf3..e0c062fae5 100644 --- a/src/docs/book/user.book +++ b/src/docs/book/user.book @@ -1,34 +1,35 @@ { "name" : "phabricator", "title" : "Phabricator User Documentation", + "short" : "Phabricator User Docs", "root" : "../../../", "groups" : { "intro" : { "name" : "Introduction" }, "config" : { "name" : "Configuration" }, "userguide" : { "name" : "Application User Guides" }, "differential" : { "name" : "Differential (Code Review)" }, "diffusion" : { "name" : "Diffusion (Repository Browser)" }, "maniphest" : { "name" : "Maniphest (Task Tracking)" }, "slowvote" : { "name" : "Slowvote (Polls)" }, "herald" : { "name" : "Herald (Notifications)" }, "phriction" : { "name" : "Phriction (Wiki)" } } } diff --git a/src/docs/feedback.diviner b/src/docs/feedback.diviner index 4caf6c31db..216482030d 100644 --- a/src/docs/feedback.diviner +++ b/src/docs/feedback.diviner @@ -1,148 +1,149 @@ @title Give Feedback! Get Support! +@short Feedback/Support @group intro How to give us feedback, report bugs, and request features, and get support for problems with Phabricator. = Overview = We'd love to hear your feedback about Phabricator, whether it's good or bad. We stay on top of bug reports and fix many of them within a day or two (and sometimes within hours). The Phabricator roadmap is determined in large part by user feedback and feature requests. Your feedback matters, will often have an immediate short-term impact, and the project leads are actively listening to it. We also try to provide a very high level of free support. If you have trouble with anything or just don't understand how something works, ask us! We're happy to help, and it's usually valuable for us because we can prevent the problem in the code (or document it better) so future users don't hit it. Some day we will no doubt grow callous and distant, but for now the community is small enough that we can provide a high level of service and support to everyone and still have plenty of time to write code. If you're in the SF bay area, we're also happy to come onsite and help you set things up, answer any questions you might have, or just hang out and tell Facebook war stories. The best ways to provide feedback are: = Maniphest = The best way to report bugs and request features is through [[http://secure.phabricator.com/maniphest/task/create/ | Maniphest]]. Just file the bug/request and we'll handle everything else. (If it's time-sensitive or blocking you, feel free to assign it to `epriestley`.) Feel free to file support requests, general questions, or random feedback this way, too. = GitHub Issues = You can also use [[https://github.com/facebook/phabricator/issues/new | GitHub Issues]] if you prefer. = IRC = We're active in #phabricator on FreeNode, and it's the best place to ask questions and get support. = Email = You can email us at `btrahan@phacility.com` and `epriestley@phacility.com`. = Filing Good Feature Requests = When filing a feature request, please provide as much information as possible, especially about what your use case is and why you want the feature. Explaining what your larger goals are is very helpful, and lets us design better features. Tell us what your problem is before you tell us your idea to solve it: sometimes we can come up with a better approach to the problem, a slightly different approach that solves more problems or helps other users, or a way to make the problem go away entirely. = Filing Good Bug Reports = When filing a bug report, please provide as much information as possible. In particular: - If you received an error message, please please please provide it! It is often incredibly useful. See @{article:Please Please Please}. - The second most useful thing to us is reproduction steps. If at all possible, provide a brief list of steps required to reproduce the problem. - If something you didn't expect (other than a crash or obvious error) happened, tell us what you expected and what actually happened. We can fix clearly-described bugs with reproduction steps and error messages much more quickly than vague bugs we can't reproduce. In particular, if you don't include error messages, we need to ask you for error messages. We will nearly always need to do this, and nearly always be unable to continue until you provide them. You'll save everyone time if you spend a few extra seconds copy/pasting them in the first place. Generally, err on the side of giving us too much information. It's much quicker and easier for us to filter out information that isn't relevant than it is to go back and forth asking you to provide more details. Tips: - For issues with `arc`, you can get more information by running the command with the `--trace` flag. For instance, run `arc diff --trace` instead of `arc diff`. Including this output in your bug report is often helpful. - For issues with Phabricator, check your webserver error logs for more information. For Apache, this is usually `/var/log/httpd/error.log` or `/var/log/apache2/error.log`. - For issues with the UI, check your Javascript error console in your browser. - Some other things, like daemons, have debug flags or troubleshooting steps covered in their documentation. Enabling these flags may give you more information about the problem. = Unreproducible Problems = Before we can fix a bug, we need to reproduce it. If we can't reproduce a problem, we can't tell if we've fixed it and often won't be able to figure out why it is occurring. Most problems reproduce easily, but some are more difficult to reproduce. We will generally make a reasonable effort to reproduce problems, but sometimes we will be unable to reproduce an issue. Many of these unreproducible issues turn out to be bizarre environmental problems that are unique to one user's install, and figuring out what is wrong takes a very long time with a lot of back and forth as we ask questions to narrow down the cause of the problem. When we eventually figure it out and fix it, few others benefit (in some cases, no one else). This sort of fishing expedition is not a good use of anyone's time, and it's very hard for us to prioritize solving these problems because they represent a huge effort for very little benefit. These problems are a tiny fraction of requests (maybe 1-2%) but take up the vast majority of our support effort (maybe 80%). **If you want us to fix a bug we can't reproduce, you need to build us a working reproduction case.** Generally, this means a `root` login on a machine where the issue occurs. For example: - Bring up a new machine (e.g., in EC2), install Phabricator on it, configure it so the problem is reproducible, and then give us access to it. - If the machine Phabricator is running on is nonessential/nonsensitive and you're comfortable with us having root on it, give us credentials. - If the issue is with Git, SVN or Mercurial, create a new empty repository, add a commit or series of commits which replicate the issue, and give us a zip/tarball of the repository as a reproduction case. - If you're in the San Francisco bay area, we can probably swing by and fix the issue onsite. These are all fairly heavyweight and will take some time, but often //less// of your time than a fishing expedition. If you can build a reproduction case, there is a very high chance we can resolve your problem quickly. Alternatively, you can pay us an enormous pile of money for some kind of enterprise support thing and we'd be thrilled to go fishing with you for as long as you remain solvent. Email us (see above) for specifics. = Next Steps = Continue by: - Filing a bug of feature request in [[http://secure.phabricator.com/maniphest/task/create/ | Maniphest]]; or - contributing to Phabricator with @{article:Contributor Introduction}. diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index a117112a0e..f929c111fd 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -1,566 +1,597 @@ addObject($comment, $field); * } * * Now, call @{method:process} to perform the actual cache/rendering * step. This is a heavyweight call which does batched data access and * transforms the markup into output. * * $engine->process(); * * Finally, do something with the results: * * $results = array(); * foreach ($comments as $comment) { * $results[] = $engine->getOutput($comment, $field); * } * * If you have a single object to render, you can use the convenience method * @{method:renderOneObject}. * * @task markup Markup Pipeline * @task engine Engine Construction */ final class PhabricatorMarkupEngine { private $objects = array(); private $viewer; private $version = 7; /* -( Markup Pipeline )---------------------------------------------------- */ /** * Convenience method for pushing a single object through the markup * pipeline. * * @param PhabricatorMarkupInterface The object to render. * @param string The field to render. * @param PhabricatorUser User viewing the markup. * @return string Marked up output. * @task markup */ public static function renderOneObject( PhabricatorMarkupInterface $object, $field, PhabricatorUser $viewer) { return id(new PhabricatorMarkupEngine()) ->setViewer($viewer) ->addObject($object, $field) ->process() ->getOutput($object, $field); } /** * Queue an object for markup generation when @{method:process} is * called. You can retrieve the output later with @{method:getOutput}. * * @param PhabricatorMarkupInterface The object to render. * @param string The field to render. * @return this * @task markup */ public function addObject(PhabricatorMarkupInterface $object, $field) { $key = $this->getMarkupFieldKey($object, $field); $this->objects[$key] = array( 'object' => $object, 'field' => $field, ); return $this; } /** * Process objects queued with @{method:addObject}. You can then retrieve * the output with @{method:getOutput}. * * @return this * @task markup */ public function process() { $keys = array(); foreach ($this->objects as $key => $info) { if (!isset($info['markup'])) { $keys[] = $key; } } if (!$keys) { return; } $objects = array_select_keys($this->objects, $keys); // Build all the markup engines. We need an engine for each field whether // we have a cache or not, since we still need to postprocess the cache. $engines = array(); foreach ($objects as $key => $info) { $engines[$key] = $info['object']->newMarkupEngine($info['field']); $engines[$key]->setConfig('viewer', $this->viewer); } // Load or build the preprocessor caches. $blocks = $this->loadPreprocessorCaches($engines, $objects); + $blocks = mpull($blocks, 'getCacheData'); + + $this->engineCaches = $blocks; // Finalize the output. foreach ($objects as $key => $info) { - $data = $blocks[$key]->getCacheData(); $engine = $engines[$key]; $field = $info['field']; $object = $info['object']; - $output = $engine->postprocessText($data); + $output = $engine->postprocessText($blocks[$key]); $output = $object->didMarkupText($field, $output, $engine); $this->objects[$key]['output'] = $output; } return $this; } /** * Get the output of markup processing for a field queued with * @{method:addObject}. Before you can call this method, you must call * @{method:process}. * * @param PhabricatorMarkupInterface The object to retrieve. * @param string The field to retrieve. * @return string Processed output. * @task markup */ public function getOutput(PhabricatorMarkupInterface $object, $field) { $key = $this->getMarkupFieldKey($object, $field); + $this->requireKeyProcessed($key); + + return $this->objects[$key]['output']; + } + + + /** + * Retrieve engine metadata for a given field. + * + * @param PhabricatorMarkupInterface The object to retrieve. + * @param string The field to retrieve. + * @param string The engine metadata field to retrieve. + * @param wild Optional default value. + * @task markup + */ + public function getEngineMetadata( + PhabricatorMarkupInterface $object, + $field, + $metadata_key, + $default = null) { + + $key = $this->getMarkupFieldKey($object, $field); + $this->requireKeyProcessed($key); + + return idx($this->engineCaches[$key]['metadata'], $metadata_key, $default); + } + + /** + * @task markup + */ + private function requireKeyProcessed($key) { if (empty($this->objects[$key])) { throw new Exception( - "Call addObject() before getOutput() (key = '{$key}')."); + "Call addObject() before using results (key = '{$key}')."); } if (!isset($this->objects[$key]['output'])) { throw new Exception( - "Call process() before getOutput()."); + "Call process() before using results."); } - - return $this->objects[$key]['output']; } /** * @task markup */ private function getMarkupFieldKey( PhabricatorMarkupInterface $object, $field) { return $object->getMarkupFieldKey($field).'@'.$this->version; } /** * @task markup */ private function loadPreprocessorCaches(array $engines, array $objects) { $blocks = array(); $use_cache = array(); foreach ($objects as $key => $info) { if ($info['object']->shouldUseMarkupCache($info['field'])) { $use_cache[$key] = true; } } if ($use_cache) { try { $blocks = id(new PhabricatorMarkupCache())->loadAllWhere( 'cacheKey IN (%Ls)', array_keys($use_cache)); $blocks = mpull($blocks, null, 'getCacheKey'); } catch (Exception $ex) { phlog($ex); } } foreach ($objects as $key => $info) { if (isset($blocks[$key])) { // If we already have a preprocessing cache, we don't need to rebuild // it. continue; } $text = $info['object']->getMarkupText($info['field']); $data = $engines[$key]->preprocessText($text); // NOTE: This is just debugging information to help sort out cache issues. // If one machine is misconfigured and poisoning caches you can use this // field to hunt it down. $metadata = array( 'host' => php_uname('n'), ); $blocks[$key] = id(new PhabricatorMarkupCache()) ->setCacheKey($key) ->setCacheData($data) ->setMetadata($metadata); if (isset($use_cache[$key])) { // This is just filling a cache and always safe, even on a read pathway. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $blocks[$key]->replace(); unset($unguarded); } } return $blocks; } /** * Set the viewing user. Used to implement object permissions. * * @param PhabricatorUser The viewing user. * @return this * @task markup */ public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } /* -( Engine Construction )------------------------------------------------ */ /** * @task engine */ public static function newManiphestMarkupEngine() { return self::newMarkupEngine(array( )); } /** * @task engine */ public static function newPhrictionMarkupEngine() { return self::newMarkupEngine(array( 'header.generate-toc' => true, )); } /** * @task engine */ public static function newPhameMarkupEngine() { return self::newMarkupEngine(array( 'macros' => false, )); } /** * @task engine */ public static function newFeedMarkupEngine() { return self::newMarkupEngine( array( 'macros' => false, 'youtube' => false, )); } /** * @task engine */ public static function newDifferentialMarkupEngine(array $options = array()) { return self::newMarkupEngine(array( 'custom-inline' => PhabricatorEnv::getEnvConfig( 'differential.custom-remarkup-rules'), 'custom-block' => PhabricatorEnv::getEnvConfig( 'differential.custom-remarkup-block-rules'), 'differential.diff' => idx($options, 'differential.diff'), )); } /** * @task engine */ public static function newDiffusionMarkupEngine(array $options = array()) { return self::newMarkupEngine(array( )); } /** * @task engine */ public static function newProfileMarkupEngine() { return self::newMarkupEngine(array( )); } /** * @task engine */ public static function newSlowvoteMarkupEngine() { return self::newMarkupEngine(array( )); } public static function newPonderMarkupEngine(array $options = array()) { return self::newMarkupEngine($options); } /** * @task engine */ public static function getEngine($ruleset = 'default') { static $engines = array(); if (isset($engines[$ruleset])) { return $engines[$ruleset]; } $engine = null; switch ($ruleset) { case 'default': $engine = self::newMarkupEngine(array()); break; default: throw new Exception("Unknown engine ruleset: {$ruleset}!"); } $engines[$ruleset] = $engine; return $engine; } /** * @task engine */ private static function getMarkupEngineDefaultConfiguration() { return array( 'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'), 'youtube' => PhabricatorEnv::getEnvConfig( 'remarkup.enable-embedded-youtube'), 'custom-inline' => array(), 'custom-block' => array(), 'differential.diff' => null, 'header.generate-toc' => false, 'macros' => true, 'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig( 'uri.allowed-protocols'), 'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig( 'syntax-highlighter.engine'), 'preserve-linebreaks' => true, ); } /** * @task engine */ public static function newMarkupEngine(array $options) { $options += self::getMarkupEngineDefaultConfiguration(); $engine = new PhutilRemarkupEngine(); $engine->setConfig('preserve-linebreaks', $options['preserve-linebreaks']); $engine->setConfig('pygments.enabled', $options['pygments']); $engine->setConfig( 'uri.allowed-protocols', $options['uri.allowed-protocols']); $engine->setConfig('differential.diff', $options['differential.diff']); $engine->setConfig('header.generate-toc', $options['header.generate-toc']); $engine->setConfig( 'syntax-highlighter.engine', $options['syntax-highlighter.engine']); $rules = array(); $rules[] = new PhutilRemarkupRuleEscapeRemarkup(); $rules[] = new PhutilRemarkupRuleMonospace(); $custom_rule_classes = $options['custom-inline']; if ($custom_rule_classes) { foreach ($custom_rule_classes as $custom_rule_class) { $rules[] = newv($custom_rule_class, array()); } } $rules[] = new PhutilRemarkupRuleDocumentLink(); if ($options['youtube']) { $rules[] = new PhabricatorRemarkupRuleYoutube(); } $rules[] = new PhutilRemarkupRuleHyperlink(); $rules[] = new PhrictionRemarkupRule(); $rules[] = new PhabricatorRemarkupRuleEmbedFile(); $rules[] = new PhabricatorCountdownRemarkupRule(); $applications = PhabricatorApplication::getAllInstalledApplications(); foreach ($applications as $application) { foreach ($application->getRemarkupRules() as $rule) { $rules[] = $rule; } } if ($options['macros']) { $rules[] = new PhabricatorRemarkupRuleImageMacro(); $rules[] = new PhabricatorRemarkupRuleMeme(); } $rules[] = new DivinerRemarkupRuleSymbol(); $rules[] = new PhabricatorRemarkupRuleMention(); $rules[] = new PhutilRemarkupRuleBold(); $rules[] = new PhutilRemarkupRuleItalic(); $rules[] = new PhutilRemarkupRuleDel(); $blocks = array(); $blocks[] = new PhutilRemarkupEngineRemarkupQuotesBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupLiteralBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupHeaderBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupHorizontalRuleBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupListBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupCodeBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupNoteBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupTableBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupSimpleTableBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupDefaultBlockRule(); $custom_block_rule_classes = $options['custom-block']; if ($custom_block_rule_classes) { foreach ($custom_block_rule_classes as $custom_block_rule_class) { $blocks[] = newv($custom_block_rule_class, array()); } } foreach ($blocks as $block) { if ($block instanceof PhutilRemarkupEngineRemarkupLiteralBlockRule) { $literal_rules = array(); $literal_rules[] = new PhutilRemarkupRuleLinebreaks(); $block->setMarkupRules($literal_rules); } else if ( !($block instanceof PhutilRemarkupEngineRemarkupCodeBlockRule)) { $block->setMarkupRules($rules); } } $engine->setBlockRules($blocks); return $engine; } public static function extractPHIDsFromMentions(array $content_blocks) { $mentions = array(); $engine = self::newDifferentialMarkupEngine(); $engine->setConfig('viewer', PhabricatorUser::getOmnipotentUser()); foreach ($content_blocks as $content_block) { $engine->markupText($content_block); $phids = $engine->getTextMetadata( PhabricatorRemarkupRuleMention::KEY_MENTIONED, array()); $mentions += $phids; } return $mentions; } public static function extractFilePHIDsFromEmbeddedFiles( array $content_blocks) { $files = array(); $engine = self::newDifferentialMarkupEngine(); $engine->setConfig('viewer', PhabricatorUser::getOmnipotentUser()); foreach ($content_blocks as $content_block) { $engine->markupText($content_block); $ids = $engine->getTextMetadata( PhabricatorRemarkupRuleEmbedFile::KEY_EMBED_FILE_PHIDS, array()); $files += $ids; } return $files; } /** * Produce a corpus summary, in a way that shortens the underlying text * without truncating it somewhere awkward. * * TODO: We could do a better job of this. * * @param string Remarkup corpus to summarize. * @return string Summarized corpus. */ public static function summarize($corpus) { // Major goals here are: // - Don't split in the middle of a character (utf-8). // - Don't split in the middle of, e.g., **bold** text, since // we end up with hanging '**' in the summary. // - Try not to pick an image macro, header, embedded file, etc. // - Hopefully don't return too much text. We don't explicitly limit // this right now. $blocks = preg_split("/\n *\n\s*/", trim($corpus)); $best = null; foreach ($blocks as $block) { // This is a test for normal spaces in the block, i.e. a heuristic to // distinguish standard paragraphs from things like image macros. It may // not work well for non-latin text. We prefer to summarize with a // paragraph of normal words over an image macro, if possible. $has_space = preg_match('/\w\s\w/', $block); // This is a test to find embedded images and headers. We prefer to // summarize with a normal paragraph over a header or an embedded object, // if possible. $has_embed = preg_match('/^[{=]/', $block); if ($has_space && !$has_embed) { // This seems like a good summary, so return it. return $block; } if (!$best) { // This is the first block we found; if everything is garbage just // use the first block. $best = $block; } } return $best; } } diff --git a/src/view/phui/PHUIDocumentView.php b/src/view/phui/PHUIDocumentView.php index 48be6946ea..cae4cf5b5a 100644 --- a/src/view/phui/PHUIDocumentView.php +++ b/src/view/phui/PHUIDocumentView.php @@ -1,149 +1,151 @@ offset = $offset; return $this; } public function setHeader(PhabricatorHeaderView $header) { $this->header = $header; return $this; } public function setSideNav(PHUIListView $list) { + $list->setType(PHUIListView::SIDENAV_LIST); $this->sidenav = $list; return $this; } public function setTopNav(PHUIListView $list) { + $list->setType(PHUIListView::NAVBAR_LIST); $this->topnav = $list; return $this; } public function setCrumbs(PHUIListView $list) { $this->crumbs = $list; return $this; } public function setBook($name, $description) { $this->bookname = $name; $this->bookdescription = $description; return $this; } public function getTagAttributes() { $classes = array(); if ($this->offset) { $classes[] = 'phui-document-offset'; }; return array( 'class' => $classes, ); } public function getTagContent() { require_celerity_resource('phui-document-view-css'); $classes = array(); $classes[] = 'phui-document-view'; if ($this->offset) { $classes[] = 'phui-offset-view'; } if ($this->sidenav) { $classes[] = 'phui-sidenav-view'; } $sidenav = null; if ($this->sidenav) { $sidenav = phutil_tag( 'div', array( 'class' => 'phui-document-sidenav' ), $this->sidenav); } $book = null; if ($this->bookname) { $book = phutil_tag( 'div', array( 'class' => 'phui-document-bookname grouped' ), array( phutil_tag( 'span', array('class' => 'bookname'), $this->bookname), phutil_tag( 'span', array('class' => 'bookdescription'), $this->bookdescription))); } $topnav = null; if ($this->topnav) { $topnav = phutil_tag( 'div', array( 'class' => 'phui-document-topnav' ), $this->topnav); } $crumbs = null; if ($this->crumbs) { $crumbs = phutil_tag( 'div', array( 'class' => 'phui-document-crumbs' ), $this->bookName); } $content_inner = phutil_tag( 'div', array( 'class' => 'phui-document-inner', ), array( $book, $this->header, $topnav, $this->renderChildren(), $crumbs )); $content = phutil_tag( 'div', array( 'class' => 'phui-document-content', ), array( $sidenav, $content_inner )); $view = phutil_tag( 'div', array( 'class' => implode(' ', $classes), ), $content); return $view; } }