'[%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('%s', "\n")); fwrite($file, sprintf('%s', "\n")); foreach ($list as $fileName => $reports) { fwrite($file, sprintf('%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%s', "\t\t", $severity, $line, htmlspecialchars($message), "\n")); } fwrite($file, sprintf('%s%s', "\t", "\n")); } fwrite($file, sprintf('%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; } }