Location: PHPKode > scripts > ApiGen > apigen/ApiGen/Template.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 Nette, FSHL;

/**
 * Customized ApiGen template class.
 *
 * Adds ApiGen helpers to the Nette\Templating\FileTemplate parent class.
 */
class Template extends Nette\Templating\FileTemplate
{
	/**
	 * Generator.
	 *
	 * @var \ApiGen\Generator
	 */
	private $generator;

	/**
	 * Config.
	 *
	 * @var \ApiGen\Config
	 */
	private $config;

	/**
	 * Texy.
	 *
	 * @var Texy
	 */
	private $texy;

	/**
	 * Creates template.
	 *
	 * @param \ApiGen\Generator $generator
	 */
	public function __construct(Generator $generator)
	{
		$this->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 <code>, <pre>
		$this->texy->addHandler('beforeParse', function($texy, &$text, $singleLine) {
			$text = preg_replace('~<code>(.+?)</code>~', '#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)>(.+?)</\1>~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)>.+?</\1>)|([^<]*)~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('<code>%s</code>%s', $that->getTypeLinks($value, $context), $description ? '<br>' . $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('<code>%s</code>', $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 ? ' ' : '<br>';
					if (null !== $generator->resolveElement($link, $context)) {
						return sprintf('<code>%s</code>%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('~(<span\\s+class="php-keyword1">(?:class|interface|trait)</span>\\s+)(\\w+)~i', function($matches) {
				$link = sprintf('<a id="%1$s" href="#%1$s">%1$s</a>', $matches[2]);
				return $matches[1] . $link;
			}, $source);

			// Methods and functions
			$source = preg_replace_callback('~(<span\\s+class="php-keyword1">function</span>\\s+)(\\w+)~i', function($matches) {
				$link = sprintf('<a id="_%1$s" href="#_%1$s">%1$s</a>', $matches[2]);
				return $matches[1] . $link;
			}, $source);

			// Constants
			$source = preg_replace_callback('~(<span class="php-keyword1">const</span>)(.*?)(;)~is', function($matches) {
				$links = preg_replace_callback('~(\\s|,)([A-Z_]+)(\\s+=)~', function($matches) {
					return $matches[1] . sprintf('<a id="%1$s" href="#%1$s">%1$s</a>', $matches[2]) . $matches[3];
				}, $matches[2]);
				return $matches[1] . $links . $matches[3];
			}, $source);

			// Properties
			$source = preg_replace_callback('~(<span\\s+class="php-keyword1">(?:private|protected|public|var|static)</span>\\s+)(<span\\s+class="php-var">.*?)(;)~is', function($matches) {
				$links = preg_replace_callback('~(<span\\s+class="php-var">)(\\$\\w+)~i', function($matches) {
					return $matches[1] . sprintf('<a id="%1$s" href="#%1$s">%1$s</a>', $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()) . '\\<b>' . $this->escapeHtml($element->getShortName()) . '</b>'
				: '<b>' . $this->escapeHtml($element->getName()) . '</b>';
			$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 .= '::<var>$' . $this->escapeHtml($element->getName()) . '</var>';
			} elseif ($element instanceof ReflectionMethod) {
				$url = $this->methodUrl($element);
				$text .= '::' . $this->escapeHtml($element->getName()) . '()';
			} elseif ($element instanceof ReflectionConstant) {
				$url = $this->constantUrl($element);
				$text .= '::<b>' . $this->escapeHtml($element->getName()) . '</b>';
			}

			$link = $this->link($url, $text, false, $classes);
		}

		return sprintf('<code>%s</code>', $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 <a> 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('<a href="%s"%s>%s</a>', $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);
	}
}
Return current item: ApiGen