Skip to content

Commit f57aded

Browse files
committed
Generate documentation from code
1 parent 0bdd736 commit f57aded

File tree

3 files changed

+353
-1
lines changed

3 files changed

+353
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
_site/
22
.jekyll-metadata
3+
/composer.lock
4+
/vendor
5+
/.idea

RoboFile.php

Lines changed: 314 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
<?php
2+
3+
use Codeception\Util\DocumentationHelpers;
24
use Symfony\Component\Finder\Finder;
35

6+
require __DIR__ . '/vendor/autoload.php';
7+
48
class RoboFile extends \Robo\Tasks
59
{
6-
10+
use DocumentationHelpers;
11+
12+
const REPO_BLOB_URL = 'https://github.com/Codeception/Codeception/blob';
13+
const STABLE_BRANCH = '4.0';
14+
715
function post()
816
{
917
$title = $this->ask("Post title");
@@ -97,4 +105,309 @@ function docsBranch($branch)
97105
$indexFile->run();
98106
// $this->taskDeleteDir('source')->run();
99107
}
108+
109+
/**
110+
* @desc generates modules reference from source files
111+
*/
112+
public function buildDocs()
113+
{
114+
$this->say('generating documentation from source files');
115+
$this->taskComposerInstall()->run();
116+
$this->buildDocsModules();
117+
$this->buildDocsUtils();
118+
$this->buildDocsCommands();
119+
$this->buildDocsStub();
120+
$this->buildDocsApi();
121+
$this->buildDocsExtensions();
122+
}
123+
124+
public function buildDocsModules()
125+
{
126+
$this->taskCleanDir('docs/modules')->run();
127+
$this->say("Modules");
128+
$modules = Finder::create()->files()->name('*.php')->in(__DIR__ . '/vendor/codeception/module-*/src/Codeception/Module');
129+
foreach ($modules as $module) {
130+
$moduleName = basename(substr($module, 0, -4));
131+
$className = 'Codeception\Module\\' . $moduleName;
132+
$classPath = 'Codeception/Module/' . $moduleName;
133+
$repositoryName = basename(substr($module, 0, -1 * (strlen($classPath) + 9)));
134+
//echo $classPath, " ", $repositoryName, "\n"; continue;
135+
//TODO: take /src/$classPath.php directly from module
136+
$source = "https://github.com/Codeception/$repositoryName/tree/master/src/$classPath.php";
137+
$documentationFile = 'docs/modules/' . $moduleName . '.md';
138+
$sourceMessage = '<p>&nbsp;</p><div class="alert alert-warning">Module reference is taken from the source code. <a href="' . $source . '">Help us to improve documentation. Edit module reference</a></div>';
139+
$this->generateDocumentationForClass($className, $documentationFile, $sourceMessage);
140+
$this->postProcessModuleDocFile($documentationFile, $moduleName, $source);
141+
}
142+
}
143+
144+
private function postProcessModuleDocFile($documentationFile, $name, $source)
145+
{
146+
$contents = file_get_contents($documentationFile);
147+
$contents = str_replace('## ', '### ', $contents);
148+
$buttons = [
149+
'source' => $source,
150+
];
151+
// building version switcher
152+
foreach (['3.1', '2.5', '1.8'] as $branch) {
153+
$buttons[$branch] = self::REPO_BLOB_URL."/$branch/docs/modules/$name.md";
154+
}
155+
$buttonHtml = "\n\n".'<div class="btn-group" role="group" style="float: right" aria-label="...">';
156+
foreach ($buttons as $link => $url) {
157+
if ($link == 'source') {
158+
$link = "<strong>$link</strong>";
159+
}
160+
$buttonHtml.= '<a class="btn btn-default" href="'.$url.'">'.$link.'</a>';
161+
}
162+
$buttonHtml .= '</div>'."\n\n";
163+
164+
$contents = $this->postProcessStandardElements($name, $buttonHtml . $contents);
165+
file_put_contents($documentationFile, $contents);
166+
}
167+
168+
public function buildDocsUtils()
169+
{
170+
$this->say("Util Classes");
171+
$utils = [
172+
'Autoload' => null,
173+
'Fixtures' => null,
174+
'Locator' => null,
175+
'XmlBuilder' => null,
176+
'JsonType' => 'module-rest',
177+
'HttpCode' => 'lib-innerbrowser',
178+
];
179+
//JsonType is in module-rest, HttpCode - in lib-innerbrowser
180+
181+
foreach ($utils as $utilName => $repositoryName) {
182+
$className = '\Codeception\Util\\' . $utilName;
183+
$documentationFile = 'docs/reference/' . $utilName . '.md';
184+
$this->documentApiClass($documentationFile, $className, false, $repositoryName);
185+
$this->postProcessFile($utilName, $documentationFile);
186+
}
187+
}
188+
189+
public function buildDocsCommands()
190+
{
191+
$this->say("Commands");
192+
193+
$path = __DIR__ . '/vendor/codeception/codeception/src/Codeception/Command';
194+
$commands = Finder::create()->files()->name('*.php')->depth(0)->in($path);
195+
196+
$documentationFile = 'docs/reference/Commands.md';
197+
$commandGenerator = $this->taskGenDoc($documentationFile);
198+
foreach ($commands as $command) {
199+
$commandName = basename(substr($command, 0, -4));
200+
$className = '\Codeception\Command\\' . $commandName;
201+
$commandGenerator->docClass($className);
202+
}
203+
$commandGenerator
204+
->prepend("# Console Commands\n")
205+
->processClassSignature(function ($r, $text) {
206+
return "## ".$r->getShortName();
207+
})
208+
->filterMethods(function (ReflectionMethod $r) {
209+
return false;
210+
})
211+
->run();
212+
213+
$this->postProcessFile('Commands', $documentationFile);
214+
}
215+
216+
public function buildDocsStub()
217+
{
218+
$this->say("Stub Classes");
219+
220+
$stubFile = 'docs/reference/Stub.md';
221+
$this->taskGenDoc($stubFile)
222+
->docClass('Codeception\Stub')
223+
->filterMethods(function (\ReflectionMethod $method) {
224+
if ($method->isConstructor() or $method->isDestructor()) {
225+
return false;
226+
}
227+
if (!$method->isPublic()) {
228+
return false;
229+
}
230+
if (strpos($method->name, '_') === 0) {
231+
return false;
232+
}
233+
return true;
234+
})
235+
->processMethodDocBlock(
236+
function (\ReflectionMethod $m, $doc) {
237+
$doc = str_replace(array('@since'), array(' * available since version'), $doc);
238+
$doc = str_replace(array(' @', "\n@"), array(" * ", "\n * "), $doc);
239+
return $doc;
240+
}
241+
)
242+
->processProperty(false)
243+
->run();
244+
245+
$mocksDocumentation = <<<EOF
246+
# Mocks
247+
248+
Declare mocks inside `Codeception\Test\Unit` class.
249+
If you want to use mocks outside it, check the reference for [Codeception/Stub](https://github.com/Codeception/Stub) library.
250+
EOF;
251+
252+
$mockFile = 'docs/reference/Mock.md';
253+
$this->taskGenDoc($mockFile)
254+
->docClass('Codeception\Test\Feature\Stub')
255+
->docClass('Codeception\Stub\Expected')
256+
->processClassDocBlock(false)
257+
->processClassSignature(false)
258+
->prepend($mocksDocumentation)
259+
->filterMethods(function (\ReflectionMethod $method) {
260+
if ($method->isConstructor() or $method->isDestructor()) {
261+
return false;
262+
}
263+
if (!$method->isPublic()) {
264+
return false;
265+
}
266+
if (strpos($method->name, '_') === 0) {
267+
return false;
268+
}
269+
if (strpos($method->name, 'stub') === 0) {
270+
return false;
271+
}
272+
return true;
273+
})
274+
->run();
275+
276+
$this->postProcessFile('Stub', $stubFile);
277+
$this->postProcessFile('Mock', $mockFile);
278+
}
279+
280+
public function buildDocsApi()
281+
{
282+
$this->say("API Classes");
283+
$apiClasses = ['Codeception\Module', 'Codeception\InitTemplate'];
284+
285+
foreach ($apiClasses as $apiClass) {
286+
$name = (new ReflectionClass($apiClass))->getShortName();
287+
$documentationFile = 'docs/reference/' . $name . '.md';
288+
$this->documentApiClass($documentationFile, $apiClass, true);
289+
$this->postProcessFile($name, $documentationFile);
290+
}
291+
}
292+
293+
public function buildDocsExtensions()
294+
{
295+
$this->say('Extensions');
296+
297+
$path = __DIR__ . '/vendor/codeception/codeception/ext';
298+
$extensions = Finder::create()->files()->sortByName()->name('*.php')->in($path);
299+
300+
$extGenerator= $this->taskGenDoc('_includes/extensions.md');
301+
foreach ($extensions as $extension) {
302+
$extensionName = basename(substr($extension, 0, -4));
303+
$className = '\Codeception\Extension\\' . $extensionName;
304+
$extGenerator->docClass($className);
305+
}
306+
$extGenerator
307+
->prepend("# Official Extensions\n")
308+
->processClassSignature(function (ReflectionClass $r, $text) {
309+
$name = $r->getShortName();
310+
return "## $name\n\n[See Source](" . self::REPO_BLOB_URL."/".self::STABLE_BRANCH. "/ext/$name.php)";
311+
})
312+
->filterMethods(function (ReflectionMethod $r) {
313+
return false;
314+
})
315+
->filterProperties(function ($r) {
316+
return false;
317+
})
318+
->run();
319+
}
320+
321+
protected function documentApiClass($file, $className, $all = false, $repositoryName = null)
322+
{
323+
if ($repositoryName === null ) {
324+
$repositoryUrl = self::REPO_BLOB_URL."/".self::STABLE_BRANCH;
325+
} else {
326+
$repositoryUrl = 'https://github.com/Codeception/' . $repositoryName . '/blob/master';
327+
}
328+
$uri = str_replace('\\', '/', $className);
329+
$source = $repositoryUrl . "/src$uri.php";
330+
331+
$this->taskGenDoc($file)
332+
->docClass($className)
333+
->filterMethods(function (ReflectionMethod $r) use ($all, $className) {
334+
return $all || $r->isPublic();
335+
})
336+
->append(
337+
'<p>&nbsp;</p><div class="alert alert-warning">Reference is taken from the source code. '
338+
. '<a href="' . $source . '">Help us to improve documentation. Edit module reference</a></div>'
339+
)
340+
->processPropertySignature(function ($r) {
341+
return "\n#### $" . $r->name. "\n\n";
342+
})
343+
->processPropertyDocBlock(function ($r, $text) {
344+
$modifiers = implode(' ', \Reflection::getModifierNames($r->getModifiers()));
345+
$text = ' *' . $modifiers . '* **$' . $r->name . "**\n" . $text;
346+
$text = preg_replace("~@(.*?)\s(.*)~", 'type `$2`', $text);
347+
return $text;
348+
})
349+
->processClassDocBlock(
350+
function (ReflectionClass $r, $text) {
351+
return $text . "\n";
352+
}
353+
)
354+
->processMethodSignature(function ($r, $text) {
355+
return "#### {$r->name}()\n\n" . ltrim($text, '#');
356+
})
357+
->processMethodDocBlock(
358+
function (ReflectionMethod $r, $text) use ($file, $source) {
359+
//$file = str_replace(__DIR__, '', $r->getFileName());
360+
//$source = self::REPO_BLOB_URL."/".self::STABLE_BRANCH. $file;
361+
362+
$line = $r->getStartLine();
363+
$text = preg_replace("~^\s?@(.*?)\s~m", ' * `$1` $2', $text);
364+
$text .= "\n[See source]($source#L$line)";
365+
return "\n" . $text . "\n";
366+
}
367+
)
368+
->reorderMethods('ksort')
369+
->run();
370+
}
371+
372+
/**
373+
* @param $name
374+
* @param $contents
375+
* @return mixed|string|string[]|null
376+
*/
377+
private function postProcessStandardElements($name, $contents)
378+
{
379+
$highlight_languages = implode('|', [
380+
'php',
381+
'html',
382+
'bash',
383+
'yaml',
384+
'json',
385+
'xml',
386+
'sql',
387+
'gherkin'
388+
]);
389+
$contents = preg_replace(
390+
"~```\s?($highlight_languages)\b(.*?)```~ms",
391+
"{% highlight $1 %}\n$2\n{% endhighlight %}",
392+
$contents
393+
);
394+
$contents = str_replace('{% highlight %}', '{% highlight yaml %}', $contents);
395+
$contents = preg_replace("~```\s?(.*?)```~ms", "{% highlight yaml %}\n$1\n{% endhighlight %}", $contents);
396+
// set default language in order not to leave unparsed code inside '```'
397+
398+
$title = $name;
399+
$contents = "---\nlayout: doc\ntitle: " . ($title != "" ? $title . " - " : "")
400+
. "Codeception - Documentation\n---\n\n" . $contents;
401+
return $contents;
402+
}
403+
404+
/**
405+
* @param $pageName
406+
* @param $documentationFile
407+
*/
408+
private function postProcessFile($pageName, $documentationFile)
409+
{
410+
$contents = $this->postProcessStandardElements($pageName, file_get_contents($documentationFile));
411+
file_put_contents($documentationFile, $contents);
412+
}
100413
}

composer.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "codeception/codeception.github.com",
3+
"type": "project",
4+
"description": "Dependencies are used to build docs",
5+
"license":"MIT",
6+
7+
"require": {
8+
"codeception/codeception": "4.0.x-dev | >=4.0",
9+
"codeception/module-amqp": "*",
10+
"codeception/module-apc": "*",
11+
"codeception/module-asserts": "*",
12+
"codeception/module-cli": "*",
13+
"codeception/module-datafactory": "*",
14+
"codeception/module-db": "*",
15+
"codeception/module-doctrine2": "*",
16+
"codeception/module-filesystem": "*",
17+
"codeception/module-ftp": "*",
18+
"codeception/module-laravel5": "*",
19+
"codeception/module-lumen": "*",
20+
"codeception/module-memcache": "*",
21+
"codeception/module-mongodb": "*",
22+
"codeception/module-phalcon": "*",
23+
"codeception/module-phpbrowser": "*",
24+
"codeception/module-queue": "*",
25+
"codeception/module-redis": "*",
26+
"codeception/module-rest": "*",
27+
"codeception/module-sequence": "*",
28+
"codeception/module-soap": "*",
29+
"codeception/module-symfony": "*",
30+
"codeception/module-webdriver": "*",
31+
"codeception/module-yii2": "*",
32+
"codeception/module-zendexpressive": "*",
33+
"codeception/module-zf2": "*",
34+
"codeception/util-robohelpers": "dev-master"
35+
}
36+
}

0 commit comments

Comments
 (0)