generator = $generator; $this->config = $generator->getConfig(); $that = $this; // Output in HTML5 Nette\Utils\Html::$xhtml = false; // FSHL $fshl = new FSHL\Highlighter(new FSHL\Output\Html()); $fshl->setLexer(new FSHL\Lexer\Php()); // Texy $this->texy = new \Texy(); $this->texy->allowedTags = array_flip($this->config->allowedHtml); $this->texy->allowed['list/definition'] = false; $this->texy->allowed['phrase/em-alt'] = false; $this->texy->allowed['longwords'] = false; $this->texy->allowed['typography'] = false; $this->texy->linkModule->shorten = false; // Highlighting ,
		$this->texy->addHandler('beforeParse', function($texy, &$text, $singleLine) {
			$text = preg_replace('~(.+?)~', '#code#\\1#/code#', $text);
		});
		$this->texy->registerLinePattern(
			function($parser, $matches, $name) use ($fshl) {
				return \TexyHtml::el('code', $fshl->highlight($matches[1]));
			},
			'~#code#(.+?)#/code#~',
			'codeInlineSyntax'
		);
		$this->texy->registerBlockPattern(
			function($parser, $matches, $name) use ($fshl) {
				if ('code' === $matches[1]) {
					$lines = array_filter(explode("\n", $matches[2]));
					if (!empty($lines)) {
						$firstLine = array_shift($lines);

						$indent = '';
						$li = 0;

						while (isset($firstLine[$li]) && preg_match('~\s~', $firstLine[$li])) {
							foreach ($lines as $line) {
								if (!isset($line[$li]) || $firstLine[$li] !== $line[$li]) {
									break 2;
								}
							}

							$indent .= $firstLine[$li++];
						}

						if (!empty($indent)) {
							$matches[2] = str_replace(
								"\n" . $indent,
								"\n",
								0 === strpos($matches[2], $indent) ? substr($matches[2], $li) : $matches[2]
							);
						}
					}

					$content = $fshl->highlight($matches[2]);
				} else {
					$content = htmlspecialchars($matches[2]);
				}

				$content = $parser->getTexy()->protect($content, \Texy::CONTENT_BLOCK);
				return \TexyHtml::el('pre', $content);
			},
			'~<(code|pre)>(.+?)~s',
			'codeBlockSyntax'
		);

		// Common operations
		$this->registerHelperLoader('Nette\Templating\Helpers::loader');

		// PHP source highlight
		$this->registerHelper('highlightPHP', function($source, $context) use ($that, $fshl) {
			return $that->resolveLink($that->getTypeName($source), $context) ?: $fshl->highlight((string) $source);
		});
		$this->registerHelper('highlightValue', function($definition, $context) use ($that) {
			return $that->highlightPHP(preg_replace('~^(?:[ ]{4}|\t)~m', '', $definition), $context);
		});

		// Urls
		$this->registerHelper('packageUrl', new Nette\Callback($this, 'getPackageUrl'));
		$this->registerHelper('namespaceUrl', new Nette\Callback($this, 'getNamespaceUrl'));
		$this->registerHelper('groupUrl', new Nette\Callback($this, 'getGroupUrl'));
		$this->registerHelper('classUrl', new Nette\Callback($this, 'getClassUrl'));
		$this->registerHelper('methodUrl', new Nette\Callback($this, 'getMethodUrl'));
		$this->registerHelper('propertyUrl', new Nette\Callback($this, 'getPropertyUrl'));
		$this->registerHelper('constantUrl', new Nette\Callback($this, 'getConstantUrl'));
		$this->registerHelper('functionUrl', new Nette\Callback($this, 'getFunctionUrl'));
		$this->registerHelper('elementUrl', new Nette\Callback($this, 'getElementUrl'));
		$this->registerHelper('sourceUrl', new Nette\Callback($this, 'getSourceUrl'));
		$this->registerHelper('manualUrl', new Nette\Callback($this, 'getManualUrl'));

		// Packages & namespaces
		$this->registerHelper('packageLinks', new Nette\Callback($this, 'getPackageLinks'));
		$this->registerHelper('namespaceLinks', new Nette\Callback($this, 'getNamespaceLinks'));
		$this->registerHelper('subgroupName', function($groupName) {
			if ($pos = strrpos($groupName, '\\')) {
				return substr($groupName, $pos + 1);
			}
			return $groupName;
		});

		// Types
		$this->registerHelper('typeLinks', new Nette\Callback($this, 'getTypeLinks'));

		// Docblock descriptions
		$this->registerHelper('description', function($annotation, $context) use ($that) {
			$description = trim(strpbrk($annotation, "\n\r\t $"));

			if ($context instanceof ReflectionParameter) {
				$description = preg_replace('~^(\\$' . $context->getName() . '(?:,\\.{3})?)(\s+|$)~i', '\\2', $description, 1);
				$context = $context->getDeclaringFunction();
			}
			return $that->doc($description, $context);
		});
		$this->registerHelper('shortDescription', function($element, $block = false) use ($that) {
			return $that->doc($element->getShortDescription(), $element, $block);
		});
		$this->registerHelper('longDescription', function($element) use ($that) {
			$long = $element->getLongDescription();

			// Merge lines
			$long = preg_replace_callback('~(?:<(code|pre)>.+?)|([^<]*)~s', function($matches) {
				return !empty($matches[2])
					? preg_replace('~\n(?:\t|[ ])+~', ' ', $matches[2])
					: $matches[0];
			}, $long);

			return $that->doc($long, $element, true);
		});

		// Individual annotations processing
		$this->registerHelper('annotation', function($value, $name, $context) use ($that, $generator) {
			switch ($name) {
				case 'param':
				case 'return':
				case 'throws':
					$description = $that->description($value, $context);
					return sprintf('%s%s', $that->getTypeLinks($value, $context), $description ? '
' . $description : ''); case 'license': list($url, $description) = $that->split($value); return $that->link($url, $description ?: $url); case 'link': list($url, $description) = $that->split($value); if (Nette\Utils\Validators::isUrl($url)) { return $that->link($url, $description ?: $url); } break; case 'see': $doc = array(); foreach (preg_split('~\\s*,\\s*~', $value) as $link) { if (null !== $generator->resolveElement($link, $context)) { $doc[] = sprintf('%s', $that->getTypeLinks($link, $context)); } else { $doc[] = $that->doc($link, $context); } } return implode(', ', $doc); case 'uses': case 'usedby': list($link, $description) = $that->split($value); $separator = $context instanceof ReflectionClass || !$description ? ' ' : '
'; if (null !== $generator->resolveElement($link, $context)) { return sprintf('%s%s%s', $that->getTypeLinks($link, $context), $separator, $description); } break; default: break; } // Default return $that->doc($value, $context); }); $todo = $this->config->todo; $internal = $this->config->internal; $this->registerHelper('annotationFilter', function(array $annotations, array $filter = array()) use ($todo, $internal) { // Filtered, unsupported or deprecated annotations static $filtered = array( 'package', 'subpackage', 'property', 'property-read', 'property-write', 'method', 'abstract', 'access', 'final', 'filesource', 'global', 'name', 'static', 'staticvar' ); foreach ($filtered as $annotation) { unset($annotations[$annotation]); } // Custom filter foreach ($filter as $annotation) { unset($annotations[$annotation]); } // Show/hide internal if (!$internal) { unset($annotations['internal']); } // Show/hide tasks if (!$todo) { unset($annotations['todo']); } return $annotations; }); $this->registerHelper('annotationSort', function(array $annotations) { uksort($annotations, function($one, $two) { static $order = array( 'deprecated' => 0, 'category' => 1, 'copyright' => 2, 'license' => 3, 'author' => 4, 'version' => 5, 'since' => 6, 'see' => 7, 'uses' => 8, 'usedby' => 9, 'link' => 10, 'internal' => 11, 'example' => 12, 'tutorial' => 13, 'todo' => 14 ); if (isset($order[$one], $order[$two])) { return $order[$one] - $order[$two]; } elseif (isset($order[$one])) { return -1; } elseif (isset($order[$two])) { return 1; } else { return strcasecmp($one, $two); } }); return $annotations; }); $this->registerHelper('annotationBeautify', function($annotation) { static $names = array( 'usedby' => 'Used by' ); if (isset($names[$annotation])) { return $names[$annotation]; } return Nette\Utils\Strings::firstUpper($annotation); }); // Static files versioning $destination = $this->config->destination; $this->registerHelper('staticFile', function($name) use ($destination) { static $versions = array(); $filename = $destination . DIRECTORY_SEPARATOR . $name; if (!isset($versions[$filename]) && is_file($filename)) { $versions[$filename] = sprintf('%u', crc32(file_get_contents($filename))); } if (isset($versions[$filename])) { $name .= '?' . $versions[$filename]; } return $name; }); // Source anchors $this->registerHelper('sourceAnchors', function($source) { // Classes, interfaces, traits and exceptions $source = preg_replace_callback('~((?:class|interface|trait)\\s+)(\\w+)~i', function($matches) { $link = sprintf('%1$s', $matches[2]); return $matches[1] . $link; }, $source); // Methods and functions $source = preg_replace_callback('~(function\\s+)(\\w+)~i', function($matches) { $link = sprintf('%1$s', $matches[2]); return $matches[1] . $link; }, $source); // Constants $source = preg_replace_callback('~(const)(.*?)(;)~is', function($matches) { $links = preg_replace_callback('~(\\s|,)([A-Z_]+)(\\s+=)~', function($matches) { return $matches[1] . sprintf('%1$s', $matches[2]) . $matches[3]; }, $matches[2]); return $matches[1] . $links . $matches[3]; }, $source); // Properties $source = preg_replace_callback('~((?:private|protected|public|var|static)\\s+)(.*?)(;)~is', function($matches) { $links = preg_replace_callback('~()(\\$\\w+)~i', function($matches) { return $matches[1] . sprintf('%1$s', $matches[2]); }, $matches[2]); return $matches[1] . $links . $matches[3]; }, $source); return $source; }); $this->registerHelper('urlize', array($this, 'urlize')); $this->registerHelper('relativePath', array($generator, 'getRelativePath')); $this->registerHelper('resolveElement', array($generator, 'resolveElement')); $this->registerHelper('getClass', array($generator, 'getClass')); } /** * Returns unified type value definition (class name or member data type). * * @param string $name * @param boolean $trimNamespaceSeparator * @return string */ public function getTypeName($name, $trimNamespaceSeparator = true) { static $names = array( 'int' => 'integer', 'bool' => 'boolean', 'double' => 'float', 'void' => '', 'FALSE' => 'false', 'TRUE' => 'true', 'NULL' => 'null', 'callback' => 'callable' ); // Simple type if (isset($names[$name])) { return $names[$name]; } // Class, constant or function return $trimNamespaceSeparator ? ltrim($name, '\\') : $name; } /** * Returns links for types. * * @param string $annotation * @param \ApiGen\ReflectionElement $context * @return string */ public function getTypeLinks($annotation, ReflectionElement $context) { $links = array(); list($types) = $this->split($annotation); if (!empty($types) && '$' === $types{0}) { $types = null; } if (empty($types)) { $types = 'mixed'; } foreach (explode('|', $types) as $type) { $type = $this->getTypeName($type, false); $links[] = $this->resolveLink($type, $context) ?: $this->escapeHtml(ltrim($type, '\\')); } return implode('|', $links); } /** * Returns links for package/namespace and its parent packages. * * @param string $package * @param boolean $last * @return string */ public function getPackageLinks($package, $last = true) { if (empty($this->packages)) { return $package; } $links = array(); $parent = ''; foreach (explode('\\', $package) as $part) { $parent = ltrim($parent . '\\' . $part, '\\'); $links[] = $last || $parent !== $package ? $this->link($this->getPackageUrl($parent), $part) : $this->escapeHtml($part); } return implode('\\', $links); } /** * Returns links for namespace and its parent namespaces. * * @param string $namespace * @param boolean $last * @return string */ public function getNamespaceLinks($namespace, $last = true) { if (empty($this->namespaces)) { return $namespace; } $links = array(); $parent = ''; foreach (explode('\\', $namespace) as $part) { $parent = ltrim($parent . '\\' . $part, '\\'); $links[] = $last || $parent !== $namespace ? $this->link($this->getNamespaceUrl($parent), $part) : $this->escapeHtml($part); } return implode('\\', $links); } /** * Returns a link to a namespace summary file. * * @param string $namespaceName Namespace name * @return string */ public function getNamespaceUrl($namespaceName) { return sprintf($this->config->template['templates']['main']['namespace']['filename'], $this->urlize($namespaceName)); } /** * Returns a link to a package summary file. * * @param string $packageName Package name * @return string */ public function getPackageUrl($packageName) { return sprintf($this->config->template['templates']['main']['package']['filename'], $this->urlize($packageName)); } /** * Returns a link to a group summary file. * * @param string $groupName Group name * @return string */ public function getGroupUrl($groupName) { if (!empty($this->packages)) { return $this->getPackageUrl($groupName); } return $this->getNamespaceUrl($groupName); } /** * Returns a link to class summary file. * * @param string|\ApiGen\ReflectionClass $class Class reflection or name * @return string */ public function getClassUrl($class) { $className = $class instanceof ReflectionClass ? $class->getName() : $class; return sprintf($this->config->template['templates']['main']['class']['filename'], $this->urlize($className)); } /** * Returns a link to method in class summary file. * * @param \ApiGen\ReflectionMethod $method Method reflection * @param \ApiGen\ReflectionClass $class Method declaring class * @return string */ public function getMethodUrl(ReflectionMethod $method, ReflectionClass $class = null) { $className = null !== $class ? $class->getName() : $method->getDeclaringClassName(); return $this->getClassUrl($className) . '#' . ($method->isMagic() ? 'm' : '') . '_' . ($method->getOriginalName() ?: $method->getName()); } /** * Returns a link to property in class summary file. * * @param \ApiGen\ReflectionProperty $property Property reflection * @param \ApiGen\ReflectionClass $class Property declaring class * @return string */ public function getPropertyUrl(ReflectionProperty $property, ReflectionClass $class = null) { $className = null !== $class ? $class->getName() : $property->getDeclaringClassName(); return $this->getClassUrl($className) . '#' . ($property->isMagic() ? 'm' : '') . '$' . $property->getName(); } /** * Returns a link to constant in class summary file or to constant summary file. * * @param \ApiGen\ReflectionConstant $constant Constant reflection * @return string */ public function getConstantUrl(ReflectionConstant $constant) { // Class constant if ($className = $constant->getDeclaringClassName()) { return $this->getClassUrl($className) . '#' . $constant->getName(); } // Constant in namespace or global space return sprintf($this->config->template['templates']['main']['constant']['filename'], $this->urlize($constant->getName())); } /** * Returns a link to function summary file. * * @param \ApiGen\ReflectionFunction $function Function reflection * @return string */ public function getFunctionUrl(ReflectionFunction $function) { return sprintf($this->config->template['templates']['main']['function']['filename'], $this->urlize($function->getName())); } /** * Returns a link to element summary file. * * @param \ApiGen\ReflectionElement $element Element reflection * @return string */ public function getElementUrl(ReflectionElement $element) { if ($element instanceof ReflectionClass) { return $this->getClassUrl($element); } elseif ($element instanceof ReflectionMethod) { return $this->getMethodUrl($element); } elseif ($element instanceof ReflectionProperty) { return $this->getPropertyUrl($element); } elseif ($element instanceof ReflectionConstant) { return $this->getConstantUrl($element); } elseif ($element instanceof ReflectionFunction) { return $this->getFunctionUrl($element); } } /** * Returns a link to a element source code. * * @param \ApiGen\ReflectionElement $element Element reflection * @param boolean $withLine Include file line number into the link * @return string */ public function getSourceUrl(ReflectionElement $element, $withLine = true) { if ($element instanceof ReflectionClass || $element instanceof ReflectionFunction || ($element instanceof ReflectionConstant && null === $element->getDeclaringClassName())) { $elementName = $element->getName(); if ($element instanceof ReflectionClass) { $file = 'class-'; } elseif ($element instanceof ReflectionConstant) { $file = 'constant-'; } elseif ($element instanceof ReflectionFunction) { $file = 'function-'; } } else { $elementName = $element->getDeclaringClassName(); $file = 'class-'; } $file .= $this->urlize($elementName); $lines = null; if ($withLine) { $lines = $element->getStartLine() !== $element->getEndLine() ? sprintf('%s-%s', $element->getStartLine(), $element->getEndLine()) : $element->getStartLine(); } return sprintf($this->config->template['templates']['main']['source']['filename'], $file) . (null !== $lines ? '#' . $lines : ''); } /** * Returns a link to a element documentation at php.net. * * @param \ApiGen\ReflectionBase $element Element reflection * @return string */ public function getManualUrl(ReflectionBase $element) { static $manual = 'http://php.net/manual'; static $reservedClasses = array('stdClass', 'Closure', 'Directory'); // Extension if ($element instanceof ReflectionExtension) { $extensionName = strtolower($element->getName()); if ('core' === $extensionName) { return $manual; } if ('date' === $extensionName) { $extensionName = 'datetime'; } return sprintf('%s/book.%s.php', $manual, $extensionName); } // Class and its members $class = $element instanceof ReflectionClass ? $element : $element->getDeclaringClass(); if (in_array($class->getName(), $reservedClasses)) { return $manual . '/reserved.classes.php'; } $className = strtolower($class->getName()); $classUrl = sprintf('%s/class.%s.php', $manual, $className); $elementName = strtolower(strtr(ltrim($element->getName(), '_'), '_', '-')); if ($element instanceof ReflectionClass) { return $classUrl; } elseif ($element instanceof ReflectionMethod) { return sprintf('%s/%s.%s.php', $manual, $className, $elementName); } elseif ($element instanceof ReflectionProperty) { return sprintf('%s#%s.props.%s', $classUrl, $className, $elementName); } elseif ($element instanceof ReflectionConstant) { return sprintf('%s#%s.constants.%s', $classUrl, $className, $elementName); } } /** * Tries to parse a definition of a class/method/property/constant/function and returns the appropriate link if successful. * * @param string $definition Definition * @param \ApiGen\ReflectionElement $context Link context * @return string|null */ public function resolveLink($definition, ReflectionElement $context) { if (empty($definition)) { return null; } $suffix = ''; if ('[]' === substr($definition, -2)) { $definition = substr($definition, 0, -2); $suffix = '[]'; } $element = $this->generator->resolveElement($definition, $context, $expectedName); if (null === $element) { return $expectedName; } $classes = array(); if ($element->isDeprecated()) { $classes[] = 'deprecated'; } if (!$element->isValid()) { $classes[] = 'invalid'; } if ($element instanceof ReflectionClass) { $link = $this->link($this->getClassUrl($element), $element->getName(), true, $classes); } elseif ($element instanceof ReflectionConstant && null === $element->getDeclaringClassName()) { $text = $element->inNamespace() ? $this->escapeHtml($element->getNamespaceName()) . '\\' . $this->escapeHtml($element->getShortName()) . '' : '' . $this->escapeHtml($element->getName()) . ''; $link = $this->link($this->getConstantUrl($element), $text, false, $classes); } elseif ($element instanceof ReflectionFunction) { $link = $this->link($this->getFunctionUrl($element), $element->getName() . '()', true, $classes); } else { $text = $this->escapeHtml($element->getDeclaringClassName()); if ($element instanceof ReflectionProperty) { $url = $this->propertyUrl($element); $text .= '::$' . $this->escapeHtml($element->getName()) . ''; } elseif ($element instanceof ReflectionMethod) { $url = $this->methodUrl($element); $text .= '::' . $this->escapeHtml($element->getName()) . '()'; } elseif ($element instanceof ReflectionConstant) { $url = $this->constantUrl($element); $text .= '::' . $this->escapeHtml($element->getName()) . ''; } $link = $this->link($url, $text, false, $classes); } return sprintf('%s', $link . $suffix); } /** * Resolves links in documentation. * * @param string $text Processed documentation text * @param \ApiGen\ReflectionElement $context Reflection object * @return string */ private function resolveLinks($text, ReflectionElement $context) { $that = $this; return preg_replace_callback('~{@(?:link|see)\\s+([^}]+)}~', function ($matches) use ($context, $that) { // Texy already added so it has to be stripped list($url, $description) = $that->split(strip_tags($matches[1])); if (Nette\Utils\Validators::isUrl($url)) { return $that->link($url, $description ?: $url); } return $that->resolveLink($matches[1], $context) ?: $matches[1]; }, $text); } /** * Resolves internal annotation. * * @param string $text * @return string */ private function resolveInternal($text) { $internal = $this->config->internal; return preg_replace_callback('~\\{@(\\w+)(?:(?:\\s+((?>(?R)|[^{}]+)*)\\})|\\})~', function($matches) use ($internal) { // Replace only internal if ('internal' !== $matches[1]) { return $matches[0]; } return $internal && isset($matches[2]) ? $matches[2] : ''; }, $text); } /** * Formats text as documentation block or line. * * @param string $text Text * @param \ApiGen\ReflectionElement $context Reflection object * @param boolean $block Parse text as block * @return string */ public function doc($text, ReflectionElement $context, $block = false) { return $this->resolveLinks($this->texy->process($this->resolveInternal($text), !$block), $context); } /** * Parses annotation value. * * @param string $value * @return array */ public function split($value) { return preg_split('~\s+|$~', $value, 2); } /** * Returns link. * * @param string $url * @param string $text * @param boolean $escape If the text should be escaped * @param array $classes List of classes * @return string */ public function link($url, $text, $escape = true, array $classes = array()) { $class = !empty($classes) ? sprintf(' class="%s"', implode(' ', $classes)) : ''; return sprintf('%s', $url, $class, $escape ? $this->escapeHtml($text) : $text); } /** * Converts string to url safe characters. * * @param string $string * @return string */ public function urlize($string) { return preg_replace('~[^\w]~', '.', $string); } }