mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-27 01:02:42 +01:00
Move Diviner further toward usability
Summary: - Complete the "project" -> "book" stuff. This is cleaner conceptually and keeps us from having yet another meaning for the word "project". - Normalize symbols during atomization. This simplifies publishing a great deal, and allows static documentation to link to dynamic documentation and vice versa, because the canonical names of symbols are agreed upon (we can tweak the actual algorithm). - Give articles a specifiable name distinct from the title, and default to something like "support" instead of "Get Help! Get Support!" so URIs end up more readable (not "Get_Help!_Get_Support!"). - Have the atomizers set book information on atoms. - Implement very basic publishers. Publishers are basically glue code between the atomization process and the rendering process -- the two we'll have initially are "static" (publish to files on disk) and "phabricator" (or similar -- publish into the database). - Handle duplicate symbol definitions in the atomize and publish pipelines. This fixes the issue where a project defines two functions named "idx()" and we currently tell them not to do that and break. Realistically, this is common in the real world and we should just roll our eyes and do the legwork to generate documentation as best we can. - Particularly, dirty all atoms with the same name as a dirty atom (e.g., if 'function f()' is updated, regnerate the documentation for all functions named f() in the book). - When publishing, we publish these at "function/f/@1", "function/f/@2". The base page will offer to disambiguate ("There are 8 functions named 'f' in this codebase, which one do you want?"). - Implement a very very basic renderer. This generates the actual HTML (or text, or XML, or whatever else) for the documentation, which the publisher dumps onto disk or into a database or whatever. - The atomize workflow actually needs to depend on books, at least sort of, so make it load config and use it properly. - Propagate multilevel dirties through the graph. If "C extends B" and "B extends A", we should regenerate C when A changes. Prior to this diff, we would regnerate B only. Test Plan: Generated some documentation. Named two articles "feedback", generated docs, saw "article/feedback/@1/" and "article/feedback/@2/" created. Reviewers: btrahan, vrana, chad Reviewed By: chad CC: aran Maniphest Tasks: T988 Differential Revision: https://secure.phabricator.com/D4896
This commit is contained in:
parent
26aac16346
commit
bcc082a01e
12 changed files with 460 additions and 77 deletions
|
@ -467,9 +467,13 @@ phutil_register_library_map(array(
|
||||||
'DivinerAtomRef' => 'applications/diviner/atom/DivinerAtomRef.php',
|
'DivinerAtomRef' => 'applications/diviner/atom/DivinerAtomRef.php',
|
||||||
'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php',
|
'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php',
|
||||||
'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php',
|
'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php',
|
||||||
|
'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php',
|
||||||
'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php',
|
'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php',
|
||||||
'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php',
|
'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php',
|
||||||
'DivinerListController' => 'applications/diviner/controller/DivinerListController.php',
|
'DivinerListController' => 'applications/diviner/controller/DivinerListController.php',
|
||||||
|
'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php',
|
||||||
|
'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php',
|
||||||
|
'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php',
|
||||||
'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php',
|
'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php',
|
||||||
'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php',
|
'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php',
|
||||||
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
|
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
|
||||||
|
@ -1962,9 +1966,11 @@ phutil_register_library_map(array(
|
||||||
'DiffusionView' => 'AphrontView',
|
'DiffusionView' => 'AphrontView',
|
||||||
'DivinerArticleAtomizer' => 'DivinerAtomizer',
|
'DivinerArticleAtomizer' => 'DivinerAtomizer',
|
||||||
'DivinerAtomizeWorkflow' => 'DivinerWorkflow',
|
'DivinerAtomizeWorkflow' => 'DivinerWorkflow',
|
||||||
|
'DivinerDefaultRenderer' => 'DivinerRenderer',
|
||||||
'DivinerFileAtomizer' => 'DivinerAtomizer',
|
'DivinerFileAtomizer' => 'DivinerAtomizer',
|
||||||
'DivinerGenerateWorkflow' => 'DivinerWorkflow',
|
'DivinerGenerateWorkflow' => 'DivinerWorkflow',
|
||||||
'DivinerListController' => 'PhabricatorController',
|
'DivinerListController' => 'PhabricatorController',
|
||||||
|
'DivinerStaticPublisher' => 'DivinerPublisher',
|
||||||
'DivinerWorkflow' => 'PhutilArgumentWorkflow',
|
'DivinerWorkflow' => 'PhutilArgumentWorkflow',
|
||||||
'DrydockAllocatorWorker' => 'PhabricatorWorker',
|
'DrydockAllocatorWorker' => 'PhabricatorWorker',
|
||||||
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
|
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
|
||||||
|
|
|
@ -22,15 +22,31 @@ final class DivinerAtom {
|
||||||
private $context;
|
private $context;
|
||||||
private $extends = array();
|
private $extends = array();
|
||||||
private $links = array();
|
private $links = array();
|
||||||
private $project;
|
private $book;
|
||||||
|
|
||||||
public function setProject($project) {
|
/**
|
||||||
$this->project = $project;
|
* Returns a sorting key which imposes an unambiguous, stable order on atoms.
|
||||||
|
*/
|
||||||
|
public function getSortKey() {
|
||||||
|
return implode(
|
||||||
|
"\0",
|
||||||
|
array(
|
||||||
|
$this->getBook(),
|
||||||
|
$this->getType(),
|
||||||
|
$this->getContext(),
|
||||||
|
$this->getName(),
|
||||||
|
$this->getFile(),
|
||||||
|
sprintf('%08', $this->getLine()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBook($book) {
|
||||||
|
$this->book = $book;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getProject() {
|
public function getBook() {
|
||||||
return $this->project;
|
return $this->book;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setContext($context) {
|
public function setContext($context) {
|
||||||
|
@ -84,6 +100,18 @@ final class DivinerAtom {
|
||||||
return $this->docblockMeta;
|
return $this->docblockMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDocblockMetaValue($key, $default = null) {
|
||||||
|
$meta = $this->getDocblockMeta();
|
||||||
|
return idx($meta, $key, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDocblockMetaValue($key, $value) {
|
||||||
|
$meta = $this->getDocblockMeta();
|
||||||
|
$meta[$key] = $value;
|
||||||
|
$this->docblockMeta = $meta;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function setType($type) {
|
public function setType($type) {
|
||||||
$this->type = $type;
|
$this->type = $type;
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -235,6 +263,7 @@ final class DivinerAtom {
|
||||||
// getAtomSerializationVersion().
|
// getAtomSerializationVersion().
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
|
'book' => $this->getBook(),
|
||||||
'type' => $this->getType(),
|
'type' => $this->getType(),
|
||||||
'name' => $this->getName(),
|
'name' => $this->getName(),
|
||||||
'file' => $this->getFile(),
|
'file' => $this->getFile(),
|
||||||
|
@ -256,7 +285,7 @@ final class DivinerAtom {
|
||||||
|
|
||||||
public function getRef() {
|
public function getRef() {
|
||||||
return id(new DivinerAtomRef())
|
return id(new DivinerAtomRef())
|
||||||
->setProject($this->getProject())
|
->setBook($this->getBook())
|
||||||
->setContext($this->getContext())
|
->setContext($this->getContext())
|
||||||
->setType($this->getType())
|
->setType($this->getType())
|
||||||
->setName($this->getName());
|
->setName($this->getName());
|
||||||
|
@ -264,6 +293,7 @@ final class DivinerAtom {
|
||||||
|
|
||||||
public static function newFromDictionary(array $dictionary) {
|
public static function newFromDictionary(array $dictionary) {
|
||||||
$atom = id(new DivinerAtom())
|
$atom = id(new DivinerAtom())
|
||||||
|
->setBook(idx($dictionary, 'book'))
|
||||||
->setType(idx($dictionary, 'type'))
|
->setType(idx($dictionary, 'type'))
|
||||||
->setName(idx($dictionary, 'name'))
|
->setName(idx($dictionary, 'name'))
|
||||||
->setFile(idx($dictionary, 'file'))
|
->setFile(idx($dictionary, 'file'))
|
||||||
|
|
|
@ -2,12 +2,18 @@
|
||||||
|
|
||||||
final class DivinerAtomRef {
|
final class DivinerAtomRef {
|
||||||
|
|
||||||
private $project;
|
private $book;
|
||||||
private $context;
|
private $context;
|
||||||
private $type;
|
private $type;
|
||||||
private $name;
|
private $name;
|
||||||
|
|
||||||
public function setName($name) {
|
public function setName($name) {
|
||||||
|
$normal_name = self::normalizeString($name);
|
||||||
|
if (preg_match('/^@[0-9]+$/', $normal_name)) {
|
||||||
|
throw new Exception(
|
||||||
|
"Atom names must not be in the form '/@\d+/'. This pattern is ".
|
||||||
|
"reserved for disambiguating atoms with similar names.");
|
||||||
|
}
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@ -17,7 +23,7 @@ final class DivinerAtomRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setType($type) {
|
public function setType($type) {
|
||||||
$this->type = $type;
|
$this->type = self::normalizeString($type);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +32,11 @@ final class DivinerAtomRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setContext($context) {
|
public function setContext($context) {
|
||||||
|
if ($context === null) {
|
||||||
$this->context = $context;
|
$this->context = $context;
|
||||||
|
} else {
|
||||||
|
$this->context = self::normalizeString($context);
|
||||||
|
}
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,18 +44,18 @@ final class DivinerAtomRef {
|
||||||
return $this->context;
|
return $this->context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setProject($project) {
|
public function setBook($book) {
|
||||||
$this->project = $project;
|
$this->book = self::normalizeString($book);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getProject() {
|
public function getBook() {
|
||||||
return $this->project;
|
return $this->book;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toDictionary() {
|
public function toDictionary() {
|
||||||
return array(
|
return array(
|
||||||
'project' => $this->getProject(),
|
'book' => $this->getBook(),
|
||||||
'context' => $this->getContext(),
|
'context' => $this->getContext(),
|
||||||
'type' => $this->getType(),
|
'type' => $this->getType(),
|
||||||
'name' => $this->getName(),
|
'name' => $this->getName(),
|
||||||
|
@ -60,10 +70,57 @@ final class DivinerAtomRef {
|
||||||
|
|
||||||
public static function newFromDictionary(array $dict) {
|
public static function newFromDictionary(array $dict) {
|
||||||
$obj = new DivinerAtomRef();
|
$obj = new DivinerAtomRef();
|
||||||
$obj->project = idx($dict, 'project');
|
$obj->book = idx($dict, 'book');
|
||||||
$obj->context = idx($dict, 'context');
|
$obj->context = idx($dict, 'context');
|
||||||
$obj->type = idx($dict, 'type');
|
$obj->type = idx($dict, 'type');
|
||||||
$obj->name = idx($dict, 'name');
|
$obj->name = idx($dict, 'name');
|
||||||
return $obj;
|
return $obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function normalizeString($str) {
|
||||||
|
// These characters create problems on the filesystem or in URIs. Replace
|
||||||
|
// them with non-problematic appoximations (instead of simply removing them)
|
||||||
|
// to keep the URIs fairly useful and avoid unnecessary collisions. These
|
||||||
|
// approximations are selected based on some domain knowledge of common
|
||||||
|
// languages: where a character is used as a delimiter, it is more helpful
|
||||||
|
// to replace it with a "." or a ":" or similar, while it's better if
|
||||||
|
// operator overloads read as, e.g., "operator_div".
|
||||||
|
|
||||||
|
$map = array(
|
||||||
|
// Hopefully not used anywhere by anything.
|
||||||
|
'#' => '.',
|
||||||
|
|
||||||
|
// Used in Ruby methods.
|
||||||
|
'?' => 'Q',
|
||||||
|
|
||||||
|
// Used in PHP namespaces.
|
||||||
|
'\\' => '.',
|
||||||
|
|
||||||
|
// Used in "operator +" in C++.
|
||||||
|
'+' => 'plus',
|
||||||
|
|
||||||
|
// Used in "operator %" in C++.
|
||||||
|
'%' => 'mod',
|
||||||
|
|
||||||
|
// Used in "operator /" in C++.
|
||||||
|
'/' => 'div',
|
||||||
|
);
|
||||||
|
$str = str_replace(array_keys($map), array_values($map), $str);
|
||||||
|
|
||||||
|
// Replace all spaces with underscores.
|
||||||
|
$str = preg_replace('/ +/', '_', $str);
|
||||||
|
|
||||||
|
// Replace control characters with "@".
|
||||||
|
$str = preg_replace('/[\x00-\x19]/', '@', $str);
|
||||||
|
|
||||||
|
// Replace specific problematic names with alternative names.
|
||||||
|
$alternates = array(
|
||||||
|
'.' => 'dot',
|
||||||
|
'..' => 'dotdot',
|
||||||
|
'' => 'null',
|
||||||
|
);
|
||||||
|
|
||||||
|
return idx($alternates, $str, $str);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,22 @@ final class DivinerArticleAtomizer extends DivinerAtomizer {
|
||||||
$atom->setDocblockRaw($block);
|
$atom->setDocblockRaw($block);
|
||||||
|
|
||||||
$meta = $atom->getDocblockMeta();
|
$meta = $atom->getDocblockMeta();
|
||||||
|
|
||||||
$title = idx($meta, 'title');
|
$title = idx($meta, 'title');
|
||||||
if (!strlen($title)) {
|
if (!strlen($title)) {
|
||||||
$title = 'Untitled Article "'.basename($file_name).'"';
|
$title = pht('Untitled Article "%s"', basename($file_name));
|
||||||
$atom->addWarning("Article has no @title!");
|
$atom->addWarning("Article has no @title!");
|
||||||
|
$atom->setDocblockMetaValue('title', $title);
|
||||||
}
|
}
|
||||||
$atom->setName($title);
|
|
||||||
|
// If the article has no @name, use the filename after stripping any
|
||||||
|
// extension.
|
||||||
|
$name = idx($meta, 'name');
|
||||||
|
if (!$name) {
|
||||||
|
$name = basename($file_name);
|
||||||
|
$name = preg_replace('/\\.[^.]+$/', '', $name);
|
||||||
|
}
|
||||||
|
$atom->setName($name);
|
||||||
|
|
||||||
return array($atom);
|
return array($atom);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
abstract class DivinerAtomizer {
|
abstract class DivinerAtomizer {
|
||||||
|
|
||||||
private $project;
|
private $book;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If you make a significant change to an atomizer, you can bump this
|
* If you make a significant change to an atomizer, you can bump this
|
||||||
|
@ -17,26 +17,26 @@ abstract class DivinerAtomizer {
|
||||||
|
|
||||||
abstract public function atomize($file_name, $file_data);
|
abstract public function atomize($file_name, $file_data);
|
||||||
|
|
||||||
final public function setProject($project) {
|
final public function setBook($book) {
|
||||||
$this->project = $project;
|
$this->book = $book;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
final public function getProject() {
|
final public function getBook() {
|
||||||
return $this->project;
|
return $this->book;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function newAtom($type) {
|
protected function newAtom($type) {
|
||||||
return id(new DivinerAtom())
|
return id(new DivinerAtom())
|
||||||
->setProject($this->getProject())
|
->setBook($this->getBook())
|
||||||
->setType($type);
|
->setType($type);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function newRef($type, $name, $project = null, $context = null) {
|
protected function newRef($type, $name, $book = null, $context = null) {
|
||||||
$project = coalesce($project, $this->getProject());
|
$book = coalesce($book, $this->getBook());
|
||||||
|
|
||||||
return id(new DivinerAtomRef())
|
return id(new DivinerAtomRef())
|
||||||
->setProject($project)
|
->setBook($book)
|
||||||
->setContext($context)
|
->setContext($context)
|
||||||
->setType($type)
|
->setType($type)
|
||||||
->setName($name);
|
->setName($name);
|
||||||
|
|
132
src/applications/diviner/publisher/DivinerPublisher.php
Normal file
132
src/applications/diviner/publisher/DivinerPublisher.php
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class DivinerPublisher {
|
||||||
|
|
||||||
|
private $atomCache;
|
||||||
|
private $atomGraphHashToNodeHashMap;
|
||||||
|
private $atomMap = array();
|
||||||
|
private $renderer;
|
||||||
|
private $config;
|
||||||
|
private $symbolReverseMap;
|
||||||
|
|
||||||
|
public function setRenderer(DivinerRenderer $renderer) {
|
||||||
|
$this->renderer = $renderer;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRenderer() {
|
||||||
|
return $this->renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setConfig(array $config) {
|
||||||
|
$this->config = $config;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfig($key, $default = null) {
|
||||||
|
return idx($this->config, $key, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAtomCache(DivinerAtomCache $cache) {
|
||||||
|
$this->atomCache = $cache;
|
||||||
|
$graph_map = $this->atomCache->getGraphMap();
|
||||||
|
$this->atomGraphHashToNodeHashMap = array_flip($graph_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getAtomFromGraphHash($graph_hash) {
|
||||||
|
if (empty($this->atomGraphHashToNodeHashMap[$graph_hash])) {
|
||||||
|
throw new Exception("No such atom '{$graph_hash}'!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getAtomFromNodeHash(
|
||||||
|
$this->atomGraphHashToNodeHashMap[$graph_hash]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getAtomFromNodeHash($node_hash) {
|
||||||
|
if (empty($this->atomMap[$node_hash])) {
|
||||||
|
$dict = $this->atomCache->getAtom($node_hash);
|
||||||
|
$this->atomMap[$node_hash] = DivinerAtom::newFromDictionary($dict);
|
||||||
|
}
|
||||||
|
return $this->atomMap[$node_hash];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getSimilarAtoms(DivinerAtom $atom) {
|
||||||
|
if ($this->symbolReverseMap === null) {
|
||||||
|
$rmap = array();
|
||||||
|
$smap = $this->atomCache->getSymbolMap();
|
||||||
|
foreach ($smap as $nhash => $shash) {
|
||||||
|
$rmap[$shash][$nhash] = true;
|
||||||
|
}
|
||||||
|
$this->symbolReverseMap = $rmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shash = $atom->getRef()->toHash();
|
||||||
|
|
||||||
|
if (empty($this->symbolReverseMap[$shash])) {
|
||||||
|
throw new Exception("Atom has no symbol map entry!");
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashes = $this->symbolReverseMap[$shash];
|
||||||
|
|
||||||
|
$atoms = array();
|
||||||
|
foreach ($hashes as $hash => $ignored) {
|
||||||
|
$atoms[] = $this->getAtomFromNodeHash($hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
$atoms = msort($atoms, 'getSortKey');
|
||||||
|
return $atoms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a book contains multiple definitions of some atom, like some function
|
||||||
|
* "f()", we assign them an arbitrary (but fairly stable) order and publish
|
||||||
|
* them as "function/f/1/", "function/f/2/", etc., or similar.
|
||||||
|
*/
|
||||||
|
protected function getAtomSimilarIndex(DivinerAtom $atom) {
|
||||||
|
$atoms = $this->getSimilarAtoms($atom);
|
||||||
|
if (count($atoms) == 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$index = 1;
|
||||||
|
foreach ($atoms as $similar_atom) {
|
||||||
|
if ($atom === $similar_atom) {
|
||||||
|
return $index;
|
||||||
|
}
|
||||||
|
$index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Expected to find atom while disambiguating!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
abstract protected function loadAllPublishedHashes();
|
||||||
|
abstract protected function deleteDocumentsByHash(array $hashes);
|
||||||
|
abstract protected function createDocumentsByHash(array $hashes);
|
||||||
|
|
||||||
|
final public function publishAtoms(array $hashes) {
|
||||||
|
$existing = $this->loadAllPublishedHashes();
|
||||||
|
|
||||||
|
$existing_map = array_fill_keys($existing, true);
|
||||||
|
$hashes_map = array_fill_keys($hashes, true);
|
||||||
|
|
||||||
|
$deleted = array_diff_key($existing_map, $hashes_map);
|
||||||
|
$created = array_diff_key($hashes_map, $existing_map);
|
||||||
|
|
||||||
|
$this->createDocumentsByHash(array_keys($created));
|
||||||
|
$this->deleteDocumentsByHash(array_keys($deleted));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function shouldGenerateDocumentForAtom(DivinerAtom $atom) {
|
||||||
|
switch ($atom->getType()) {
|
||||||
|
case DivinerAtom::TYPE_FILE:
|
||||||
|
return false;
|
||||||
|
case DivinerAtom::TYPE_ARTICLE:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class DivinerStaticPublisher extends DivinerPublisher {
|
||||||
|
|
||||||
|
protected function loadAllPublishedHashes() {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function deleteDocumentsByHash(array $hashes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createDocumentsByHash(array $hashes) {
|
||||||
|
foreach ($hashes as $hash) {
|
||||||
|
$atom = $this->getAtomFromGraphHash($hash);
|
||||||
|
|
||||||
|
if (!$this->shouldGenerateDocumentForAtom($atom)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = $this->getRenderer()->renderAtom($atom);
|
||||||
|
$this->writeDocument($atom, $content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeDocument(DivinerAtom $atom, $content) {
|
||||||
|
$root = $this->getConfig('root');
|
||||||
|
$path = $root.DIRECTORY_SEPARATOR.$this->getAtomRelativePath($atom);
|
||||||
|
|
||||||
|
if (!Filesystem::pathExists($path)) {
|
||||||
|
Filesystem::createDirectory($path, $umask = 0755, $recursive = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Filesystem::writeFile($path.'index.html', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAtomRelativePath(DivinerAtom $atom) {
|
||||||
|
$ref = $atom->getRef();
|
||||||
|
|
||||||
|
$book = $ref->getBook();
|
||||||
|
$type = $ref->getType();
|
||||||
|
$context = $ref->getContext();
|
||||||
|
$name = $ref->getName();
|
||||||
|
|
||||||
|
$path = array(
|
||||||
|
'docs',
|
||||||
|
$book,
|
||||||
|
$type,
|
||||||
|
);
|
||||||
|
if ($context !== null) {
|
||||||
|
$path[] = $context;
|
||||||
|
}
|
||||||
|
$path[] = $name;
|
||||||
|
|
||||||
|
$index = $this->getAtomSimilarIndex($atom);
|
||||||
|
if ($index !== null) {
|
||||||
|
$path[] = '@'.$index;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path[] = null;
|
||||||
|
|
||||||
|
return implode(DIRECTORY_SEPARATOR, $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class DivinerDefaultRenderer extends DivinerRenderer {
|
||||||
|
|
||||||
|
public function renderAtom(DivinerAtom $atom) {
|
||||||
|
return "ATOM: ".$atom->getType()." ".$atom->getName()."!";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
7
src/applications/diviner/renderer/DivinerRenderer.php
Normal file
7
src/applications/diviner/renderer/DivinerRenderer.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class DivinerRenderer {
|
||||||
|
|
||||||
|
abstract public function renderAtom(DivinerAtom $atom);
|
||||||
|
|
||||||
|
}
|
|
@ -11,7 +11,12 @@ final class DivinerAtomizeWorkflow extends DivinerWorkflow {
|
||||||
array(
|
array(
|
||||||
'name' => 'atomizer',
|
'name' => 'atomizer',
|
||||||
'param' => 'class',
|
'param' => 'class',
|
||||||
'help' => 'Specify a subclass of DivinerAtomizer.',
|
'help' => pht('Specify a subclass of DivinerAtomizer.'),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'book',
|
||||||
|
'param' => 'path',
|
||||||
|
'help' => pht('Path to a Diviner book configuration.'),
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'name' => 'files',
|
'name' => 'files',
|
||||||
|
@ -19,12 +24,14 @@ final class DivinerAtomizeWorkflow extends DivinerWorkflow {
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'name' => 'ugly',
|
'name' => 'ugly',
|
||||||
'help' => 'Produce ugly (but faster) output.',
|
'help' => pht('Produce ugly (but faster) output.'),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(PhutilArgumentParser $args) {
|
public function execute(PhutilArgumentParser $args) {
|
||||||
|
$this->readBookConfiguration($args);
|
||||||
|
|
||||||
$console = PhutilConsole::getConsole();
|
$console = PhutilConsole::getConsole();
|
||||||
|
|
||||||
$atomizer_class = $args->getArg('atomizer');
|
$atomizer_class = $args->getArg('atomizer');
|
||||||
|
@ -81,6 +88,11 @@ final class DivinerAtomizeWorkflow extends DivinerWorkflow {
|
||||||
}
|
}
|
||||||
|
|
||||||
$all_atoms = array_mergev($all_atoms);
|
$all_atoms = array_mergev($all_atoms);
|
||||||
|
|
||||||
|
foreach ($all_atoms as $atom) {
|
||||||
|
$atom->setBook($this->getConfig('name'));
|
||||||
|
}
|
||||||
|
|
||||||
$all_atoms = mpull($all_atoms, 'toDictionary');
|
$all_atoms = mpull($all_atoms, 'toDictionary');
|
||||||
$all_atoms = ipull($all_atoms, null, 'hash');
|
$all_atoms = ipull($all_atoms, null, 'hash');
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
final class DivinerGenerateWorkflow extends DivinerWorkflow {
|
final class DivinerGenerateWorkflow extends DivinerWorkflow {
|
||||||
|
|
||||||
private $config;
|
|
||||||
private $atomCache;
|
private $atomCache;
|
||||||
|
|
||||||
public function didConstruct() {
|
public function didConstruct() {
|
||||||
|
@ -23,10 +22,6 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getConfig($key, $default = null) {
|
|
||||||
return idx($this->config, $key, $default);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getAtomCache() {
|
protected function getAtomCache() {
|
||||||
if (!$this->atomCache) {
|
if (!$this->atomCache) {
|
||||||
$book_root = $this->getConfig('root');
|
$book_root = $this->getConfig('root');
|
||||||
|
@ -49,6 +44,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
|
||||||
if ($args->getArg('clean')) {
|
if ($args->getArg('clean')) {
|
||||||
$this->log(pht('CLEARING CACHES'));
|
$this->log(pht('CLEARING CACHES'));
|
||||||
$this->getAtomCache()->delete();
|
$this->getAtomCache()->delete();
|
||||||
|
$this->log(pht('Done.')."\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// The major challenge of documentation generation is one of dependency
|
// The major challenge of documentation generation is one of dependency
|
||||||
|
@ -134,6 +130,8 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
|
||||||
|
|
||||||
$this->buildAtomCache();
|
$this->buildAtomCache();
|
||||||
$this->buildGraphCache();
|
$this->buildGraphCache();
|
||||||
|
|
||||||
|
$this->publishDocumentation();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -( Atom Cache )--------------------------------------------------------- */
|
/* -( Atom Cache )--------------------------------------------------------- */
|
||||||
|
@ -167,7 +165,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
|
||||||
|
|
||||||
$this->getAtomCache()->saveAtoms();
|
$this->getAtomCache()->saveAtoms();
|
||||||
|
|
||||||
$this->log(pht("Done."));
|
$this->log(pht('Done.')."\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getAtomizersForFiles(array $files) {
|
private function getAtomizersForFiles(array $files) {
|
||||||
|
@ -252,8 +250,9 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
|
||||||
foreach ($atomizers as $class => $files) {
|
foreach ($atomizers as $class => $files) {
|
||||||
foreach (array_chunk($files, 32) as $chunk) {
|
foreach (array_chunk($files, 32) as $chunk) {
|
||||||
$future = new ExecFuture(
|
$future = new ExecFuture(
|
||||||
'%s atomize --ugly --atomizer %s -- %Ls',
|
'%s atomize --ugly --book %s --atomizer %s -- %Ls',
|
||||||
dirname(phutil_get_library_root('phabricator')).'/bin/diviner',
|
dirname(phutil_get_library_root('phabricator')).'/bin/diviner',
|
||||||
|
$this->getBookConfigPath(),
|
||||||
$class,
|
$class,
|
||||||
$chunk);
|
$chunk);
|
||||||
$future->setCWD($this->getConfig('root'));
|
$future->setCWD($this->getConfig('root'));
|
||||||
|
@ -352,9 +351,25 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
|
||||||
|
|
||||||
$this->log(pht('Propagating changes through the graph.'));
|
$this->log(pht('Propagating changes through the graph.'));
|
||||||
|
|
||||||
foreach ($dirty_symbols as $symbol => $ignored) {
|
// Find all the nodes which point at a dirty node, and dirty them. Then
|
||||||
foreach ($atom_cache->getEdgesWithDestination($symbol) as $edge) {
|
// find all the nodes which point at those nodes and dirty them, and so
|
||||||
|
// on. (This is slightly overkill since we probably don't need to propagate
|
||||||
|
// dirtiness across documentation "links" between symbols, but we do want
|
||||||
|
// to propagate it across "extends", and we suffer only a little bit of
|
||||||
|
// collateral damage by over-dirtying as long as the documentation isn't
|
||||||
|
// too well-connected.)
|
||||||
|
|
||||||
|
$symbol_stack = array_keys($dirty_symbols);
|
||||||
|
while ($symbol_stack) {
|
||||||
|
$symbol_hash = array_pop($symbol_stack);
|
||||||
|
|
||||||
|
foreach ($atom_cache->getEdgesWithDestination($symbol_hash) as $edge) {
|
||||||
$dirty_nhashes[$edge] = true;
|
$dirty_nhashes[$edge] = true;
|
||||||
|
$src_hash = $this->computeSymbolHash($edge);
|
||||||
|
if (empty($dirty_symbols[$src_hash])) {
|
||||||
|
$dirty_symbols[$src_hash] = true;
|
||||||
|
$symbol_stack[] = $src_hash;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,7 +385,7 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
|
||||||
$atom_cache->saveEdges();
|
$atom_cache->saveEdges();
|
||||||
$atom_cache->saveSymbols();
|
$atom_cache->saveSymbols();
|
||||||
|
|
||||||
$this->log(pht('Done.'));
|
$this->log(pht('Done.')."\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
private function computeSymbolHash($node_hash) {
|
private function computeSymbolHash($node_hash) {
|
||||||
|
@ -386,9 +401,15 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
|
||||||
$atom = $atom_cache->getAtom($node_hash);
|
$atom = $atom_cache->getAtom($node_hash);
|
||||||
|
|
||||||
$refs = array();
|
$refs = array();
|
||||||
|
|
||||||
|
// Make the atom depend on its own symbol, so that all atoms with the same
|
||||||
|
// symbol are dirtied (e.g., if a codebase defines the function "f()"
|
||||||
|
// several times, all of them should be dirtied when one is dirtied).
|
||||||
|
$refs[DivinerAtomRef::newFromDictionary($atom)->toHash()] = true;
|
||||||
|
|
||||||
foreach (array_merge($atom['extends'], $atom['links']) as $ref_dict) {
|
foreach (array_merge($atom['extends'], $atom['links']) as $ref_dict) {
|
||||||
$ref = DivinerAtomRef::newFromDictionary($ref_dict);
|
$ref = DivinerAtomRef::newFromDictionary($ref_dict);
|
||||||
if ($ref->getProject() == $atom['project']) {
|
if ($ref->getBook() == $atom['book']) {
|
||||||
$refs[$ref->toHash()] = true;
|
$refs[$ref->toHash()] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -411,42 +432,21 @@ final class DivinerGenerateWorkflow extends DivinerWorkflow {
|
||||||
return md5(serialize($inputs)).'G';
|
return md5(serialize($inputs)).'G';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function readBookConfiguration(PhutilArgumentParser $args) {
|
|
||||||
$book_path = $args->getArg('book');
|
private function publishDocumentation() {
|
||||||
if ($book_path === null) {
|
$atom_cache = $this->getAtomCache();
|
||||||
throw new PhutilArgumentUsageException(
|
$graph_map = $atom_cache->getGraphMap();
|
||||||
"Specify a Diviner book configuration file with --book.");
|
|
||||||
|
$this->log(pht('PUBLISHING DOCUMENTATION'));
|
||||||
|
|
||||||
|
$publisher = new DivinerStaticPublisher();
|
||||||
|
$publisher->setConfig($this->getAllConfig());
|
||||||
|
$publisher->setAtomCache($atom_cache);
|
||||||
|
$publisher->setRenderer(new DivinerDefaultRenderer());
|
||||||
|
$publisher->publishAtoms(array_values($graph_map));
|
||||||
|
|
||||||
|
$this->log(pht('Done.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$book_data = Filesystem::readFile($book_path);
|
|
||||||
$book = json_decode($book_data, true);
|
|
||||||
if (!is_array($book)) {
|
|
||||||
throw new PhutilArgumentUsageException(
|
|
||||||
"Book configuration '{$book_path}' is not in JSON format.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the book specifies a "root", resolve it; otherwise, use the directory
|
|
||||||
// the book configuration file lives in.
|
|
||||||
$full_path = dirname(Filesystem::resolvePath($book_path));
|
|
||||||
if (empty($book['root'])) {
|
|
||||||
$book['root'] = '.';
|
|
||||||
}
|
|
||||||
$book['root'] = Filesystem::resolvePath($book['root'], $full_path);
|
|
||||||
|
|
||||||
// Make sure we have a valid book name.
|
|
||||||
if (!isset($book['name'])) {
|
|
||||||
throw new PhutilArgumentUsageException(
|
|
||||||
"Book configuration '{$book_path}' is missing required ".
|
|
||||||
"property 'name'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preg_match('/^[a-z][a-z-]*$/', $book['name'])) {
|
|
||||||
$name = $book['name'];
|
|
||||||
throw new PhutilArgumentUsageException(
|
|
||||||
"Book configuration '{$book_path}' has name '{$name}', but book names ".
|
|
||||||
"must include only lowercase letters and hyphens.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->config = $book;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,63 @@
|
||||||
|
|
||||||
abstract class DivinerWorkflow extends PhutilArgumentWorkflow {
|
abstract class DivinerWorkflow extends PhutilArgumentWorkflow {
|
||||||
|
|
||||||
|
private $config;
|
||||||
|
private $bookConfigPath;
|
||||||
|
|
||||||
|
public function getBookConfigPath() {
|
||||||
|
return $this->bookConfigPath;
|
||||||
|
}
|
||||||
|
|
||||||
public function isExecutable() {
|
public function isExecutable() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getConfig($key, $default = null) {
|
||||||
|
return idx($this->config, $key, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getAllConfig() {
|
||||||
|
return $this->config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function readBookConfiguration(PhutilArgumentParser $args) {
|
||||||
|
$book_path = $args->getArg('book');
|
||||||
|
if ($book_path === null) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Specify a Diviner book configuration file with --book.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$book_data = Filesystem::readFile($book_path);
|
||||||
|
$book = json_decode($book_data, true);
|
||||||
|
if (!is_array($book)) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Book configuration '{$book_path}' is not in JSON format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the book specifies a "root", resolve it; otherwise, use the directory
|
||||||
|
// the book configuration file lives in.
|
||||||
|
$full_path = dirname(Filesystem::resolvePath($book_path));
|
||||||
|
if (empty($book['root'])) {
|
||||||
|
$book['root'] = '.';
|
||||||
|
}
|
||||||
|
$book['root'] = Filesystem::resolvePath($book['root'], $full_path);
|
||||||
|
|
||||||
|
// Make sure we have a valid book name.
|
||||||
|
if (!isset($book['name'])) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Book configuration '{$book_path}' is missing required ".
|
||||||
|
"property 'name'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/^[a-z][a-z-]*$/', $book['name'])) {
|
||||||
|
$name = $book['name'];
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Book configuration '{$book_path}' has name '{$name}', but book names ".
|
||||||
|
"must include only lowercase letters and hyphens.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bookConfigPath = $book_path;
|
||||||
|
$this->config = $book;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue