woocommerce/apigen/ApiGen/Template.php

810 lines
25 KiB
PHP

<?php
/**
* ApiGen 2.8.0 - API documentation generator for PHP 5.3+
*
* Copyright (c) 2010-2011 David Grudl (http://davidgrudl.com)
* Copyright (c) 2011-2012 Jaroslav Hanslík (https://github.com/kukulich)
* Copyright (c) 2011-2012 Ondřej Nešpor (https://github.com/Andrewsville)
*
* For the full copyright and license information, please view
* the file LICENSE.md that was distributed with this source code.
*/
namespace ApiGen;
use 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);
}
}