woocommerce/apigen/ApiGen/Generator.php

2122 lines
62 KiB
PHP

<?php
/**
* ApiGen 2.8.0 - API documentation generator for PHP 5.3+
*
* Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com)
* Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich)
* Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville)
*
* For the full copyright and license information, please view
* the file LICENSE.md that was distributed with this source code.
*/
namespace ApiGen;
use TokenReflection\Broker;
use Nette, FSHL;
use InvalidArgumentException, RuntimeException;
/**
* Generates a HTML API documentation.
*/
class Generator extends Nette\Object
{
/**
* Library name.
*
* @var string
*/
const NAME = 'ApiGen';
/**
* Library version.
*
* @var string
*/
const VERSION = '2.8.0';
/**
* Configuration.
*
* @var \ApiGen\Config
*/
private $config;
/**
* List of parsed classes.
*
* @var \ArrayObject
*/
private $parsedClasses = null;
/**
* List of parsed constants.
*
* @var \ArrayObject
*/
private $parsedConstants = null;
/**
* List of parsed functions.
*
* @var \ArrayObject
*/
private $parsedFunctions = null;
/**
* List of packages.
*
* @var array
*/
private $packages = array();
/**
* List of namespaces.
*
* @var array
*/
private $namespaces = array();
/**
* List of classes.
*
* @var array
*/
private $classes = array();
/**
* List of interfaces.
*
* @var array
*/
private $interfaces = array();
/**
* List of traits.
*
* @var array
*/
private $traits = array();
/**
* List of exceptions.
*
* @var array
*/
private $exceptions = array();
/**
* List of constants.
*
* @var array
*/
private $constants = array();
/**
* List of functions.
*
* @var array
*/
private $functions = array();
/**
* List of symlinks.
*
* @var array
*/
private $symlinks = array();
/**
* List of detected character sets for parsed files.
*
* @var array
*/
private $charsets = array();
/**
* Progressbar settings and status.
*
* @var array
*/
private $progressbar = array(
'skeleton' => '[%s] %\' 6.2f%% %\' 3dMB',
'width' => 80,
'bar' => 64,
'current' => 0,
'maximum' => 1
);
/**
* Sets configuration.
*
* @param array $config
*/
public function __construct(Config $config)
{
$this->config = $config;
$this->parsedClasses = new \ArrayObject();
$this->parsedConstants = new \ArrayObject();
$this->parsedFunctions = new \ArrayObject();
}
/**
* Scans and parses PHP files.
*
* @return array
* @throws \RuntimeException If no PHP files have been found.
*/
public function parse()
{
$files = array();
$flags = \RecursiveDirectoryIterator::CURRENT_AS_FILEINFO | \RecursiveDirectoryIterator::SKIP_DOTS;
if (defined('\\RecursiveDirectoryIterator::FOLLOW_SYMLINKS')) {
// Available from PHP 5.3.1
$flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS;
}
foreach ($this->config->source as $source) {
$entries = array();
if (is_dir($source)) {
foreach (new \RecursiveIteratorIterator(new SourceFilesFilterIterator(new \RecursiveDirectoryIterator($source, $flags), $this->config->exclude)) as $entry) {
if (!$entry->isFile()) {
continue;
}
$entries[] = $entry;
}
} elseif ($this->isPhar($source)) {
if (!extension_loaded('phar')) {
throw new RuntimeException('Phar extension is not loaded');
}
foreach (new \RecursiveIteratorIterator(new \Phar($source, $flags)) as $entry) {
if (!$entry->isFile()) {
continue;
}
$entries[] = $entry;
}
} else {
$entries[] = new \SplFileInfo($source);
}
$regexp = '~\\.' . implode('|', $this->config->extensions) . '$~i';
foreach ($entries as $entry) {
if (!preg_match($regexp, $entry->getFilename())) {
continue;
}
$pathName = $this->normalizePath($entry->getPathName());
$files[$pathName] = $entry->getSize();
if (false !== $entry->getRealPath() && $pathName !== $entry->getRealPath()) {
$this->symlinks[$entry->getRealPath()] = $pathName;
}
}
}
if (empty($files)) {
throw new RuntimeException('No PHP files found');
}
if ($this->config->progressbar) {
$this->prepareProgressBar(array_sum($files));
}
$broker = new Broker(new Backend($this, !empty($this->config->report)), Broker::OPTION_DEFAULT & ~(Broker::OPTION_PARSE_FUNCTION_BODY | Broker::OPTION_SAVE_TOKEN_STREAM));
$errors = array();
foreach ($files as $fileName => $size) {
$content = file_get_contents($fileName);
$charset = $this->detectCharset($content);
$this->charsets[$fileName] = $charset;
$content = $this->toUtf($content, $charset);
try {
$broker->processString($content, $fileName);
} catch (\Exception $e) {
$errors[] = $e;
}
$this->incrementProgressBar($size);
$this->checkMemory();
}
// Classes
$this->parsedClasses->exchangeArray($broker->getClasses(Backend::TOKENIZED_CLASSES | Backend::INTERNAL_CLASSES | Backend::NONEXISTENT_CLASSES));
$this->parsedClasses->uksort('strcasecmp');
// Constants
$this->parsedConstants->exchangeArray($broker->getConstants());
$this->parsedConstants->uksort('strcasecmp');
// Functions
$this->parsedFunctions->exchangeArray($broker->getFunctions());
$this->parsedFunctions->uksort('strcasecmp');
$documentedCounter = function($count, $element) {
return $count += (int) $element->isDocumented();
};
return (object) array(
'classes' => count($broker->getClasses(Backend::TOKENIZED_CLASSES)),
'constants' => count($this->parsedConstants),
'functions' => count($this->parsedFunctions),
'internalClasses' => count($broker->getClasses(Backend::INTERNAL_CLASSES)),
'documentedClasses' => array_reduce($broker->getClasses(Backend::TOKENIZED_CLASSES), $documentedCounter),
'documentedConstants' => array_reduce($this->parsedConstants->getArrayCopy(), $documentedCounter),
'documentedFunctions' => array_reduce($this->parsedFunctions->getArrayCopy(), $documentedCounter),
'documentedInternalClasses' => array_reduce($broker->getClasses(Backend::INTERNAL_CLASSES), $documentedCounter),
'errors' => $errors
);
}
/**
* Returns configuration.
*
* @return mixed
*/
public function getConfig()
{
return $this->config;
}
/**
* Returns parsed class list.
*
* @return \ArrayObject
*/
public function getParsedClasses()
{
return $this->parsedClasses;
}
/**
* Returns parsed constant list.
*
* @return \ArrayObject
*/
public function getParsedConstants()
{
return $this->parsedConstants;
}
/**
* Returns parsed function list.
*
* @return \ArrayObject
*/
public function getParsedFunctions()
{
return $this->parsedFunctions;
}
/**
* Wipes out the destination directory.
*
* @return boolean
*/
public function wipeOutDestination()
{
foreach ($this->getGeneratedFiles() as $path) {
if (is_file($path) && !@unlink($path)) {
return false;
}
}
$archive = $this->getArchivePath();
if (is_file($archive) && !@unlink($archive)) {
return false;
}
return true;
}
/**
* Generates API documentation.
*
* @throws \RuntimeException If destination directory is not writable.
*/
public function generate()
{
@mkdir($this->config->destination, 0755, true);
if (!is_dir($this->config->destination) || !is_writable($this->config->destination)) {
throw new RuntimeException(sprintf('Directory "%s" isn\'t writable', $this->config->destination));
}
// Copy resources
foreach ($this->config->template['resources'] as $resourceSource => $resourceDestination) {
// File
$resourcePath = $this->getTemplateDir() . DIRECTORY_SEPARATOR . $resourceSource;
if (is_file($resourcePath)) {
copy($resourcePath, $this->forceDir($this->config->destination . DIRECTORY_SEPARATOR . $resourceDestination));
continue;
}
// Dir
$iterator = Nette\Utils\Finder::findFiles('*')->from($resourcePath)->getIterator();
foreach ($iterator as $item) {
copy($item->getPathName(), $this->forceDir($this->config->destination . DIRECTORY_SEPARATOR . $resourceDestination . DIRECTORY_SEPARATOR . $iterator->getSubPathName()));
}
}
// Categorize by packages and namespaces
$this->categorize();
// Prepare progressbar
if ($this->config->progressbar) {
$max = count($this->packages)
+ count($this->namespaces)
+ count($this->classes)
+ count($this->interfaces)
+ count($this->traits)
+ count($this->exceptions)
+ count($this->constants)
+ count($this->functions)
+ count($this->config->template['templates']['common'])
+ (int) !empty($this->config->report)
+ (int) $this->config->tree
+ (int) $this->config->deprecated
+ (int) $this->config->todo
+ (int) $this->config->download
+ (int) $this->isSitemapEnabled()
+ (int) $this->isOpensearchEnabled()
+ (int) $this->isRobotsEnabled();
if ($this->config->sourceCode) {
$tokenizedFilter = function(ReflectionClass $class) {
return $class->isTokenized();
};
$max += count(array_filter($this->classes, $tokenizedFilter))
+ count(array_filter($this->interfaces, $tokenizedFilter))
+ count(array_filter($this->traits, $tokenizedFilter))
+ count(array_filter($this->exceptions, $tokenizedFilter))
+ count($this->constants)
+ count($this->functions);
unset($tokenizedFilter);
}
$this->prepareProgressBar($max);
}
// Prepare template
$tmp = $this->config->destination . DIRECTORY_SEPARATOR . 'tmp';
$this->deleteDir($tmp);
@mkdir($tmp, 0755, true);
$template = new Template($this);
$template->setCacheStorage(new Nette\Caching\Storages\PhpFileStorage($tmp));
$template->generator = self::NAME;
$template->version = self::VERSION;
$template->config = $this->config;
$this->registerCustomTemplateMacros($template);
// Common files
$this->generateCommon($template);
// Optional files
$this->generateOptional($template);
// List of poorly documented elements
if (!empty($this->config->report)) {
$this->generateReport();
}
// List of deprecated elements
if ($this->config->deprecated) {
$this->generateDeprecated($template);
}
// List of tasks
if ($this->config->todo) {
$this->generateTodo($template);
}
// Classes/interfaces/traits/exceptions tree
if ($this->config->tree) {
$this->generateTree($template);
}
// Generate packages summary
$this->generatePackages($template);
// Generate namespaces summary
$this->generateNamespaces($template);
// Generate classes, interfaces, traits, exceptions, constants and functions files
$this->generateElements($template);
// Generate ZIP archive
if ($this->config->download) {
$this->generateArchive();
}
// Delete temporary directory
$this->deleteDir($tmp);
}
/**
* Loads template-specific macro and helper libraries.
*
* @param \ApiGen\Template $template Template instance
*/
private function registerCustomTemplateMacros(Template $template)
{
$latte = new Nette\Latte\Engine();
if (!empty($this->config->template['options']['extensions'])) {
$this->output("Loading custom template macro and helper libraries\n");
$broker = new Broker(new Broker\Backend\Memory(), 0);
$baseDir = dirname($this->config->template['config']);
foreach ((array) $this->config->template['options']['extensions'] as $fileName) {
$pathName = $baseDir . DIRECTORY_SEPARATOR . $fileName;
if (is_file($pathName)) {
try {
$reflectionFile = $broker->processFile($pathName, true);
foreach ($reflectionFile->getNamespaces() as $namespace) {
foreach ($namespace->getClasses() as $class) {
if ($class->isSubclassOf('ApiGen\\MacroSet')) {
// Macro set
include $pathName;
call_user_func(array($class->getName(), 'install'), $latte->compiler);
$this->output(sprintf(" %s (macro set)\n", $class->getName()));
} elseif ($class->implementsInterface('ApiGen\\IHelperSet')) {
// Helpers set
include $pathName;
$className = $class->getName();
$template->registerHelperLoader(callback(new $className($template), 'loader'));
$this->output(sprintf(" %s (helper set)\n", $class->getName()));
}
}
}
} catch (\Exception $e) {
throw new \Exception(sprintf('Could not load macros and helpers from file "%s"', $pathName), 0, $e);
}
} else {
throw new \Exception(sprintf('Helper file "%s" does not exist.', $pathName));
}
}
}
$template->registerFilter($latte);
}
/**
* Categorizes by packages and namespaces.
*
* @return \ApiGen\Generator
*/
private function categorize()
{
foreach (array('classes', 'constants', 'functions') as $type) {
foreach ($this->{'parsed' . ucfirst($type)} as $elementName => $element) {
if (!$element->isDocumented()) {
continue;
}
$packageName = $element->getPseudoPackageName();
$namespaceName = $element->getPseudoNamespaceName();
if ($element instanceof ReflectionConstant) {
$this->constants[$elementName] = $element;
$this->packages[$packageName]['constants'][$elementName] = $element;
$this->namespaces[$namespaceName]['constants'][$element->getShortName()] = $element;
} elseif ($element instanceof ReflectionFunction) {
$this->functions[$elementName] = $element;
$this->packages[$packageName]['functions'][$elementName] = $element;
$this->namespaces[$namespaceName]['functions'][$element->getShortName()] = $element;
} elseif ($element->isInterface()) {
$this->interfaces[$elementName] = $element;
$this->packages[$packageName]['interfaces'][$elementName] = $element;
$this->namespaces[$namespaceName]['interfaces'][$element->getShortName()] = $element;
} elseif ($element->isTrait()) {
$this->traits[$elementName] = $element;
$this->packages[$packageName]['traits'][$elementName] = $element;
$this->namespaces[$namespaceName]['traits'][$element->getShortName()] = $element;
} elseif ($element->isException()) {
$this->exceptions[$elementName] = $element;
$this->packages[$packageName]['exceptions'][$elementName] = $element;
$this->namespaces[$namespaceName]['exceptions'][$element->getShortName()] = $element;
} else {
$this->classes[$elementName] = $element;
$this->packages[$packageName]['classes'][$elementName] = $element;
$this->namespaces[$namespaceName]['classes'][$element->getShortName()] = $element;
}
}
}
// Select only packages or namespaces
$userPackagesCount = count(array_diff(array_keys($this->packages), array('PHP', 'None')));
$userNamespacesCount = count(array_diff(array_keys($this->namespaces), array('PHP', 'None')));
$namespacesEnabled = ('auto' === $this->config->groups && ($userNamespacesCount > 0 || 0 === $userPackagesCount)) || 'namespaces' === $this->config->groups;
$packagesEnabled = ('auto' === $this->config->groups && !$namespacesEnabled) || 'packages' === $this->config->groups;
if ($namespacesEnabled) {
$this->packages = array();
$this->namespaces = $this->sortGroups($this->namespaces);
} elseif ($packagesEnabled) {
$this->namespaces = array();
$this->packages = $this->sortGroups($this->packages);
} else {
$this->namespaces = array();
$this->packages = array();
}
return $this;
}
/**
* Sorts and filters groups.
*
* @param array $groups
* @return array
*/
private function sortGroups(array $groups)
{
// Don't generate only 'None' groups
if (1 === count($groups) && isset($groups['None'])) {
return array();
}
$emptyList = array('classes' => array(), 'interfaces' => array(), 'traits' => array(), 'exceptions' => array(), 'constants' => array(), 'functions' => array());
$groupNames = array_keys($groups);
$lowerGroupNames = array_flip(array_map(function($y) {
return strtolower($y);
}, $groupNames));
foreach ($groupNames as $groupName) {
// Add missing parent groups
$parent = '';
foreach (explode('\\', $groupName) as $part) {
$parent = ltrim($parent . '\\' . $part, '\\');
if (!isset($lowerGroupNames[strtolower($parent)])) {
$groups[$parent] = $emptyList;
}
}
// Add missing element types
foreach ($this->getElementTypes() as $type) {
if (!isset($groups[$groupName][$type])) {
$groups[$groupName][$type] = array();
}
}
}
$main = $this->config->main;
uksort($groups, function($one, $two) use ($main) {
// \ as separator has to be first
$one = str_replace('\\', ' ', $one);
$two = str_replace('\\', ' ', $two);
if ($main) {
if (0 === strpos($one, $main) && 0 !== strpos($two, $main)) {
return -1;
} elseif (0 !== strpos($one, $main) && 0 === strpos($two, $main)) {
return 1;
}
}
return strcasecmp($one, $two);
});
return $groups;
}
/**
* Generates common files.
*
* @param \ApiGen\Template $template Template
* @return \ApiGen\Generator
*/
private function generateCommon(Template $template)
{
$template->namespace = null;
$template->namespaces = array_keys($this->namespaces);
$template->package = null;
$template->packages = array_keys($this->packages);
$template->class = null;
$template->classes = array_filter($this->classes, $this->getMainFilter());
$template->interfaces = array_filter($this->interfaces, $this->getMainFilter());
$template->traits = array_filter($this->traits, $this->getMainFilter());
$template->exceptions = array_filter($this->exceptions, $this->getMainFilter());
$template->constant = null;
$template->constants = array_filter($this->constants, $this->getMainFilter());
$template->function = null;
$template->functions = array_filter($this->functions, $this->getMainFilter());
$template->archive = basename($this->getArchivePath());
// Elements for autocomplete
$elements = array();
$autocomplete = array_flip($this->config->autocomplete);
foreach ($this->getElementTypes() as $type) {
foreach ($this->$type as $element) {
if ($element instanceof ReflectionClass) {
if (isset($autocomplete['classes'])) {
$elements[] = array('c', $element->getPrettyName());
}
if (isset($autocomplete['methods'])) {
foreach ($element->getOwnMethods() as $method) {
$elements[] = array('m', $method->getPrettyName());
}
foreach ($element->getOwnMagicMethods() as $method) {
$elements[] = array('mm', $method->getPrettyName());
}
}
if (isset($autocomplete['properties'])) {
foreach ($element->getOwnProperties() as $property) {
$elements[] = array('p', $property->getPrettyName());
}
foreach ($element->getOwnMagicProperties() as $property) {
$elements[] = array('mp', $property->getPrettyName());
}
}
if (isset($autocomplete['classconstants'])) {
foreach ($element->getOwnConstants() as $constant) {
$elements[] = array('cc', $constant->getPrettyName());
}
}
} elseif ($element instanceof ReflectionConstant && isset($autocomplete['constants'])) {
$elements[] = array('co', $element->getPrettyName());
} elseif ($element instanceof ReflectionFunction && isset($autocomplete['functions'])) {
$elements[] = array('f', $element->getPrettyName());
}
}
}
usort($elements, function($one, $two) {
return strcasecmp($one[1], $two[1]);
});
$template->elements = $elements;
foreach ($this->config->template['templates']['common'] as $source => $destination) {
$template
->setFile($this->getTemplateDir() . DIRECTORY_SEPARATOR . $source)
->save($this->forceDir($this->config->destination . DIRECTORY_SEPARATOR . $destination));
$this->incrementProgressBar();
}
unset($template->elements);
$this->checkMemory();
return $this;
}
/**
* Generates optional files.
*
* @param \ApiGen\Template $template Template
* @return \ApiGen\Generator
*/
private function generateOptional(Template $template)
{
if ($this->isSitemapEnabled()) {
$template
->setFile($this->getTemplatePath('sitemap', 'optional'))
->save($this->forceDir($this->getTemplateFileName('sitemap', 'optional')));
$this->incrementProgressBar();
}
if ($this->isOpensearchEnabled()) {
$template
->setFile($this->getTemplatePath('opensearch', 'optional'))
->save($this->forceDir($this->getTemplateFileName('opensearch', 'optional')));
$this->incrementProgressBar();
}
if ($this->isRobotsEnabled()) {
$template
->setFile($this->getTemplatePath('robots', 'optional'))
->save($this->forceDir($this->getTemplateFileName('robots', 'optional')));
$this->incrementProgressBar();
}
$this->checkMemory();
return $this;
}
/**
* Generates list of poorly documented elements.
*
* @return \ApiGen\Generator
* @throws \RuntimeException If file isn't writable.
*/
private function generateReport()
{
// Function for element labels
$that = $this;
$labeler = function($element) use ($that) {
if ($element instanceof ReflectionClass) {
if ($element->isInterface()) {
$label = 'interface';
} elseif ($element->isTrait()) {
$label = 'trait';
} elseif ($element->isException()) {
$label = 'exception';
} else {
$label = 'class';
}
} elseif ($element instanceof ReflectionMethod) {
$label = 'method';
} elseif ($element instanceof ReflectionFunction) {
$label = 'function';
} elseif ($element instanceof ReflectionConstant) {
$label = 'constant';
} elseif ($element instanceof ReflectionProperty) {
$label = 'property';
} elseif ($element instanceof ReflectionParameter) {
$label = 'parameter';
}
return sprintf('%s %s', $label, $element->getPrettyName());
};
$list = array();
foreach ($this->getElementTypes() as $type) {
foreach ($this->$type as $parentElement) {
$fileName = $this->unPharPath($parentElement->getFileName());
if (!$parentElement->isValid()) {
$list[$fileName][] = array('error', 0, sprintf('Duplicate %s', $labeler($parentElement)));
continue;
}
// Skip elements not from the main project
if (!$parentElement->isMain()) {
continue;
}
// Internal elements don't have documentation
if ($parentElement->isInternal()) {
continue;
}
$elements = array($parentElement);
if ($parentElement instanceof ReflectionClass) {
$elements = array_merge(
$elements,
array_values($parentElement->getOwnMethods()),
array_values($parentElement->getOwnConstants()),
array_values($parentElement->getOwnProperties())
);
}
$tokens = $parentElement->getBroker()->getFileTokens($parentElement->getFileName());
foreach ($elements as $element) {
$line = $element->getStartLine();
$label = $labeler($element);
$annotations = $element->getAnnotations();
// Documentation
if (empty($element->shortDescription)) {
if (empty($annotations)) {
$list[$fileName][] = array('error', $line, sprintf('Missing documentation of %s', $label));
continue;
}
// Description
$list[$fileName][] = array('error', $line, sprintf('Missing description of %s', $label));
}
// Documentation of method
if ($element instanceof ReflectionMethod || $element instanceof ReflectionFunction) {
// Parameters
$unlimited = false;
foreach ($element->getParameters() as $no => $parameter) {
if (!isset($annotations['param'][$no])) {
$list[$fileName][] = array('error', $line, sprintf('Missing documentation of %s', $labeler($parameter)));
continue;
}
if (!preg_match('~^[\\w\\\\]+(?:\\[\\])?(?:\\|[\\w\\\\]+(?:\\[\\])?)*(?:\\s+\\$' . $parameter->getName() . ($parameter->isUnlimited() ? ',\\.{3}' : '') . ')?(?:\\s+.+)?$~s', $annotations['param'][$no])) {
$list[$fileName][] = array('warning', $line, sprintf('Invalid documentation "%s" of %s', $annotations['param'][$no], $labeler($parameter)));
}
if ($unlimited && $parameter->isUnlimited()) {
$list[$fileName][] = array('warning', $line, sprintf('More than one unlimited parameters of %s', $labeler($element)));
} elseif ($parameter->isUnlimited()) {
$unlimited = true;
}
unset($annotations['param'][$no]);
}
if (isset($annotations['param'])) {
foreach ($annotations['param'] as $annotation) {
$list[$fileName][] = array('warning', $line, sprintf('Existing documentation "%s" of nonexistent parameter of %s', $annotation, $label));
}
}
// Return values
$return = false;
$tokens->seek($element->getStartPosition())
->find(T_FUNCTION);
while ($tokens->next() && $tokens->key() < $element->getEndPosition()) {
$type = $tokens->getType();
if (T_FUNCTION === $type) {
// Skip annonymous functions
$tokens->find('{')->findMatchingBracket();
} elseif (T_RETURN === $type && !$tokens->skipWhitespaces()->is(';')) {
// Skip return without return value
$return = true;
break;
}
}
if ($return && !isset($annotations['return'])) {
$list[$fileName][] = array('error', $line, sprintf('Missing documentation of return value of %s', $label));
} elseif (isset($annotations['return'])) {
if (!$return && 'void' !== $annotations['return'][0] && ($element instanceof ReflectionFunction || (!$parentElement->isInterface() && !$element->isAbstract()))) {
$list[$fileName][] = array('warning', $line, sprintf('Existing documentation "%s" of nonexistent return value of %s', $annotations['return'][0], $label));
} elseif (!preg_match('~^[\\w\\\\]+(?:\\[\\])?(?:\\|[\\w\\\\]+(?:\\[\\])?)*(?:\\s+.+)?$~s', $annotations['return'][0])) {
$list[$fileName][] = array('warning', $line, sprintf('Invalid documentation "%s" of return value of %s', $annotations['return'][0], $label));
}
}
if (isset($annotations['return'][1])) {
$list[$fileName][] = array('warning', $line, sprintf('Duplicate documentation "%s" of return value of %s', $annotations['return'][1], $label));
}
// Throwing exceptions
$throw = false;
$tokens->seek($element->getStartPosition())
->find(T_FUNCTION);
while ($tokens->next() && $tokens->key() < $element->getEndPosition()) {
$type = $tokens->getType();
if (T_TRY === $type) {
// Skip try
$tokens->find('{')->findMatchingBracket();
} elseif (T_THROW === $type) {
$throw = true;
break;
}
}
if ($throw && !isset($annotations['throws'])) {
$list[$fileName][] = array('error', $line, sprintf('Missing documentation of throwing an exception of %s', $label));
} elseif (isset($annotations['throws']) && !preg_match('~^[\\w\\\\]+(?:\\|[\\w\\\\]+)*(?:\\s+.+)?$~s', $annotations['throws'][0])) {
$list[$fileName][] = array('warning', $line, sprintf('Invalid documentation "%s" of throwing an exception of %s', $annotations['throws'][0], $label));
}
}
// Data type of constants & properties
if ($element instanceof ReflectionProperty || $element instanceof ReflectionConstant) {
if (!isset($annotations['var'])) {
$list[$fileName][] = array('error', $line, sprintf('Missing documentation of the data type of %s', $label));
} elseif (!preg_match('~^[\\w\\\\]+(?:\\[\\])?(?:\\|[\\w\\\\]+(?:\\[\\])?)*(?:\\s+.+)?$~s', $annotations['var'][0])) {
$list[$fileName][] = array('warning', $line, sprintf('Invalid documentation "%s" of the data type of %s', $annotations['var'][0], $label));
}
if (isset($annotations['var'][1])) {
$list[$fileName][] = array('warning', $line, sprintf('Duplicate documentation "%s" of the data type of %s', $annotations['var'][1], $label));
}
}
}
unset($tokens);
}
}
uksort($list, 'strcasecmp');
$file = @fopen($this->config->report, 'w');
if (false === $file) {
throw new RuntimeException(sprintf('File "%s" isn\'t writable', $this->config->report));
}
fwrite($file, sprintf('<?xml version="1.0" encoding="UTF-8"?>%s', "\n"));
fwrite($file, sprintf('<checkstyle version="1.3.0">%s', "\n"));
foreach ($list as $fileName => $reports) {
fwrite($file, sprintf('%s<file name="%s">%s', "\t", $fileName, "\n"));
// Sort by line
usort($reports, function($one, $two) {
return strnatcmp($one[1], $two[1]);
});
foreach ($reports as $report) {
list($severity, $line, $message) = $report;
$message = preg_replace('~\\s+~u', ' ', $message);
fwrite($file, sprintf('%s<error severity="%s" line="%s" message="%s" source="ApiGen.Documentation.Documentation"/>%s', "\t\t", $severity, $line, htmlspecialchars($message), "\n"));
}
fwrite($file, sprintf('%s</file>%s', "\t", "\n"));
}
fwrite($file, sprintf('</checkstyle>%s', "\n"));
fclose($file);
$this->incrementProgressBar();
$this->checkMemory();
return $this;
}
/**
* Generates list of deprecated elements.
*
* @param \ApiGen\Template $template Template
* @return \ApiGen\Generator
* @throws \RuntimeException If template is not set.
*/
private function generateDeprecated(Template $template)
{
$this->prepareTemplate('deprecated');
$deprecatedFilter = function($element) {
return $element->isDeprecated();
};
$template->deprecatedMethods = array();
$template->deprecatedConstants = array();
$template->deprecatedProperties = array();
foreach (array_reverse($this->getElementTypes()) as $type) {
$template->{'deprecated' . ucfirst($type)} = array_filter(array_filter($this->$type, $this->getMainFilter()), $deprecatedFilter);
if ('constants' === $type || 'functions' === $type) {
continue;
}
foreach ($this->$type as $class) {
if (!$class->isMain()) {
continue;
}
if ($class->isDeprecated()) {
continue;
}
$template->deprecatedMethods = array_merge($template->deprecatedMethods, array_values(array_filter($class->getOwnMethods(), $deprecatedFilter)));
$template->deprecatedConstants = array_merge($template->deprecatedConstants, array_values(array_filter($class->getOwnConstants(), $deprecatedFilter)));
$template->deprecatedProperties = array_merge($template->deprecatedProperties, array_values(array_filter($class->getOwnProperties(), $deprecatedFilter)));
}
}
usort($template->deprecatedMethods, array($this, 'sortMethods'));
usort($template->deprecatedConstants, array($this, 'sortConstants'));
usort($template->deprecatedFunctions, array($this, 'sortFunctions'));
usort($template->deprecatedProperties, array($this, 'sortProperties'));
$template
->setFile($this->getTemplatePath('deprecated'))
->save($this->forceDir($this->getTemplateFileName('deprecated')));
foreach ($this->getElementTypes() as $type) {
unset($template->{'deprecated' . ucfirst($type)});
}
unset($template->deprecatedMethods);
unset($template->deprecatedProperties);
$this->incrementProgressBar();
$this->checkMemory();
return $this;
}
/**
* Generates list of tasks.
*
* @param \ApiGen\Template $template Template
* @return \ApiGen\Generator
* @throws \RuntimeException If template is not set.
*/
private function generateTodo(Template $template)
{
$this->prepareTemplate('todo');
$todoFilter = function($element) {
return $element->hasAnnotation('todo');
};
$template->todoMethods = array();
$template->todoConstants = array();
$template->todoProperties = array();
foreach (array_reverse($this->getElementTypes()) as $type) {
$template->{'todo' . ucfirst($type)} = array_filter(array_filter($this->$type, $this->getMainFilter()), $todoFilter);
if ('constants' === $type || 'functions' === $type) {
continue;
}
foreach ($this->$type as $class) {
if (!$class->isMain()) {
continue;
}
$template->todoMethods = array_merge($template->todoMethods, array_values(array_filter($class->getOwnMethods(), $todoFilter)));
$template->todoConstants = array_merge($template->todoConstants, array_values(array_filter($class->getOwnConstants(), $todoFilter)));
$template->todoProperties = array_merge($template->todoProperties, array_values(array_filter($class->getOwnProperties(), $todoFilter)));
}
}
usort($template->todoMethods, array($this, 'sortMethods'));
usort($template->todoConstants, array($this, 'sortConstants'));
usort($template->todoFunctions, array($this, 'sortFunctions'));
usort($template->todoProperties, array($this, 'sortProperties'));
$template
->setFile($this->getTemplatePath('todo'))
->save($this->forceDir($this->getTemplateFileName('todo')));
foreach ($this->getElementTypes() as $type) {
unset($template->{'todo' . ucfirst($type)});
}
unset($template->todoMethods);
unset($template->todoProperties);
$this->incrementProgressBar();
$this->checkMemory();
return $this;
}
/**
* Generates classes/interfaces/traits/exceptions tree.
*
* @param \ApiGen\Template $template Template
* @return \ApiGen\Generator
* @throws \RuntimeException If template is not set.
*/
private function generateTree(Template $template)
{
$this->prepareTemplate('tree');
$classTree = array();
$interfaceTree = array();
$traitTree = array();
$exceptionTree = array();
$processed = array();
foreach ($this->parsedClasses as $className => $reflection) {
if (!$reflection->isMain() || !$reflection->isDocumented() || isset($processed[$className])) {
continue;
}
if (null === $reflection->getParentClassName()) {
// No parent classes
if ($reflection->isInterface()) {
$t = &$interfaceTree;
} elseif ($reflection->isTrait()) {
$t = &$traitTree;
} elseif ($reflection->isException()) {
$t = &$exceptionTree;
} else {
$t = &$classTree;
}
} else {
foreach (array_values(array_reverse($reflection->getParentClasses())) as $level => $parent) {
if (0 === $level) {
// The topmost parent decides about the reflection type
if ($parent->isInterface()) {
$t = &$interfaceTree;
} elseif ($parent->isTrait()) {
$t = &$traitTree;
} elseif ($parent->isException()) {
$t = &$exceptionTree;
} else {
$t = &$classTree;
}
}
$parentName = $parent->getName();
if (!isset($t[$parentName])) {
$t[$parentName] = array();
$processed[$parentName] = true;
ksort($t, SORT_STRING);
}
$t = &$t[$parentName];
}
}
$t[$className] = array();
ksort($t, SORT_STRING);
$processed[$className] = true;
unset($t);
}
$template->classTree = new Tree($classTree, $this->parsedClasses);
$template->interfaceTree = new Tree($interfaceTree, $this->parsedClasses);
$template->traitTree = new Tree($traitTree, $this->parsedClasses);
$template->exceptionTree = new Tree($exceptionTree, $this->parsedClasses);
$template
->setFile($this->getTemplatePath('tree'))
->save($this->forceDir($this->getTemplateFileName('tree')));
unset($template->classTree);
unset($template->interfaceTree);
unset($template->traitTree);
unset($template->exceptionTree);
$this->incrementProgressBar();
$this->checkMemory();
return $this;
}
/**
* Generates packages summary.
*
* @param \ApiGen\Template $template Template
* @return \ApiGen\Generator
* @throws \RuntimeException If template is not set.
*/
private function generatePackages(Template $template)
{
if (empty($this->packages)) {
return $this;
}
$this->prepareTemplate('package');
$template->namespace = null;
foreach ($this->packages as $packageName => $package) {
$template->package = $packageName;
$template->subpackages = array_filter($template->packages, function($subpackageName) use ($packageName) {
return (bool) preg_match('~^' . preg_quote($packageName) . '\\\\[^\\\\]+$~', $subpackageName);
});
$template->classes = $package['classes'];
$template->interfaces = $package['interfaces'];
$template->traits = $package['traits'];
$template->exceptions = $package['exceptions'];
$template->constants = $package['constants'];
$template->functions = $package['functions'];
$template
->setFile($this->getTemplatePath('package'))
->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getPackageUrl($packageName));
$this->incrementProgressBar();
}
unset($template->subpackages);
$this->checkMemory();
return $this;
}
/**
* Generates namespaces summary.
*
* @param \ApiGen\Template $template Template
* @return \ApiGen\Generator
* @throws \RuntimeException If template is not set.
*/
private function generateNamespaces(Template $template)
{
if (empty($this->namespaces)) {
return $this;
}
$this->prepareTemplate('namespace');
$template->package = null;
foreach ($this->namespaces as $namespaceName => $namespace) {
$template->namespace = $namespaceName;
$template->subnamespaces = array_filter($template->namespaces, function($subnamespaceName) use ($namespaceName) {
return (bool) preg_match('~^' . preg_quote($namespaceName) . '\\\\[^\\\\]+$~', $subnamespaceName);
});
$template->classes = $namespace['classes'];
$template->interfaces = $namespace['interfaces'];
$template->traits = $namespace['traits'];
$template->exceptions = $namespace['exceptions'];
$template->constants = $namespace['constants'];
$template->functions = $namespace['functions'];
$template
->setFile($this->getTemplatePath('namespace'))
->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getNamespaceUrl($namespaceName));
$this->incrementProgressBar();
}
unset($template->subnamespaces);
$this->checkMemory();
return $this;
}
/**
* Generate classes, interfaces, traits, exceptions, constants and functions files.
*
* @param Template $template Template
* @return \ApiGen\Generator
* @throws \RuntimeException If template is not set.
*/
private function generateElements(Template $template)
{
if (!empty($this->classes) || !empty($this->interfaces) || !empty($this->traits) || !empty($this->exceptions)) {
$this->prepareTemplate('class');
}
if (!empty($this->constants)) {
$this->prepareTemplate('constant');
}
if (!empty($this->functions)) {
$this->prepareTemplate('function');
}
if ($this->config->sourceCode) {
$this->prepareTemplate('source');
$fshl = new FSHL\Highlighter(new FSHL\Output\Html(), FSHL\Highlighter::OPTION_TAB_INDENT | FSHL\Highlighter::OPTION_LINE_COUNTER);
$fshl->setLexer(new FSHL\Lexer\Php());
}
// Add @usedby annotation
foreach ($this->getElementTypes() as $type) {
foreach ($this->$type as $parentElement) {
$elements = array($parentElement);
if ($parentElement instanceof ReflectionClass) {
$elements = array_merge(
$elements,
array_values($parentElement->getOwnMethods()),
array_values($parentElement->getOwnConstants()),
array_values($parentElement->getOwnProperties())
);
}
foreach ($elements as $element) {
$uses = $element->getAnnotation('uses');
if (null === $uses) {
continue;
}
foreach ($uses as $value) {
list($link, $description) = preg_split('~\s+|$~', $value, 2);
$resolved = $this->resolveElement($link, $element);
if (null !== $resolved) {
$resolved->addAnnotation('usedby', $element->getPrettyName() . ' ' . $description);
}
}
}
}
}
$template->package = null;
$template->namespace = null;
$template->classes = $this->classes;
$template->interfaces = $this->interfaces;
$template->traits = $this->traits;
$template->exceptions = $this->exceptions;
$template->constants = $this->constants;
$template->functions = $this->functions;
foreach ($this->getElementTypes() as $type) {
foreach ($this->$type as $element) {
if (!empty($this->namespaces)) {
$template->namespace = $namespaceName = $element->getPseudoNamespaceName();
$template->classes = $this->namespaces[$namespaceName]['classes'];
$template->interfaces = $this->namespaces[$namespaceName]['interfaces'];
$template->traits = $this->namespaces[$namespaceName]['traits'];
$template->exceptions = $this->namespaces[$namespaceName]['exceptions'];
$template->constants = $this->namespaces[$namespaceName]['constants'];
$template->functions = $this->namespaces[$namespaceName]['functions'];
} elseif (!empty($this->packages)) {
$template->package = $packageName = $element->getPseudoPackageName();
$template->classes = $this->packages[$packageName]['classes'];
$template->interfaces = $this->packages[$packageName]['interfaces'];
$template->traits = $this->packages[$packageName]['traits'];
$template->exceptions = $this->packages[$packageName]['exceptions'];
$template->constants = $this->packages[$packageName]['constants'];
$template->functions = $this->packages[$packageName]['functions'];
}
$template->class = null;
$template->constant = null;
$template->function = null;
if ($element instanceof ReflectionClass) {
// Class
$template->tree = array_merge(array_reverse($element->getParentClasses()), array($element));
$template->directSubClasses = $element->getDirectSubClasses();
uksort($template->directSubClasses, 'strcasecmp');
$template->indirectSubClasses = $element->getIndirectSubClasses();
uksort($template->indirectSubClasses, 'strcasecmp');
$template->directImplementers = $element->getDirectImplementers();
uksort($template->directImplementers, 'strcasecmp');
$template->indirectImplementers = $element->getIndirectImplementers();
uksort($template->indirectImplementers, 'strcasecmp');
$template->directUsers = $element->getDirectUsers();
uksort($template->directUsers, 'strcasecmp');
$template->indirectUsers = $element->getIndirectUsers();
uksort($template->indirectUsers, 'strcasecmp');
$template->class = $element;
$template
->setFile($this->getTemplatePath('class'))
->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getClassUrl($element));
} elseif ($element instanceof ReflectionConstant) {
// Constant
$template->constant = $element;
$template
->setFile($this->getTemplatePath('constant'))
->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getConstantUrl($element));
} elseif ($element instanceof ReflectionFunction) {
// Function
$template->function = $element;
$template
->setFile($this->getTemplatePath('function'))
->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getFunctionUrl($element));
}
$this->incrementProgressBar();
// Generate source codes
if ($this->config->sourceCode && $element->isTokenized()) {
$template->fileName = $this->getRelativePath($element->getFileName());
$template->source = $fshl->highlight($this->toUtf(file_get_contents($element->getFileName()), $this->charsets[$element->getFileName()]));
$template
->setFile($this->getTemplatePath('source'))
->save($this->config->destination . DIRECTORY_SEPARATOR . $template->getSourceUrl($element, false));
$this->incrementProgressBar();
}
$this->checkMemory();
}
}
return $this;
}
/**
* Creates ZIP archive.
*
* @return \ApiGen\Generator
* @throws \RuntimeException If something went wrong.
*/
private function generateArchive()
{
if (!extension_loaded('zip')) {
throw new RuntimeException('Extension zip is not loaded');
}
$archive = new \ZipArchive();
if (true !== $archive->open($this->getArchivePath(), \ZipArchive::CREATE)) {
throw new RuntimeException('Could not open ZIP archive');
}
$archive->setArchiveComment(trim(sprintf('%s API documentation generated by %s %s on %s', $this->config->title, self::NAME, self::VERSION, date('Y-m-d H:i:s'))));
$directory = Nette\Utils\Strings::webalize(trim(sprintf('%s API documentation', $this->config->title)), null, false);
$destinationLength = strlen($this->config->destination);
foreach ($this->getGeneratedFiles() as $file) {
if (is_file($file)) {
$archive->addFile($file, $directory . DIRECTORY_SEPARATOR . substr($file, $destinationLength + 1));
}
}
if (false === $archive->close()) {
throw new RuntimeException('Could not save ZIP archive');
}
$this->incrementProgressBar();
$this->checkMemory();
return $this;
}
/**
* Tries to resolve string as class, interface or exception name.
*
* @param string $className Class name description
* @param string $namespace Namespace name
* @return \ApiGen\ReflectionClass
*/
public function getClass($className, $namespace = '')
{
if (isset($this->parsedClasses[$namespace . '\\' . $className])) {
$class = $this->parsedClasses[$namespace . '\\' . $className];
} elseif (isset($this->parsedClasses[ltrim($className, '\\')])) {
$class = $this->parsedClasses[ltrim($className, '\\')];
} else {
return null;
}
// Class is not "documented"
if (!$class->isDocumented()) {
return null;
}
return $class;
}
/**
* Tries to resolve type as constant name.
*
* @param string $constantName Constant name
* @param string $namespace Namespace name
* @return \ApiGen\ReflectionConstant
*/
public function getConstant($constantName, $namespace = '')
{
if (isset($this->parsedConstants[$namespace . '\\' . $constantName])) {
$constant = $this->parsedConstants[$namespace . '\\' . $constantName];
} elseif (isset($this->parsedConstants[ltrim($constantName, '\\')])) {
$constant = $this->parsedConstants[ltrim($constantName, '\\')];
} else {
return null;
}
// Constant is not "documented"
if (!$constant->isDocumented()) {
return null;
}
return $constant;
}
/**
* Tries to resolve type as function name.
*
* @param string $functionName Function name
* @param string $namespace Namespace name
* @return \ApiGen\ReflectionFunction
*/
public function getFunction($functionName, $namespace = '')
{
if (isset($this->parsedFunctions[$namespace . '\\' . $functionName])) {
$function = $this->parsedFunctions[$namespace . '\\' . $functionName];
} elseif (isset($this->parsedFunctions[ltrim($functionName, '\\')])) {
$function = $this->parsedFunctions[ltrim($functionName, '\\')];
} else {
return null;
}
// Function is not "documented"
if (!$function->isDocumented()) {
return null;
}
return $function;
}
/**
* Tries to parse a definition of a class/method/property/constant/function and returns the appropriate instance if successful.
*
* @param string $definition Definition
* @param \ApiGen\ReflectionElement|\ApiGen\ReflectionParameter $context Link context
* @param string $expectedName Expected element name
* @return \ApiGen\ReflectionElement|null
*/
public function resolveElement($definition, $context, &$expectedName = null)
{
// No simple type resolving
static $types = array(
'boolean' => 1, 'integer' => 1, 'float' => 1, 'string' => 1,
'array' => 1, 'object' => 1, 'resource' => 1, 'callback' => 1,
'callable' => 1, 'null' => 1, 'false' => 1, 'true' => 1, 'mixed' => 1
);
if (empty($definition) || isset($types[$definition])) {
return null;
}
$originalContext = $context;
if ($context instanceof ReflectionParameter && null === $context->getDeclaringClassName()) {
// Parameter of function in namespace or global space
$context = $this->getFunction($context->getDeclaringFunctionName());
} elseif ($context instanceof ReflectionMethod || $context instanceof ReflectionParameter
|| ($context instanceof ReflectionConstant && null !== $context->getDeclaringClassName())
|| $context instanceof ReflectionProperty
) {
// Member of a class
$context = $this->getClass($context->getDeclaringClassName());
}
if (null === $context) {
return null;
}
// self, $this references
if ('self' === $definition || '$this' === $definition) {
return $context instanceof ReflectionClass ? $context : null;
}
$definitionBase = substr($definition, 0, strcspn($definition, '\\:'));
$namespaceAliases = $context->getNamespaceAliases();
if (!empty($definitionBase) && isset($namespaceAliases[$definitionBase]) && $definition !== ($className = \TokenReflection\Resolver::resolveClassFQN($definition, $namespaceAliases, $context->getNamespaceName()))) {
// Aliased class
$expectedName = $className;
if (false === strpos($className, ':')) {
return $this->getClass($className, $context->getNamespaceName());
} else {
$definition = $className;
}
} elseif ($class = $this->getClass($definition, $context->getNamespaceName())) {
// Class
return $class;
} elseif ($constant = $this->getConstant($definition, $context->getNamespaceName())) {
// Constant
return $constant;
} elseif (($function = $this->getFunction($definition, $context->getNamespaceName()))
|| ('()' === substr($definition, -2) && ($function = $this->getFunction(substr($definition, 0, -2), $context->getNamespaceName())))
) {
// Function
return $function;
}
if (($pos = strpos($definition, '::')) || ($pos = strpos($definition, '->'))) {
// Class::something or Class->something
if (0 === strpos($definition, 'parent::') && ($parentClassName = $context->getParentClassName())) {
$context = $this->getClass($parentClassName);
} elseif (0 !== strpos($definition, 'self::')) {
$class = $this->getClass(substr($definition, 0, $pos), $context->getNamespaceName());
if (null === $class) {
$class = $this->getClass(\TokenReflection\Resolver::resolveClassFQN(substr($definition, 0, $pos), $context->getNamespaceAliases(), $context->getNamespaceName()));
}
$context = $class;
}
$definition = substr($definition, $pos + 2);
} elseif ($originalContext instanceof ReflectionParameter) {
return null;
}
// No usable context
if (null === $context || $context instanceof ReflectionConstant || $context instanceof ReflectionFunction) {
return null;
}
if ($context->hasProperty($definition)) {
// Class property
return $context->getProperty($definition);
} elseif ('$' === $definition{0} && $context->hasProperty(substr($definition, 1))) {
// Class $property
return $context->getProperty(substr($definition, 1));
} elseif ($context->hasMethod($definition)) {
// Class method
return $context->getMethod($definition);
} elseif ('()' === substr($definition, -2) && $context->hasMethod(substr($definition, 0, -2))) {
// Class method()
return $context->getMethod(substr($definition, 0, -2));
} elseif ($context->hasConstant($definition)) {
// Class constant
return $context->getConstant($definition);
}
return null;
}
/**
* Prints message if printing is enabled.
*
* @param string $message Output message
*/
public function output($message)
{
if (!$this->config->quiet) {
echo $this->colorize($message);
}
}
/**
* Colorizes message or removes placeholders if OS doesn't support colors.
*
* @param string $message
* @return string
*/
public function colorize($message)
{
static $placeholders = array(
'@header@' => "\x1b[1;34m",
'@count@' => "\x1b[1;34m",
'@option@' => "\x1b[0;36m",
'@value@' => "\x1b[0;32m",
'@error@' => "\x1b[0;31m",
'@c' => "\x1b[0m"
);
if (!$this->config->colors) {
$placeholders = array_fill_keys(array_keys($placeholders), '');
}
return strtr($message, $placeholders);
}
/**
* Returns header.
*
* @return string
*/
public function getHeader()
{
$name = sprintf('%s %s', self::NAME, self::VERSION);
return sprintf("@header@%s@c\n%s\n", $name, str_repeat('-', strlen($name)));
}
/**
* Removes phar:// from the path.
*
* @param string $path Path
* @return string
*/
public function unPharPath($path)
{
if (0 === strpos($path, 'phar://')) {
$path = substr($path, 7);
}
return $path;
}
/**
* Adds phar:// to the path.
*
* @param string $path Path
* @return string
*/
private function pharPath($path)
{
return 'phar://' . $path;
}
/**
* Checks if given path is a phar.
*
* @param string $path
* @return boolean
*/
private function isPhar($path)
{
return (bool) preg_match('~\\.phar(?:\\.zip|\\.tar|(?:(?:\\.tar)?(?:\\.gz|\\.bz2))|$)~i', $path);
}
/**
* Normalizes directory separators in given path.
*
* @param string $path Path
* @return string
*/
private function normalizePath($path)
{
$path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
$path = str_replace('phar:\\\\', 'phar://', $path);
return $path;
}
/**
* Prepares the progressbar.
*
* @param integer $maximum Maximum progressbar value
*/
private function prepareProgressBar($maximum = 1)
{
if (!$this->config->progressbar) {
return;
}
$this->progressbar['current'] = 0;
$this->progressbar['maximum'] = $maximum;
}
/**
* Increments the progressbar by one.
*
* @param integer $increment Progressbar increment
*/
private function incrementProgressBar($increment = 1)
{
if (!$this->config->progressbar) {
return;
}
echo str_repeat(chr(0x08), $this->progressbar['width']);
$this->progressbar['current'] += $increment;
$percent = $this->progressbar['current'] / $this->progressbar['maximum'];
$progress = str_pad(str_pad('>', round($percent * $this->progressbar['bar']), '=', STR_PAD_LEFT), $this->progressbar['bar'], ' ', STR_PAD_RIGHT);
echo sprintf($this->progressbar['skeleton'], $progress, $percent * 100, round(memory_get_usage(true) / 1024 / 1024));
if ($this->progressbar['current'] === $this->progressbar['maximum']) {
echo "\n";
}
}
/**
* Checks memory usage.
*
* @return \ApiGen\Generator
* @throws \RuntimeException If there is unsufficient reserve of memory.
*/
public function checkMemory()
{
static $limit = null;
if (null === $limit) {
$value = ini_get('memory_limit');
$unit = substr($value, -1);
if ('-1' === $value) {
$limit = 0;
} elseif ('G' === $unit) {
$limit = (int) $value * 1024 * 1024 * 1024;
} elseif ('M' === $unit) {
$limit = (int) $value * 1024 * 1024;
} else {
$limit = (int) $value;
}
}
if ($limit && memory_get_usage(true) / $limit >= 0.9) {
throw new RuntimeException(sprintf('Used %d%% of the current memory limit, please increase the limit to generate the whole documentation.', round(memory_get_usage(true) / $limit * 100)));
}
return $this;
}
/**
* Detects character set for the given text.
*
* @param string $text Text
* @return string
*/
private function detectCharset($text)
{
// One character set
if (1 === count($this->config->charset) && 'AUTO' !== $this->config->charset[0]) {
return $this->config->charset[0];
}
static $charsets = array();
if (empty($charsets)) {
if (1 === count($this->config->charset) && 'AUTO' === $this->config->charset[0]) {
// Autodetection
$charsets = array(
'Windows-1251', 'Windows-1252', 'ISO-8859-2', 'ISO-8859-1', 'ISO-8859-3', 'ISO-8859-4', 'ISO-8859-5', 'ISO-8859-6',
'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9', 'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15'
);
} else {
// More character sets
$charsets = $this->config->charset;
if (false !== ($key = array_search('WINDOWS-1250', $charsets))) {
// WINDOWS-1250 is not supported
$charsets[$key] = 'ISO-8859-2';
}
}
// Only supported character sets
$charsets = array_intersect($charsets, mb_list_encodings());
// UTF-8 have to be first
array_unshift($charsets, 'UTF-8');
}
$charset = mb_detect_encoding($text, $charsets);
// The previous function can not handle WINDOWS-1250 and returns ISO-8859-2 instead
if ('ISO-8859-2' === $charset && preg_match('~[\x7F-\x9F\xBC]~', $text)) {
$charset = 'WINDOWS-1250';
}
return $charset;
}
/**
* Converts text from given character set to UTF-8.
*
* @param string $text Text
* @param string $charset Character set
* @return string
*/
private function toUtf($text, $charset)
{
if ('UTF-8' === $charset) {
return $text;
}
return @iconv($charset, 'UTF-8//TRANSLIT//IGNORE', $text);
}
/**
* Checks if sitemap.xml is enabled.
*
* @return boolean
*/
private function isSitemapEnabled()
{
return !empty($this->config->baseUrl) && $this->templateExists('sitemap', 'optional');
}
/**
* Checks if opensearch.xml is enabled.
*
* @return boolean
*/
private function isOpensearchEnabled()
{
return !empty($this->config->googleCseId) && !empty($this->config->baseUrl) && $this->templateExists('opensearch', 'optional');
}
/**
* Checks if robots.txt is enabled.
*
* @return boolean
*/
private function isRobotsEnabled()
{
return !empty($this->config->baseUrl) && $this->templateExists('robots', 'optional');
}
/**
* Sorts methods by FQN.
*
* @param \ApiGen\ReflectionMethod $one
* @param \ApiGen\ReflectionMethod $two
* @return integer
*/
private function sortMethods(ReflectionMethod $one, ReflectionMethod $two)
{
return strcasecmp($one->getDeclaringClassName() . '::' . $one->getName(), $two->getDeclaringClassName() . '::' . $two->getName());
}
/**
* Sorts constants by FQN.
*
* @param \ApiGen\ReflectionConstant $one
* @param \ApiGen\ReflectionConstant $two
* @return integer
*/
private function sortConstants(ReflectionConstant $one, ReflectionConstant $two)
{
return strcasecmp(($one->getDeclaringClassName() ?: $one->getNamespaceName()) . '\\' . $one->getName(), ($two->getDeclaringClassName() ?: $two->getNamespaceName()) . '\\' . $two->getName());
}
/**
* Sorts functions by FQN.
*
* @param \ApiGen\ReflectionFunction $one
* @param \ApiGen\ReflectionFunction $two
* @return integer
*/
private function sortFunctions(ReflectionFunction $one, ReflectionFunction $two)
{
return strcasecmp($one->getNamespaceName() . '\\' . $one->getName(), $two->getNamespaceName() . '\\' . $two->getName());
}
/**
* Sorts functions by FQN.
*
* @param \ApiGen\ReflectionProperty $one
* @param \ApiGen\ReflectionProperty $two
* @return integer
*/
private function sortProperties(ReflectionProperty $one, ReflectionProperty $two)
{
return strcasecmp($one->getDeclaringClassName() . '::' . $one->getName(), $two->getDeclaringClassName() . '::' . $two->getName());
}
/**
* Returns list of element types.
*
* @return array
*/
private function getElementTypes()
{
static $types = array('classes', 'interfaces', 'traits', 'exceptions', 'constants', 'functions');
return $types;
}
/**
* Returns main filter.
*
* @return \Closure
*/
private function getMainFilter()
{
return function($element) {
return $element->isMain();
};
}
/**
* Returns ZIP archive path.
*
* @return string
*/
private function getArchivePath()
{
$name = trim(sprintf('%s API documentation', $this->config->title));
return $this->config->destination . DIRECTORY_SEPARATOR . Nette\Utils\Strings::webalize($name) . '.zip';
}
/**
* Returns filename relative path to the source directory.
*
* @param string $fileName
* @return string
* @throws \InvalidArgumentException If relative path could not be determined.
*/
public function getRelativePath($fileName)
{
if (isset($this->symlinks[$fileName])) {
$fileName = $this->symlinks[$fileName];
}
foreach ($this->config->source as $source) {
if ($this->isPhar($source)) {
$source = $this->pharPath($source);
}
if (0 === strpos($fileName, $source)) {
return is_dir($source) ? str_replace('\\', '/', substr($fileName, strlen($source) + 1)) : basename($fileName);
}
}
throw new InvalidArgumentException(sprintf('Could not determine "%s" relative path', $fileName));
}
/**
* Returns template directory.
*
* @return string
*/
private function getTemplateDir()
{
return dirname($this->config->templateConfig);
}
/**
* Returns template path.
*
* @param string $name Template name
* @param string $type Template type
* @return string
*/
private function getTemplatePath($name, $type = 'main')
{
return $this->getTemplateDir() . DIRECTORY_SEPARATOR . $this->config->template['templates'][$type][$name]['template'];
}
/**
* Returns template filename.
*
* @param string $name Template name
* @param string $type Template type
* @return string
*/
private function getTemplateFileName($name, $type = 'main')
{
return $this->config->destination . DIRECTORY_SEPARATOR . $this->config->template['templates'][$type][$name]['filename'];
}
/**
* Checks if template exists.
*
* @param string $name Template name
* @param string $type Template type
* @return string
*/
private function templateExists($name, $type = 'main')
{
return isset($this->config->template['templates'][$type][$name]);
}
/**
* Checks if template exists and creates dir.
*
* @param string $name
* @throws \RuntimeException If template is not set.
*/
private function prepareTemplate($name)
{
if (!$this->templateExists($name)) {
throw new RuntimeException(sprintf('Template for "%s" is not set', $name));
}
$this->forceDir($this->getTemplateFileName($name));
return $this;
}
/**
* Returns list of all generated files.
*
* @return array
*/
private function getGeneratedFiles()
{
$files = array();
// Resources
foreach ($this->config->template['resources'] as $item) {
$path = $this->getTemplateDir() . DIRECTORY_SEPARATOR . $item;
if (is_dir($path)) {
$iterator = Nette\Utils\Finder::findFiles('*')->from($path)->getIterator();
foreach ($iterator as $innerItem) {
$files[] = $this->config->destination . DIRECTORY_SEPARATOR . $item . DIRECTORY_SEPARATOR . $iterator->getSubPathName();
}
} else {
$files[] = $this->config->destination . DIRECTORY_SEPARATOR . $item;
}
}
// Common files
foreach ($this->config->template['templates']['common'] as $item) {
$files[] = $this->config->destination . DIRECTORY_SEPARATOR . $item;
}
// Optional files
foreach ($this->config->template['templates']['optional'] as $optional) {
$files[] = $this->config->destination . DIRECTORY_SEPARATOR . $optional['filename'];
}
// Main files
$masks = array_map(function($config) {
return preg_replace('~%[^%]*?s~', '*', $config['filename']);
}, $this->config->template['templates']['main']);
$filter = function($item) use ($masks) {
foreach ($masks as $mask) {
if (fnmatch($mask, $item->getFilename())) {
return true;
}
}
return false;
};
foreach (Nette\Utils\Finder::findFiles('*')->filter($filter)->from($this->config->destination) as $item) {
$files[] = $item->getPathName();
}
return $files;
}
/**
* Ensures a directory is created.
*
* @param string $path Directory path
* @return string
*/
private function forceDir($path)
{
@mkdir(dirname($path), 0755, true);
return $path;
}
/**
* Deletes a directory.
*
* @param string $path Directory path
* @return boolean
*/
private function deleteDir($path)
{
if (!is_dir($path)) {
return true;
}
foreach (Nette\Utils\Finder::find('*')->from($path)->childFirst() as $item) {
if ($item->isDir()) {
if (!@rmdir($item)) {
return false;
}
} elseif ($item->isFile()) {
if (!@unlink($item)) {
return false;
}
}
}
if (!@rmdir($path)) {
return false;
}
return true;
}
}