public function __construct($htmlPurifierCachePath) { $markdown = new \Michelf\MarkdownExtra(); $htmlPurifierConfig = \HTMLPurifier_Config::createDefault(); $htmlPurifierConfig->set('Cache.SerializerPath', $htmlPurifierCachePath); $htmlPurifier = new \HTMLPurifier($htmlPurifierConfig); $this->converter = function ($description) use($markdown, $htmlPurifier) { $description = Strings::replace($description, '/([^#]*)(#{1,6})(.*)/', function ($matches) { return $matches[1] . (strlen($matches[2]) < 6 ? '#' : '') . $matches[2] . $matches[3]; }); $description = Strings::replace($description, '/```(php|neon|javascript|js|css|html|htmlcb|latte|sql)?\\h*\\v(.+?)\\v```/s', function ($matches) { $fshl = new Highlighter(new Html(), Highlighter::OPTION_TAB_INDENT); switch (strtolower($matches[1])) { case 'php': $fshl->setLexer(new Lexer\Php()); break; case 'neon': $fshl->setLexer(new Lexer\Neon()); break; case 'javascript': case 'js': $fshl->setLexer(new Lexer\Javascript()); break; case 'css': $fshl->setLexer(new Lexer\Css()); break; case 'html': case 'htmlcb': case 'latte': $fshl->setLexer(new Lexer\Html()); break; case 'sql': $fshl->setLexer(new Lexer\Sql()); break; default: $fshl->setLexer(new Lexer\Minimal()); break; } return '<pre' . ($matches[1] ? ' class="src-' . strtolower($matches[1]) . '"' : '') . '><code>' . $fshl->highlight(ltrim($matches[2])) . '</code></pre>'; }); return $htmlPurifier->purify($markdown->transform($description)); }; }
/** * 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; }
/** * @param string $sourceCode * @return string */ public function highlightAndAddLineNumbers($sourceCode) { $this->highlighter->setOptions(Highlighter::OPTION_TAB_INDENT | Highlighter::OPTION_LINE_COUNTER); return $this->highlighter->highlight($sourceCode); }
/** * 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')); }
/** * User handler for code block. * * @param \TexyHandlerInvocation * @param string * @param string * @param string * @param \TexyModifier * @return \TexyHtml */ public function blockHandler($invocation, $blocktype, $content, $lang, $modifier) { if (!in_array($blocktype, array('block/php', 'block/neon', 'block/javascript', 'block/js', 'block/css', 'block/html', 'block/htmlcb', 'block/code', 'block/default'))) { return $invocation->proceed($blocktype, $content, $lang, $modifier); } if (!$lang) { list(, $lang) = explode('/', $blocktype); } $lang = strtolower($lang); if ($lang === 'js') { $lang = 'javascript'; } $texy = $invocation->getTexy(); $lexerClass = 'FSHL\\Lexer\\' . ucfirst($lang); if (class_exists($lexerClass)) { $fshl = new FSHL\Highlighter(new FSHL\Output\Html(), FSHL\Highlighter::OPTION_TAB_INDENT); $lexer = new $lexerClass(); $content = Texy::outdent($content); $content = $fshl->highlight($content, $lexer); } $content = $texy->protect($content, Texy::CONTENT_BLOCK); $elPre = \TexyHtml::el('pre'); if ($modifier) { $modifier->decorate($texy, $elPre); } $elPre->attrs['class'] = $lang ? 'src-' . $lang : NULL; $elCode = $elPre->create('code', $content); return $elPre; }