From aa8d08664f4b5c1d733e882383add048e3301476 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 15 May 2012 13:21:05 +0200 Subject: [PATCH] added files --- .gitignore | 2 + CHANGELOG | 6 + LICENSE | 19 + README.md | 192 ++++++ Sami/Console/Application.php | 42 ++ Sami/Console/Command/Command.php | 228 ++++++ Sami/Console/Command/ParseCommand.php | 62 ++ Sami/Console/Command/RenderCommand.php | 61 ++ Sami/Console/Command/UpdateCommand.php | 54 ++ Sami/ErrorHandler.php | 47 ++ Sami/Indexer.php | 53 ++ Sami/Message.php | 25 + Sami/Parser/ClassTraverser.php | 66 ++ .../ClassVisitor/InheritdocClassVisitor.php | 65 ++ Sami/Parser/ClassVisitorInterface.php | 19 + Sami/Parser/CodeParser.php | 40 ++ Sami/Parser/DocBlockParser.php | 152 ++++ Sami/Parser/Filter/DefaultFilter.php | 28 + Sami/Parser/Filter/FilterInterface.php | 25 + Sami/Parser/Filter/SymfonyFilter.php | 28 + Sami/Parser/Filter/TrueFilter.php | 34 + Sami/Parser/Node/DocBlockNode.php | 84 +++ Sami/Parser/NodeVisitor.php | 269 ++++++++ Sami/Parser/Parser.php | 99 +++ Sami/Parser/ParserContext.php | 139 ++++ Sami/Parser/Transaction.php | 82 +++ Sami/Project.php | 430 ++++++++++++ Sami/Reflection/ClassReflection.php | 397 +++++++++++ Sami/Reflection/ConstantReflection.php | 53 ++ Sami/Reflection/HintReflection.php | 54 ++ Sami/Reflection/LazyClassReflection.php | 237 +++++++ Sami/Reflection/MethodReflection.php | 171 +++++ Sami/Reflection/ParameterReflection.php | 96 +++ Sami/Reflection/PropertyReflection.php | 105 +++ Sami/Reflection/Reflection.php | 153 ++++ Sami/Renderer/Diff.php | 107 +++ Sami/Renderer/Index.php | 62 ++ Sami/Renderer/Renderer.php | 210 ++++++ Sami/Renderer/Theme.php | 72 ++ Sami/Renderer/ThemeSet.php | 67 ++ Sami/Renderer/TwigExtension.php | 133 ++++ Sami/Resources/themes/default/classes.twig | 33 + Sami/Resources/themes/default/index.twig | 3 + .../Resources/themes/default/layout/base.twig | 21 + .../themes/default/layout/frame.twig | 13 + .../themes/default/layout/layout.twig | 11 + .../Resources/themes/default/layout/page.twig | 30 + Sami/Resources/themes/default/macros.twig | 59 ++ Sami/Resources/themes/default/manifest.yml | 21 + Sami/Resources/themes/default/namespace.twig | 48 ++ Sami/Resources/themes/default/namespaces.twig | 26 + .../Resources/themes/default/pages/class.twig | 213 ++++++ .../themes/default/pages/classes.twig | 28 + .../Resources/themes/default/pages/index.twig | 41 ++ .../themes/default/pages/interfaces.twig | 24 + .../themes/default/pages/namespace.twig | 49 ++ .../themes/default/pages/namespaces.twig | 21 + .../themes/default/pages/opensearch.twig | 14 + Sami/Resources/themes/default/stylesheet.css | 212 ++++++ Sami/Resources/themes/enhanced/README.md | 2 + Sami/Resources/themes/enhanced/css/main.css | 214 ++++++ Sami/Resources/themes/enhanced/css/panel.css | 427 ++++++++++++ Sami/Resources/themes/enhanced/css/reset.css | 53 ++ Sami/Resources/themes/enhanced/i/arrows.png | Bin 0 -> 477 bytes Sami/Resources/themes/enhanced/i/loader.gif | Bin 0 -> 9427 bytes .../themes/enhanced/i/results_bg.png | Bin 0 -> 696 bytes Sami/Resources/themes/enhanced/i/tree_bg.png | Bin 0 -> 207 bytes Sami/Resources/themes/enhanced/index.twig | 3 + .../themes/enhanced/js/jquery-1.3.2.min.js | 19 + .../Resources/themes/enhanced/js/searchdoc.js | 651 ++++++++++++++++++ .../themes/enhanced/layout/page.twig | 4 + Sami/Resources/themes/enhanced/manifest.yml | 18 + Sami/Resources/themes/enhanced/panel.twig | 80 +++ .../themes/enhanced/search_index.twig | 39 ++ Sami/Resources/themes/enhanced/tree.twig | 17 + Sami/Sami.php | 157 +++++ Sami/Store/JsonStore.php | 84 +++ Sami/Store/StoreInterface.php | 28 + Sami/Tests/Parser/DocBlockParserTest.php | 133 ++++ Sami/Tree.php | 80 +++ Sami/Version/GitVersionCollection.php | 112 +++ Sami/Version/SingleVersionCollection.php | 28 + Sami/Version/Version.php | 56 ++ Sami/Version/VersionCollection.php | 98 +++ composer.json | 30 + composer.lock | 95 +++ examples/sf2.php | 29 + examples/swiftmailer.php | 26 + examples/twig.php | 10 + examples/zf2.php | 18 + phpunit.xml.dist | 19 + sami.php | 14 + 92 files changed, 7679 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Sami/Console/Application.php create mode 100644 Sami/Console/Command/Command.php create mode 100644 Sami/Console/Command/ParseCommand.php create mode 100644 Sami/Console/Command/RenderCommand.php create mode 100644 Sami/Console/Command/UpdateCommand.php create mode 100644 Sami/ErrorHandler.php create mode 100644 Sami/Indexer.php create mode 100644 Sami/Message.php create mode 100644 Sami/Parser/ClassTraverser.php create mode 100644 Sami/Parser/ClassVisitor/InheritdocClassVisitor.php create mode 100644 Sami/Parser/ClassVisitorInterface.php create mode 100644 Sami/Parser/CodeParser.php create mode 100644 Sami/Parser/DocBlockParser.php create mode 100644 Sami/Parser/Filter/DefaultFilter.php create mode 100644 Sami/Parser/Filter/FilterInterface.php create mode 100644 Sami/Parser/Filter/SymfonyFilter.php create mode 100644 Sami/Parser/Filter/TrueFilter.php create mode 100644 Sami/Parser/Node/DocBlockNode.php create mode 100644 Sami/Parser/NodeVisitor.php create mode 100644 Sami/Parser/Parser.php create mode 100644 Sami/Parser/ParserContext.php create mode 100644 Sami/Parser/Transaction.php create mode 100644 Sami/Project.php create mode 100644 Sami/Reflection/ClassReflection.php create mode 100644 Sami/Reflection/ConstantReflection.php create mode 100644 Sami/Reflection/HintReflection.php create mode 100644 Sami/Reflection/LazyClassReflection.php create mode 100644 Sami/Reflection/MethodReflection.php create mode 100644 Sami/Reflection/ParameterReflection.php create mode 100644 Sami/Reflection/PropertyReflection.php create mode 100644 Sami/Reflection/Reflection.php create mode 100644 Sami/Renderer/Diff.php create mode 100644 Sami/Renderer/Index.php create mode 100644 Sami/Renderer/Renderer.php create mode 100644 Sami/Renderer/Theme.php create mode 100644 Sami/Renderer/ThemeSet.php create mode 100644 Sami/Renderer/TwigExtension.php create mode 100644 Sami/Resources/themes/default/classes.twig create mode 100644 Sami/Resources/themes/default/index.twig create mode 100644 Sami/Resources/themes/default/layout/base.twig create mode 100644 Sami/Resources/themes/default/layout/frame.twig create mode 100644 Sami/Resources/themes/default/layout/layout.twig create mode 100644 Sami/Resources/themes/default/layout/page.twig create mode 100644 Sami/Resources/themes/default/macros.twig create mode 100644 Sami/Resources/themes/default/manifest.yml create mode 100644 Sami/Resources/themes/default/namespace.twig create mode 100644 Sami/Resources/themes/default/namespaces.twig create mode 100644 Sami/Resources/themes/default/pages/class.twig create mode 100644 Sami/Resources/themes/default/pages/classes.twig create mode 100644 Sami/Resources/themes/default/pages/index.twig create mode 100644 Sami/Resources/themes/default/pages/interfaces.twig create mode 100644 Sami/Resources/themes/default/pages/namespace.twig create mode 100644 Sami/Resources/themes/default/pages/namespaces.twig create mode 100644 Sami/Resources/themes/default/pages/opensearch.twig create mode 100644 Sami/Resources/themes/default/stylesheet.css create mode 100644 Sami/Resources/themes/enhanced/README.md create mode 100644 Sami/Resources/themes/enhanced/css/main.css create mode 100644 Sami/Resources/themes/enhanced/css/panel.css create mode 100644 Sami/Resources/themes/enhanced/css/reset.css create mode 100644 Sami/Resources/themes/enhanced/i/arrows.png create mode 100644 Sami/Resources/themes/enhanced/i/loader.gif create mode 100644 Sami/Resources/themes/enhanced/i/results_bg.png create mode 100644 Sami/Resources/themes/enhanced/i/tree_bg.png create mode 100644 Sami/Resources/themes/enhanced/index.twig create mode 100644 Sami/Resources/themes/enhanced/js/jquery-1.3.2.min.js create mode 100644 Sami/Resources/themes/enhanced/js/searchdoc.js create mode 100644 Sami/Resources/themes/enhanced/layout/page.twig create mode 100644 Sami/Resources/themes/enhanced/manifest.yml create mode 100644 Sami/Resources/themes/enhanced/panel.twig create mode 100644 Sami/Resources/themes/enhanced/search_index.twig create mode 100644 Sami/Resources/themes/enhanced/tree.twig create mode 100644 Sami/Sami.php create mode 100644 Sami/Store/JsonStore.php create mode 100644 Sami/Store/StoreInterface.php create mode 100644 Sami/Tests/Parser/DocBlockParserTest.php create mode 100644 Sami/Tree.php create mode 100644 Sami/Version/GitVersionCollection.php create mode 100644 Sami/Version/SingleVersionCollection.php create mode 100644 Sami/Version/Version.php create mode 100644 Sami/Version/VersionCollection.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 examples/sf2.php create mode 100644 examples/swiftmailer.php create mode 100644 examples/twig.php create mode 100644 examples/zf2.php create mode 100644 phpunit.xml.dist create mode 100755 sami.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d26d6c2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor +composer.phar diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 00000000..f3fb851c --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,6 @@ +CHANGELOG +========= + +* 0.8 (2012-05-15) + + * initial Open-Source version diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..4c03565c --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-2012 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..bca2e0c0 --- /dev/null +++ b/README.md @@ -0,0 +1,192 @@ +Sami: an API documentation generator +==================================== + +Curious about what Sami generates? Have a look at the [Symfony +API](http://api.symfony.com/). + +Installation +------------ + +First, get Sami from [Github](https://github.com/fabpot/Sami) (or integrate it +as a dependency in your project [Composer](packagist.org/fabpot/Sami) file -- +you are using [Composer](http://getcomposer.org/), right?): + + https://github.com/fabpot/Sami + +You can also download an [archive](https://github.com/fabpot/Sami/downloads) +from Github. + +As Sami uses Composer to manage its dependencies, installing it is a matter of +running composer: + + $ composer.phar install + +Check that everything worked as expected by executing the `sami.php` file +without any arguments: + + $ php sami.php + +Configuration +------------- + +Before generating documentation, you must create a configuration file. Here is +the simplest possible one: + + files() + ->name('*.php') + ->exclude('Resources') + ->exclude('Tests') + ->in('/path/to/symfony/src') + ; + + return new Sami($iterator); + +The `Sami` constructor optionally takes an array of options as a second +argument: + + return new Sami($iterator, array( + 'theme' => 'symfony', + 'title' => 'Symfony2 API', + 'build_dir' => __DIR__.'/build', + 'cache_dir' => __DIR__.'/cache', + 'default_opened_level' => 2, + )); + +And here is how you can configure different versions: + + files() + ->name('*.php') + ->exclude('Resources') + ->exclude('Tests') + ->in($dir = '/path/to/symfony/src') + ; + + // generate documentation for all v2.0.* tags, the 2.0 branch, and the master one + $versions = GitVersionCollection::create($dir) + ->addFromTags('v2.0.*') + ->add('2.0', '2.0 branch') + ->add('master', 'master branch') + ; + + return new Sami($iterator, array( + 'theme' => 'symfony', + 'versions' => $versions, + 'title' => 'Symfony2 API', + 'build_dir' => __DIR__.'/../build/sf2/%version%', + 'cache_dir' => __DIR__.'/../cache/sf2/%version%', + 'default_opened_level' => 2, + )); + +To generate documentation for a PHP 5.2 project, simply set the +`simulate_namespaces` option to `true`. + +You can find more configuration examples under the `examples/` directory of +the source code. + +Rendering +--------- + +Now that we have a configuration file, let's generate the API documentation: + + $ php sami.php update /path/to/config.php + +The generated documentation can be found under the configured `build/` +directory (note that the client side search engine does not work on Chrome due +to JavaScript execution restriction -- it works fine in Firefox). + +By default, Sami is configured to run in "incremental" mode. It means that +when running the `update` command, Sami only re-generates the files that needs +to be updated based on what has changed in your code since the last execution. + +Sami also detects problems in your phpdoc and can tell you what you need to +fix if you add the `-v` option: + + $ php sami.php update /path/to/config.php -v + +Creating a Theme +---------------- + +If the default themes do not suit your needs, you can very easily create a new +one, or just override an existing one. + +A theme is just a directory with a `manifest.yml` file that describes the +theme (this is a YAML file): + + name: symfony + parent: enhanced + +The above configuration creates a new `symfony` theme based on the `enhanced` +built-in theme. To override a template, just create a file with the same name +as the original one. For instance, here is how you can extend the default +class template to prefix the class name with "Class " in the class page title: + + {# pages/class.twig #} + + {% extends 'default/pages/class.twig' %} + + {% block title %}Class {{ parent() }}{% endblock %} + +If you are familiar with Twig, you will be able to very easily tweak every +aspect of the templates as everything has been well isolated in named Twig +blocks. + +A theme can also add more templates and static files. Here is the manifest for +the default theme: + + name: default + + static: + 'stylesheet.css': 'stylesheet.css' + + global: + 'index.twig': 'index.html' + 'namespaces.twig': 'namespaces-frame.html' + 'classes.twig': 'classes-frame.html' + 'pages/opensearch.twig': 'opensearch.xml' + 'pages/index.twig': 'doc-index.html' + 'pages/namespaces.twig': 'namespaces.html' + 'pages/interfaces.twig': 'interfaces.html' + 'pages/classes.twig': 'classes.html' + + namespace: + 'namespace.twig': '%s/namespace-frame.html' + 'pages/namespace.twig': '%s.html' + + class: + 'pages/class.twig': '%s.html' + +Files are contained into sections, depending on how Sami needs to treat them: + + * `static`: Files are copied as is (for assets like images, stylesheets, or + JavaScript files); + + * `global`: Templates that do not depend on the current class context; + + * `namespace`: Templates that should be generated for every namespace; + + * `class`: Templates that should be generated for every class. diff --git a/Sami/Console/Application.php b/Sami/Console/Application.php new file mode 100644 index 00000000..159dc342 --- /dev/null +++ b/Sami/Console/Application.php @@ -0,0 +1,42 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Console; + +use Symfony\Component\Console\Application as BaseApplication; +use Sami\Console\Command\UpdateCommand; +use Sami\Console\Command\ParseCommand; +use Sami\Console\Command\RenderCommand; +use Sami\Sami; +use Sami\ErrorHandler; + +class Application extends BaseApplication +{ + /** + * Constructor. + */ + public function __construct() + { + error_reporting(-1); + ErrorHandler::register(); + + parent::__construct('Sami', Sami::VERSION); + + $this->add(new UpdateCommand()); + $this->add(new ParseCommand()); + $this->add(new RenderCommand()); + } + + public function getLongVersion() + { + return parent::getLongVersion().' by Fabien Potencier'; + } +} diff --git a/Sami/Console/Command/Command.php b/Sami/Console/Command/Command.php new file mode 100644 index 00000000..1c5e7f8d --- /dev/null +++ b/Sami/Console/Command/Command.php @@ -0,0 +1,228 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Console\Command; + +use Symfony\Component\Console\Command\Command as BaseCommand; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Filesystem; +use Sami\Sami; +use Sami\Project; +use Sami\Version\Version; +use Sami\Parser\Transaction; +use Sami\Renderer\Diff; +use Sami\Message; + +abstract class Command extends BaseCommand +{ + protected $sami; + protected $version; + protected $started; + protected $diffs; + protected $transactions; + protected $errors; + protected $input; + protected $output; + + /** + * @see Command + */ + protected function configure() + { + $this->getDefinition()->addArgument( + new InputArgument('config', InputArgument::REQUIRED, 'The configuration'), + new InputOption('version', '', InputOption::VALUE_REQUIRED, 'The version to build') + ); + } + + protected function initialize(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + + $config = $input->getArgument('config'); + $filesystem = new Filesystem(); + + if (!$filesystem->isAbsolutePath($config)) { + $config = getcwd().'/'.$config; + } + + if (!file_exists($config)) { + throw new \InvalidArgumentException(sprintf('Configuration file "%s" does not exist.', $config)); + } + + $this->sami = require $config; + + if ($input->getOption('version')) { + $this->sami['version'] = $input->getOption('version'); + } + + if (!$this->sami instanceof Sami) { + throw new \RuntimeException(sprintf('Configuration file "%s" must return a Sami instance.', $config)); + } + } + + public function update(Project $project) + { + $callback = $this->output->isDecorated() ? array($this, 'messageCallback') : null; + + $project->update($callback, $this->input->getOption('force')); + + $this->displayParseSummary(); + $this->displayRenderSummary(); + } + + public function parse(Project $project) + { + $callback = $this->output->isDecorated() ? array($this, 'messageCallback') : null; + + $project->parse($callback, $this->input->getOption('force')); + + $this->displayParseSummary(); + } + + public function render(Project $project) + { + $callback = $this->output->isDecorated() ? array($this, 'messageCallback') : null; + + $diff = $project->render($callback, $this->input->getOption('force')); + + $this->displayRenderSummary(); + } + + public function messageCallback($message, $data) + { + switch ($message) { + case Message::PARSE_CLASS: + list($progress, $class) = $data; + $this->displayParseProgress($progress, $class); + break; + case Message::PARSE_ERROR: + $this->errors = array_merge($this->errors, $data); + break; + case Message::SWITCH_VERSION: + $this->version = $data; + $this->errors = array(); + $this->started = false; + $this->displaySwitch(); + break; + case Message::PARSE_VERSION_FINISHED: + $this->transactions[(string) $this->version] = $data; + $this->displayParseEnd($data); + $this->started = false; + break; + case Message::RENDER_VERSION_FINISHED: + $this->diffs[(string) $this->version] = $data; + $this->displayRenderEnd($data); + $this->started = false; + break; + case Message::RENDER_PROGRESS: + list ($section, $message, $progression) = $data; + $this->displayRenderProgress($section, $message, $progression); + break; + } + } + + public function renderProgressBar($percent, $length) + { + return + str_repeat('#', floor($percent / 100 * $length)) + .sprintf(' %d%%', $percent) + .str_repeat(' ', $length - floor($percent / 100 * $length)) + ; + } + + public function displayParseProgress($progress, $class) + { + if ($this->started) { + $this->output->write("\033[2A"); + } + $this->started = true; + + $this->output->write(sprintf( + " Parsing %s%s\033[K\n %s\033[K\n", + $this->renderProgressBar($progress, 50), count($this->errors) ? ' '.count($this->errors).' error'.(1 == count($this->errors) ? '' : 's').'' : '', $class->getName()) + ); + } + + public function displayRenderProgress($section, $message, $progression) + { + if ($this->started) { + $this->output->write("\033[2A"); + } + $this->started = true; + + $this->output->write(sprintf( + " Rendering %s\033[K\n %s %s\033[K\n", + $this->renderProgressBar($progression, 50), $section, $message + )); + } + + public function displayParseEnd(Transaction $transaction) + { + if (!$this->started) { + return; + } + + $this->output->write(sprintf("\033[2A Parsing done\033[K\n\033[K\n\033[1A", count($this->errors) ? ' '.count($this->errors).' errors' : '')); + + if ($this->input->getOption('verbose') && count($this->errors)) { + foreach ($this->errors as $error) { + $this->output->write(sprintf("ERROR: ")); + $this->output->writeln($error, OutputInterface::OUTPUT_RAW); + } + $this->output->writeln(''); + } + } + + public function displayRenderEnd(Diff $diff) + { + if (!$this->started) { + return; + } + + $this->output->write("\033[2A Rendering done\033[K\n\033[K\n\033[1A"); + } + + public function displayParseSummary() + { + $this->output->writeln(''); + $this->output->writeln(' Version Updated C Removed C '); + + foreach ($this->transactions as $version => $transaction) { + $this->output->writeln(sprintf('%9s %11d %11d', $version, count($transaction->getModifiedClasses()), count($transaction->getRemovedClasses()))); + } + $this->output->writeln(''); + } + + public function displayRenderSummary() + { + $this->output->writeln(' Version Updated C Updated N Removed C Removed N '); + + foreach ($this->diffs as $version => $diff) { + $this->output->writeln(sprintf('%9s %11d %11d %11d %11d', $version, + count($diff->getModifiedClasses()), + count($diff->getModifiedNamespaces()), + count($diff->getRemovedClasses()), + count($diff->getRemovedNamespaces()) + )); + } + $this->output->writeln(''); + } + + public function displaySwitch() + { + $this->output->writeln(sprintf("\nVersion %s", $this->version)); + } +} diff --git a/Sami/Console/Command/ParseCommand.php b/Sami/Console/Command/ParseCommand.php new file mode 100644 index 00000000..31b69776 --- /dev/null +++ b/Sami/Console/Command/ParseCommand.php @@ -0,0 +1,62 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Console\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ParseCommand extends Command +{ + /** + * @see Command + */ + protected function configure() + { + parent::configure(); + + $this->getDefinition()->addOption(new InputOption('force', '', InputOption::VALUE_NONE, 'Forces to rebuild from scratch', null)); + + $this + ->setName('parse') + ->setDescription('Parses a project') + ->setHelp(<<parse command parses a project and generates a database +with API information: + + php sami.php config/symfony.php parse + +The --force option forces a rebuild (it disables the +incremental parsing algorithm): + + php sami.php parse config/symfony.php --force + +The --version option overrides the version specified +in the configuration: + + php sami.php parse config/symfony.php --version=master +EOF + ); + } + + /** + * @see Command + * + * @throws \InvalidArgumentException When the target directory does not exist + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln(' Parsing project '); + + $this->parse($this->sami['project']); + } +} diff --git a/Sami/Console/Command/RenderCommand.php b/Sami/Console/Command/RenderCommand.php new file mode 100644 index 00000000..d9244db0 --- /dev/null +++ b/Sami/Console/Command/RenderCommand.php @@ -0,0 +1,61 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Console\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\InputOption; + +class RenderCommand extends Command +{ + /** + * @see Command + */ + protected function configure() + { + parent::configure(); + + $this->getDefinition()->addOption(new InputOption('force', '', InputOption::VALUE_NONE, 'Forces to rebuild from scratch', null)); + + $this + ->setName('render') + ->setDescription('Renders a project') + ->setHelp(<<render command renders a project as a static set of HTML files: + + php sami.php config/symfony.php render + +The --force option forces a rebuild (it disables the +incremental rendering algorithm): + + php sami.php render config/symfony.php --force + +The --version option overrides the version specified +in the configuration: + + php sami.php render config/symfony.php --version=master +EOF + ); + } + + /** + * @see Command + * + * @throws \InvalidArgumentException When the target directory does not exist + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln(' Rendering project '); + + $this->render($this->sami['project']); + } +} diff --git a/Sami/Console/Command/UpdateCommand.php b/Sami/Console/Command/UpdateCommand.php new file mode 100644 index 00000000..f8b7d4a7 --- /dev/null +++ b/Sami/Console/Command/UpdateCommand.php @@ -0,0 +1,54 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Console\Command; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class UpdateCommand extends Command +{ + /** + * @see Command + */ + protected function configure() + { + parent::configure(); + + $this->getDefinition()->addOption(new InputOption('force', '', InputOption::VALUE_NONE, 'Forces to rebuild from scratch', null)); + + $this + ->setName('update') + ->setDescription('Parses then renders a project') + ->setHelp(<<update command parses and renders a project: + + php sami.php config/symfony.php parses + +This command is the same as running the parse command followed +by the render command. +EOF + ); + } + + /** + * @see Command + * + * @throws \InvalidArgumentException When the target directory does not exist + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln(' Updating project '); + + $this->update($this->sami['project']); + } +} diff --git a/Sami/ErrorHandler.php b/Sami/ErrorHandler.php new file mode 100644 index 00000000..c12ed6a5 --- /dev/null +++ b/Sami/ErrorHandler.php @@ -0,0 +1,47 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami; + +class ErrorHandler +{ + private $levels = array( + E_WARNING => 'Warning', + E_NOTICE => 'Notice', + E_USER_ERROR => 'User Error', + E_USER_WARNING => 'User Warning', + E_USER_NOTICE => 'User Notice', + E_STRICT => 'Runtime Notice', + E_RECOVERABLE_ERROR => 'Catchable Fatal Error', + ); + + /** + * Registers the error handler. + * + * @return The registered error handler + */ + static public function register() + { + set_error_handler(array(new static(), 'handle')); + } + + /** + * @throws \ErrorException When error_reporting returns error + */ + public function handle($level, $message, $file, $line, $context) + { + if (error_reporting() & $level) { + throw new \ErrorException(sprintf('%s: %s in %s line %d', isset($this->levels[$level]) ? $this->levels[$level] : $level, $message, $file, $line)); + } + + return false; + } +} diff --git a/Sami/Indexer.php b/Sami/Indexer.php new file mode 100644 index 00000000..d20039e3 --- /dev/null +++ b/Sami/Indexer.php @@ -0,0 +1,53 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami; + +use Sami\Project; + +class Indexer +{ + const TYPE_CLASS = 1; + const TYPE_METHOD = 2; + const TYPE_NAMESPACE = 3; + + public function getIndex(Project $project) + { + $index = array( + 'searchIndex' => array(), + 'info' => array(), + ); + + foreach ($project->getNamespaces() as $namespace) { + $index['searchIndex'][] = $this->getSearchString($namespace); + $index['info'][] = array(self::TYPE_NAMESPACE, $namespace); + } + + foreach ($project->getProjectClasses() as $class) { + $index['searchIndex'][] = $this->getSearchString((string) $class); + $index['info'][] = array(self::TYPE_CLASS, $class); + } + + foreach ($project->getProjectClasses() as $class) { + foreach ($class->getMethods() as $method) { + $index['searchIndex'][] = $this->getSearchString((string) $method); + $index['info'][] = array(self::TYPE_METHOD, $method); + } + } + + return $index; + } + + protected function getSearchString($string) + { + return strtolower(preg_replace("/\s+/", '', $string)); + } +} diff --git a/Sami/Message.php b/Sami/Message.php new file mode 100644 index 00000000..154eef9e --- /dev/null +++ b/Sami/Message.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami; + +/** + * @author Fabien Potencier + */ +class Message +{ + const PARSE_ERROR = 1; + const PARSE_CLASS = 2; + const PARSE_VERSION_FINISHED = 3; + const RENDER_PROGRESS = 4; + const RENDER_VERSION_FINISHED = 5; + const SWITCH_VERSION = 6; +} diff --git a/Sami/Parser/ClassTraverser.php b/Sami/Parser/ClassTraverser.php new file mode 100644 index 00000000..5faa34f9 --- /dev/null +++ b/Sami/Parser/ClassTraverser.php @@ -0,0 +1,66 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser; + +use Sami\Project; + +class ClassTraverser +{ + protected $visitors; + + public function __construct(array $visitors = array()) + { + $this->visitors = array(); + foreach ($visitors as $visitor) { + $this->addVisitor($visitor); + } + } + + public function addVisitor(ClassVisitorInterface $visitor) + { + $this->visitors[] = $visitor; + } + + public function traverse(Project $project) + { + // parent classes/interfaces are visited before their "children" + $classes = $project->getProjectClasses(); + $modified = array(); + while ($class = array_shift($classes)) { + // re-push the class at the end if parent class/interfaces have not been visited yet + if (($parent = $class->getParent()) && isset($classes[$parent->getName()])) { + $classes[$class->getName()] = $class; + + continue; + } + + foreach ($class->getInterfaces() as $interface) { + if (isset($classes[$interface->getName()])) { + $classes[$class->getName()] = $class; + + continue 2; + } + } + + $isModified = false; + foreach ($this->visitors as $visitor) { + $isModified = $isModified || $visitor->visit($class); + } + + if ($isModified) { + $modified[] = $class; + } + } + + return $modified; + } +} diff --git a/Sami/Parser/ClassVisitor/InheritdocClassVisitor.php b/Sami/Parser/ClassVisitor/InheritdocClassVisitor.php new file mode 100644 index 00000000..795c562f --- /dev/null +++ b/Sami/Parser/ClassVisitor/InheritdocClassVisitor.php @@ -0,0 +1,65 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser\ClassVisitor; + +use Sami\Reflection\ClassReflection; +use Sami\Parser\ClassVisitorInterface; + +class InheritdocClassVisitor implements ClassVisitorInterface +{ + public function visit(ClassReflection $class) + { + $modified = false; + foreach ($class->getMethods() as $name => $method) { + if (!$parentMethod = $class->getParentMethod($name)) { + continue; + } + + foreach ($method->getParameters() as $name => $parameter) { + if (!$parentParameter = $parentMethod->getParameter($name)) { + continue; + } + + if (!$parameter->getShortDesc()) { + $parameter->setShortDesc($parentParameter->getShortDesc()); + $modified = true; + } + + if (!$parameter->getHint()) { + // FIXME: should test for a raw hint from tags, not the one from PHP itself + $parameter->setHint($parentParameter->getRawHint()); + $modified = true; + } + } + + if (!$method->getHint()) { + $method->setHint($parentMethod->getRawHint()); + $modified = true; + } + + if (!$method->getHintDesc()) { + $method->setHintDesc($parentMethod->getHintDesc()); + $modified = true; + } + + if ('{@inheritdoc}' == strtolower(trim($method->getShortDesc())) || !$method->getDocComment()) { + $method->setShortDesc($parentMethod->getShortDesc()); + $method->setLongDesc($parentMethod->getLongDesc()); + $method->setExceptions($parentMethod->getRawExceptions()); + + $modified = true; + } + } + + return $modified; + } +} diff --git a/Sami/Parser/ClassVisitorInterface.php b/Sami/Parser/ClassVisitorInterface.php new file mode 100644 index 00000000..aeb10b84 --- /dev/null +++ b/Sami/Parser/ClassVisitorInterface.php @@ -0,0 +1,19 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser; + +use Sami\Reflection\ClassReflection; + +interface ClassVisitorInterface +{ + function visit(ClassReflection $class); +} diff --git a/Sami/Parser/CodeParser.php b/Sami/Parser/CodeParser.php new file mode 100644 index 00000000..693f7786 --- /dev/null +++ b/Sami/Parser/CodeParser.php @@ -0,0 +1,40 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser; + +class CodeParser +{ + protected $parser; + protected $traverser; + protected $context; + + public function __construct(ParserContext $context, \PHPParser_Parser $parser, \PHPParser_NodeTraverser $traverser) + { + $this->context = $context; + $this->parser = $parser; + $this->traverser = $traverser; + } + + public function getContext() + { + return $this->context; + } + + public function parse($code) + { + try { + $this->traverser->traverse($this->parser->parse($code)); + } catch (\PHPParser_Error $e) { + $this->context->addError($this->context->getFile(), 0, $e->getMessage()); + } + } +} diff --git a/Sami/Parser/DocBlockParser.php b/Sami/Parser/DocBlockParser.php new file mode 100644 index 00000000..5a2ea39c --- /dev/null +++ b/Sami/Parser/DocBlockParser.php @@ -0,0 +1,152 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser; + +use Sami\Parser\Node\DocBlockNode; + +class DocBlockParser +{ + const TAG_REGEX = '@([^ ]+)(?:\s+(.*?))?(?=(\n[ \t]*@|\s*$))'; + + protected $position; + protected $comment; + protected $lineno; + protected $cursor; + + public function parse($comment) + { + // remove comment characters and normalize + $comment = preg_replace(array('#^/\*\*\s*#', '#\s*\*/$#', '#^\s*\*#m'), '', trim($comment)); + $comment = "\n".preg_replace('/(\r\n|\r)/', "\n", $comment); + + $this->position = 'desc'; + $this->comment = $comment; + $this->lineno = 1; + $this->cursor = 0; + + $doc = new DocBlockNode(); + while ($this->cursor < strlen($this->comment)) { + switch ($this->position) { + case 'desc': + list($short, $long) = $this->parseDesc(); + $doc->setShortDesc($short); + $doc->setLongDesc($long); + break; + + case 'tag': + try { + list($type, $values) = $this->parseTag(); + $doc->addTag($type, $values); + } catch (\LogicException $e) { + $doc->addError($e->getMessage()); + } + break; + } + + if (preg_match('/\s*$/As', $this->comment, $match, null, $this->cursor)) { + $this->cursor = strlen($this->comment); + } + } + + return $doc; + } + + protected function parseDesc() + { + if (preg_match('/(.*?)(\n[ \t]*'.self::TAG_REGEX.'|$)/As', $this->comment, $match, null, $this->cursor)) { + $this->move($match[1]); + + $short = trim($match[1]); + $long = ''; + + // short desc ends at the first dot or when \n\n occurs + if (preg_match('/(.*?)(\.\s|\n\n|$)/s', $short, $match)) { + $long = trim(substr($short, strlen($match[0]))); + $short = trim($match[0]); + } + } + + $this->position = 'tag'; + + return array(str_replace("\n", '', $short), $long); + } + + protected function parseTag() + { + if (preg_match('/\n\s*'.self::TAG_REGEX.'/As', $this->comment, $match, null, $this->cursor)) { + $this->move($match[0]); + + switch ($type = $match[1]) { + case 'param': + if (!preg_match('/^([^\s]*)\s*(?:\$([^\s]+))?\s*(.*)$/s', $match[2], $m)) { + throw new \LogicException(sprintf('Unable to parse "@%s" tag " %s"', $type, $match[2])); + + return; + } + + return array($type, array($this->parseHint(trim($m[1])), trim($m[2]), $this->normalizeString($m[3]))); + + case 'return': + case 'var': + if (!preg_match('/^([^\s]+)\s*(.*)$/s', $match[2], $m)) { + throw new \LogicException(sprintf('Unable to parse "@%s" tag "%s"', $type, $match[2])); + + return; + } + + return array($type, array($this->parseHint(trim($m[1])), $this->normalizeString($m[2]))); + + case 'throws': + if (!preg_match('/^([^\s]+)\s*(.*)$/s', $match[2], $m)) { + throw new \LogicException(sprintf('Unable to parse "@%s" tag "%s"', $type, $match[2])); + + return; + } + + return array($type, array(trim($m[1]), $this->normalizeString($m[2]))); + + default: + return array($type, $this->normalizeString($match[2])); + } + } else { + // skip + $this->cursor = strlen($this->comment); + + throw new \LogicException(sprintf('Unable to parse block comment near "... %s ...".', substr($this->comment, max(0, $this->cursor - 15), 15))); + } + } + + protected function parseHint($hint) + { + $hints = array(); + foreach (explode('|', $hint) as $hint) { + if ('[]' == substr($hint, -2)) { + $hints[] = array(substr($hint, 0, -2), true); + } else { + $hints[] = array($hint, false); + } + } + + return $hints; + } + + protected function normalizeString($str) + { + return preg_replace('/\s*\n\s*/', ' ', trim($str)); + } + + protected function move($text) + { + $this->lineno += substr_count($text, "\n"); + $this->cursor += strlen($text); + } +} diff --git a/Sami/Parser/Filter/DefaultFilter.php b/Sami/Parser/Filter/DefaultFilter.php new file mode 100644 index 00000000..a4c4964b --- /dev/null +++ b/Sami/Parser/Filter/DefaultFilter.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser\Filter; + +use Sami\Reflection\MethodReflection; +use Sami\Reflection\PropertyReflection; + +class DefaultFilter extends TrueFilter +{ + public function acceptMethod(MethodReflection $method) + { + return $method->isPublic(); + } + + public function acceptProperty(PropertyReflection $property) + { + return $property->isPublic(); + } +} diff --git a/Sami/Parser/Filter/FilterInterface.php b/Sami/Parser/Filter/FilterInterface.php new file mode 100644 index 00000000..ee685c9e --- /dev/null +++ b/Sami/Parser/Filter/FilterInterface.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser\Filter; + +use Sami\Reflection\ClassReflection; +use Sami\Reflection\MethodReflection; +use Sami\Reflection\PropertyReflection; + +interface FilterInterface +{ + function acceptClass(ClassReflection $class); + + function acceptMethod(MethodReflection $method); + + function acceptProperty(PropertyReflection $property); +} diff --git a/Sami/Parser/Filter/SymfonyFilter.php b/Sami/Parser/Filter/SymfonyFilter.php new file mode 100644 index 00000000..f7ac7e6d --- /dev/null +++ b/Sami/Parser/Filter/SymfonyFilter.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser\Filter; + +use Sami\Reflection\ClassReflection; +use Sami\Reflection\MethodReflection; + +class SymfonyFilter extends DefaultFilter +{ + public function acceptClass(ClassReflection $class) + { + return $class->getDocBlock()->getTag('api'); + } + + public function acceptMethod(MethodReflection $method) + { + return parent::acceptMethod($method) && $method->getDocBlock()->getTag('api'); + } +} diff --git a/Sami/Parser/Filter/TrueFilter.php b/Sami/Parser/Filter/TrueFilter.php new file mode 100644 index 00000000..fe0b8d14 --- /dev/null +++ b/Sami/Parser/Filter/TrueFilter.php @@ -0,0 +1,34 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser\Filter; + +use Sami\Reflection\ClassReflection; +use Sami\Reflection\MethodReflection; +use Sami\Reflection\PropertyReflection; + +class TrueFilter implements FilterInterface +{ + public function acceptClass(ClassReflection $class) + { + return true; + } + + public function acceptMethod(MethodReflection $method) + { + return true; + } + + public function acceptProperty(PropertyReflection $property) + { + return true; + } +} diff --git a/Sami/Parser/Node/DocBlockNode.php b/Sami/Parser/Node/DocBlockNode.php new file mode 100644 index 00000000..b98c74be --- /dev/null +++ b/Sami/Parser/Node/DocBlockNode.php @@ -0,0 +1,84 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser\Node; + +class DocBlockNode +{ + protected $shortDesc; + protected $longDesc; + protected $tags = array(); + protected $errors = array(); + + public function addTag($key, $value) + { + $this->tags[$key][] = $value; + } + + public function getTags() + { + return $this->tags; + } + + public function getOtherTags() + { + $tags = $this->tags; + unset($tags['param'], $tags['return'], $tags['var'], $tags['throws']); + + foreach ($tags as $name => $values) { + foreach ($values as $i => $value) { + $tags[$name][$i] = explode(' ', $value); + } + } + + return $tags; + } + + public function getTag($key) + { + return isset($this->tags[$key]) ? $this->tags[$key] : array(); + } + + public function getShortDesc() + { + return $this->shortDesc; + } + + public function getLongDesc() + { + return $this->longDesc; + } + + public function setShortDesc($shortDesc) + { + $this->shortDesc = $shortDesc; + } + + public function setLongDesc($longDesc) + { + $this->longDesc = $longDesc; + } + + public function getDesc() + { + return $this->shortDesc."\n\n".$this->longDesc; + } + + public function addError($error) + { + $this->errors[] = $error; + } + + public function getErrors() + { + return $this->errors; + } +} diff --git a/Sami/Parser/NodeVisitor.php b/Sami/Parser/NodeVisitor.php new file mode 100644 index 00000000..f4d75684 --- /dev/null +++ b/Sami/Parser/NodeVisitor.php @@ -0,0 +1,269 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser; + +use Sami\Reflection\ClassReflection; +use Sami\Reflection\MethodReflection; +use Sami\Reflection\ParameterReflection; +use Sami\Reflection\PropertyReflection; +use Sami\Reflection\ConstantReflection; +use Sami\Project; + +class NodeVisitor extends \PHPParser_NodeVisitorAbstract +{ + protected $context; + + public function __construct(ParserContext $context) + { + $this->context = $context; + } + + public function enterNode(\PHPParser_Node $node) + { + if ($node instanceof \PHPParser_Node_Stmt_Namespace) { + $this->context->enterNamespace((string) $node->name); + } elseif ($node instanceof \PHPParser_Node_Stmt_Use) { + $this->addAliases($node); + } elseif ($node instanceof \PHPParser_Node_Stmt_Interface) { + $this->addInterface($node); + } elseif ($node instanceof \PHPParser_Node_Stmt_Class) { + $this->addClass($node); + } elseif ($this->context->getClass() && $node instanceof \PHPParser_Node_Stmt_Property) { + $this->addProperty($node); + } elseif ($this->context->getClass() && $node instanceof \PHPParser_Node_Stmt_ClassMethod) { + $this->addMethod($node); + } elseif ($this->context->getClass() && $node instanceof \PHPParser_Node_Stmt_ClassConst) { + $this->addConstant($node); + } + } + + public function leaveNode(\PHPParser_Node $node) + { + if ($node instanceof \PHPParser_Node_Stmt_Namespace) { + $this->context->leaveNamespace(); + } elseif ($node instanceof \PHPParser_Node_Stmt_Class || $node instanceof \PHPParser_Node_Stmt_Interface) { + $this->context->leaveClass(); + } + } + + protected function addAliases(\PHPParser_Node_Stmt_Use $node) + { + foreach ($node->uses as $use) { + $this->context->addAlias($use->alias, (string) $use->name); + } + } + + protected function addInterface(\PHPParser_Node_Stmt_Interface $node) + { + $class = $this->addClassOrInterface($node); + + $class->setInterface(true); + foreach ($node->extends as $interface) { + $class->addInterface((string) $interface); + } + } + + protected function addClass(\PHPParser_Node_Stmt_Class $node) + { + $class = $this->addClassOrInterface($node); + + foreach ($node->implements as $interface) { + $class->addInterface((string) $interface); + } + + if ($node->extends) { + $class->setParent((string) $node->extends); + } + } + + protected function addClassOrInterface($node) + { + $class = new ClassReflection((string) $node->namespacedName, $node->getLine()); + $class->setModifiers($node->type); + $class->setNamespace($this->context->getNamespace()); + $class->setAliases($this->context->getAliases()); + $class->setHash($this->context->getHash()); + $class->setFile($this->context->getFile()); + + if ($this->context->getFilter()->acceptClass($class)) { + $comment = $this->context->getDocBlockParser()->parse($node->getDocComment(), $this->context, $class); + $class->setDocComment($node->getDocComment()); + $class->setShortDesc($comment->getShortDesc()); + $class->setLongDesc($comment->getLongDesc()); + if ($errors = $comment->getErrors()) { + $this->context->addErrors((string) $class, $node->getLine(), $errors); + } else { + $class->setTags($comment->getOtherTags()); + } + + $this->context->enterClass($class); + } + + return $class; + } + + protected function addMethod(\PHPParser_Node_Stmt_ClassMethod $node) + { + $method = new MethodReflection($node->name, $node->getLine()); + $method->setModifiers((string) $node->type); + + if ($this->context->getFilter()->acceptMethod($method)) { + $this->context->getClass()->addMethod($method); + + $method->setByRef((string) $node->byRef); + + foreach ($node->params as $param) { + $parameter = new ParameterReflection($param->name, $param->getLine()); + $parameter->setModifiers((string) $param->type); + $parameter->setByRef($param->byRef); + if ($param->default) { + $parameter->setDefault($this->context->getPrettyPrinter()->prettyPrintExpr($param->default)); + } + if ((string) $param->type) { + $parameter->setHint($this->resolveHint(array(array((string) $param->type, false)))); + } + $method->addParameter($parameter); + } + + $comment = $this->context->getDocBlockParser()->parse($node->getDocComment(), $this->context, $method); + $method->setDocComment($node->getDocComment()); + $method->setShortDesc($comment->getShortDesc()); + $method->setLongDesc($comment->getLongDesc()); + if (!$errors = $comment->getErrors()) { + $errors = $this->updateMethodParametersFromTags($method, $comment->getTag('param')); + + if ($tag = $comment->getTag('return')) { + $method->setHint($this->resolveHint($tag[0][0])); + $method->setHintDesc($tag[0][1]); + } + + $method->setExceptions($comment->getTag('throws')); + $method->setTags($comment->getOtherTags()); + } + + $this->context->addErrors((string) $method, $node->getLine(), $errors); + } + } + + protected function addProperty(\PHPParser_Node_Stmt_Property $node) + { + foreach ($node->props as $prop) { + $property = new PropertyReflection($prop->name, $prop->getLine()); + $property->setModifiers($node->type); + + if ($this->context->getFilter()->acceptProperty($property)) { + $this->context->getClass()->addProperty($property); + + $property->setDefault($prop->default); + + $comment = $this->context->getDocBlockParser()->parse($node->getDocComment(), $this->context, $property); + $property->setDocComment($node->getDocComment()); + $property->setShortDesc($comment->getShortDesc()); + $property->setLongDesc($comment->getLongDesc()); + if ($errors = $comment->getErrors()) { + $this->context->addErrors((string) $property, $prop->getLine(), $errors); + } else { + if ($tag = $comment->getTag('var')) { + $property->setHint($this->resolveHint($tag[0][0])); + $property->setHintDesc($tag[0][1]); + } + + $property->setTags($comment->getOtherTags()); + } + } + } + } + + protected function addConstant(\PHPParser_Node_Stmt_ClassConst $node) + { + foreach ($node->consts as $const) { + $constant = new ConstantReflection($const->name, $const->getLine()); + $comment = $this->context->getDocBlockParser()->parse($node->getDocComment(), $this->context, $constant); + $constant->setDocComment($node->getDocComment()); + $constant->setShortDesc($comment->getShortDesc()); + $constant->setLongDesc($comment->getLongDesc()); + + $this->context->getClass()->addConstant($constant); + } + } + + protected function updateMethodParametersFromTags(MethodReflection $method, array $tags) + { + // bypass if there is no @param tags defined (@param tags are optional) + if (!count($tags)) { + return array(); + } + + if (count($method->getParameters()) != count($tags)) { + return array(sprintf('"%d" @param tags are defined by "%d" are expected', count($tags), count($method->getParameters()))); + } + + $errors = array(); + foreach (array_keys($method->getParameters()) as $i => $name) { + if ($tags[$i][1] && $tags[$i][1] != $name) { + $errors[] = sprintf('The "%s" @param tag variable name is wrong (should be "%s")', $tags[$i][1], $name); + } + } + + if ($errors) { + return $errors; + } + + foreach ($tags as $i => $tag) { + $parameter = $method->getParameter($tag[1] ? $tag[1] : $i); + $parameter->setShortDesc($tag[2]); + if (!$parameter->hasHint()) { + $parameter->setHint($this->resolveHint($tag[0])); + } + } + + return array(); + } + + protected function resolveHint($hints) + { + foreach ($hints as $i => $hint) { + $hints[$i] = array($this->resolveAlias($hint[0]), $hint[1]); + } + + return $hints; + } + + protected function resolveAlias($alias) + { + // not a class + if (Project::isPhpTypeHint($alias)) { + return $alias; + } + + // FQCN + if ('\\' == substr($alias, 0, 1)) { + return $alias; + } + + $class = $this->context->getClass(); + + // special aliases + if ('self' === $alias || 'static' === $alias) { + return $class->getName(); + } + + // an alias defined by a use statement + $aliases = $class->getAliases(); + if (isset($aliases[$alias])) { + return $aliases[$alias]; + } + + // a class in the current class namespace + return $class->getNamespace().'\\'.$alias; + } +} diff --git a/Sami/Parser/Parser.php b/Sami/Parser/Parser.php new file mode 100644 index 00000000..8b3ddbf2 --- /dev/null +++ b/Sami/Parser/Parser.php @@ -0,0 +1,99 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser; + +use Sami\Store\StoreInterface; +use Sami\Reflection\LazyClassReflection; +use Sami\Parser\Parser; +use Sami\Parser\Transaction; +use Sami\Project; +use Sami\Message; +use Symfony\Component\Finder\Finder; + +class Parser +{ + protected $store; + protected $iterator; + protected $parser; + protected $traverser; + + public function __construct($iterator, StoreInterface $store, CodeParser $parser, ClassTraverser $traverser) + { + $this->iterator = $this->createIterator($iterator); + $this->store = $store; + $this->parser = $parser; + $this->traverser = $traverser; + } + + public function parse(Project $project, $callback = null) + { + $step = 0; + $steps = iterator_count($this->iterator); + $context = $this->parser->getContext(); + $transaction = new Transaction($project); + foreach ($this->iterator as $file) { + ++$step; + + $code = file_get_contents($file); + $hash = sha1($code); + if ($transaction->hasHash($hash)) { + continue; + } + + $context->enterFile((string) $file, $hash); + + $this->parser->parse($code); + + if (null !== $callback) { + call_user_func($callback, Message::PARSE_ERROR, $context->getErrors()); + } + + foreach ($context->leaveFile() as $class) { + if (null !== $callback) { + call_user_func($callback, Message::PARSE_CLASS, array(floor($step / $steps * 100), $class)); + } + + $project->addClass($class); + $transaction->addClass($class); + $this->store->writeClass($project, $class); + } + } + + // cleanup + foreach ($transaction->getRemovedClasses() as $class) { + $project->removeClass(new LazyClassReflection($class)); + $this->store->removeClass($project, $class); + } + + // visit each class for stuff that can only be done when all classes are parsed + $modified = $this->traverser->traverse($project); + foreach ($modified as $class) { + $this->store->writeClass($project, $class); + } + + return $transaction; + } + + private function createIterator($iterator) + { + if (is_string($iterator)) { + $it = new Finder(); + $it->files()->name('*.php')->in($iterator); + + return $it; + } elseif (!$iterator instanceof \Traversable) { + throw new \InvalidArgumentException('The iterator must be a directory name or a Finder instance.'); + } + + return $iterator; + } +} diff --git a/Sami/Parser/ParserContext.php b/Sami/Parser/ParserContext.php new file mode 100644 index 00000000..6ae9c5ce --- /dev/null +++ b/Sami/Parser/ParserContext.php @@ -0,0 +1,139 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser; + +use Sami\Reflection\ClassReflection; +use Sami\Parser\Filter\FilterInterface; +use Sami\Parser\DocBlockParser; + +class ParserContext +{ + protected $filter; + protected $docBlockParser; + protected $prettyPrinter; + protected $errors; + protected $namespace; + protected $aliases; + protected $class; + protected $file; + protected $hash; + protected $classes; + + public function __construct(FilterInterface $filter, DocBlockParser $docBlockParser, $prettyPrinter) + { + $this->filter = $filter; + $this->docBlockParser = $docBlockParser; + $this->prettyPrinter = $prettyPrinter; + } + + public function getFilter() + { + return $this->filter; + } + + public function getDocBlockParser() + { + return $this->docBlockParser; + } + + public function getPrettyPrinter() + { + return $this->prettyPrinter; + } + + public function addAlias($alias, $name) + { + $this->aliases[$alias] = $name; + } + + public function getAliases() + { + return $this->aliases; + } + + public function enterFile($file, $hash) + { + $this->file = $file; + $this->hash = $hash; + $this->errors = array(); + $this->classes = array(); + } + + public function leaveFile() + { + $this->hash = null; + $this->file = null; + $this->errors = array(); + + return $this->classes; + } + + public function getHash() + { + return $this->hash; + } + + public function getFile() + { + return $this->file; + } + + public function addErrors($name, $line, array $errors) + { + foreach ($errors as $error) { + $this->addError($name, $line, $error); + } + } + + public function addError($name, $line, $error) + { + $this->errors[] = sprintf('An error occured while parsing "%s" line "%d": %s', $name, $line, $error); + } + + public function getErrors() + { + return $this->errors; + } + + public function enterClass(ClassReflection $class) + { + $this->class = $class; + } + + public function leaveClass() + { + $this->classes[] = $this->class; + $this->class = null; + } + + public function getClass() + { + return $this->class; + } + + public function enterNamespace($namespace) + { + $this->namespace = $namespace; + $this->aliases = array(); + } + + public function leaveNamespace() + { + $this->namespace = null; + $this->aliases = array(); + } + + public function getNamespace() + { + return $this->namespace; + } +} diff --git a/Sami/Parser/Transaction.php b/Sami/Parser/Transaction.php new file mode 100644 index 00000000..5ac2e98f --- /dev/null +++ b/Sami/Parser/Transaction.php @@ -0,0 +1,82 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Parser; + +use Sami\Project; +use Sami\Reflection\ClassReflection; + +class Transaction +{ + protected $hashes; + protected $classes; + protected $visited; + protected $modified; + + public function __construct(Project $project) + { + $this->hashes = array(); + $this->classes = array(); + + foreach ($project->getProjectClasses() as $class) { + $this->addClass($class); + } + + $this->visited = array(); + $this->modified = array(); + } + + public function hasHash($hash) + { + if (!array_key_exists($hash, $this->hashes)) { + return false; + } + + $this->visited[$hash] = true; + + return true; + } + + public function getModifiedClasses() + { + return $this->modified; + } + + public function getRemovedClasses() + { + $classes = array(); + foreach ($this->hashes as $hash => $c) { + if (!isset($this->visited[$hash])) { + $classes = array_merge($classes, $c); + } + } + + return array_keys($classes); + } + + public function addClass(ClassReflection $class) + { + $name = $class->getName(); + $hash = $class->getHash(); + + if (isset($this->classes[$name])) { + unset($this->hashes[$this->classes[$name]][$name]); + if (!$this->hashes[$this->classes[$name]]) { + unset($this->hashes[$this->classes[$name]]); + } + } + + $this->hashes[$hash][$name] = true; + $this->classes[$name] = $hash; + $this->modified[] = $name; + $this->visited[$hash] = true; + } +} diff --git a/Sami/Project.php b/Sami/Project.php new file mode 100644 index 00000000..24b2b21f --- /dev/null +++ b/Sami/Project.php @@ -0,0 +1,430 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami; + +use Sami\Store\StoreInterface; +use Sami\Reflection\ClassReflection; +use Sami\Reflection\LazyClassReflection; +use Sami\Parser\Parser; +use Sami\Renderer\Renderer; +use Sami\Version\Version; +use Sami\Version\VersionCollection; +use Symfony\Component\Filesystem\Filesystem; + +/** + * Project represents an API project. + * + * @author Fabien Potencier + */ +class Project +{ + protected $versions; + protected $store; + protected $parser; + protected $renderer; + protected $classes; + protected $namespaceClasses; + protected $namespaceInterfaces; + protected $namespaceExceptions; + protected $namespaces; + protected $simulatedNamespaces; + protected $sami; + protected $indexer; + protected $tree; + protected $version; + protected $filesystem; + + public function __construct(VersionCollection $versions, StoreInterface $store, Parser $parser, Renderer $renderer, Tree $tree, Indexer $indexer, Sami $sami) + { + $this->versions = $versions; + $this->store = $store; + $this->parser = $parser; + $this->renderer = $renderer; + $this->sami = $sami; + $this->tree = $tree; + $this->indexer = $indexer; + $this->filesystem = new Filesystem(); + + if (count($this->versions) > 1) { + foreach (array('build_dir', 'cache_dir') as $dir) { + if (false === strpos($this->sami[$dir], '%version%')) { + throw new \LogicException(sprintf('The "%s" setting must have the "%%version%%" placeholder as the project has more than one version.', $dir)); + } + } + } + + $this->initialize(); + } + + public function getConfig($name, $default = null) + { + return isset($this->sami[$name]) ? $this->sami[$name] : $default; + } + + public function getVersion() + { + return $this->version; + } + + public function getVersions() + { + return $this->versions->getVersions(); + } + + public function update($callback = null, $force = false) + { + $previousParse = null; + $previousRender = null; + foreach ($this->versions as $version) { + $this->switchVersion($version, $callback); + + $this->parseVersion($version, $previousParse, $callback, $force); + $this->renderVersion($version, $previousRender, $callback, $force); + + $previousParse = $this->getCacheDir(); + $previousRender = $this->getBuildDir(); + } + } + + public function parse($callback = null, $force = false) + { + $previous = null; + foreach ($this->versions as $version) { + $this->switchVersion($version, $callback); + + $this->parseVersion($version, $previous, $callback, $force); + + $previous = $this->getCacheDir(); + } + } + + public function render($callback = null, $force = false) + { + $previous = null; + foreach ($this->versions as $version) { + $this->switchVersion($version, $callback); + + $this->renderVersion($version, $previous, $callback, $force); + + $previous = $this->getBuildDir(); + } + } + + public function switchVersion(Version $version, $callback = null) + { + call_user_func($callback, Message::SWITCH_VERSION, $version); + + $this->version = $version; + $this->read(); + } + + public function getIndex() + { + return $this->indexer->getIndex($this); + } + + public function getTree() + { + return $this->tree->getTree($this); + } + + public function hasNamespaces() + { + // if there is only one namespace and this is the global one, it means that there is no namespace in the project + return array('') != array_keys($this->namespaces); + } + + public function hasNamespace($namespace) + { + return array_key_exists($namespace, $this->namespaces); + } + + public function getNamespaces() + { + ksort($this->namespaces); + + return array_keys($this->namespaces); + } + + public function getSimulatedNamespaces() + { + ksort($this->simulatedNamespaces); + + return array_keys($this->simulatedNamespaces); + } + + public function getSimulatedNamespaceAllClasses($namespace) + { + if (!isset($this->simulatedNamespaces[$namespace])) { + return array(); + } + + ksort($this->simulatedNamespaces[$namespace]); + + return $this->simulatedNamespaces[$namespace]; + } + + public function getNamespaceAllClasses($namespace) + { + $classes = array_merge( + $this->getNamespaceExceptions($namespace), + $this->getNamespaceInterfaces($namespace), + $this->getNamespaceClasses($namespace) + ); + + ksort($classes); + + return $classes; + } + + public function getNamespaceExceptions($namespace) + { + if (!isset($this->namespaceExceptions[$namespace])) { + return array(); + } + + ksort($this->namespaceExceptions[$namespace]); + + return $this->namespaceExceptions[$namespace]; + } + + public function getNamespaceClasses($namespace) + { + if (!isset($this->namespaceClasses[$namespace])) { + return array(); + } + + ksort($this->namespaceClasses[$namespace]); + + return $this->namespaceClasses[$namespace]; + } + + public function getNamespaceInterfaces($namespace) + { + if (!isset($this->namespaceInterfaces[$namespace])) { + return array(); + } + + ksort($this->namespaceInterfaces[$namespace]); + + return $this->namespaceInterfaces[$namespace]; + } + + public function addClass(ClassReflection $class) + { + $this->classes[$class->getName()] = $class; + $class->setProject($this); + + if ($class->isProjectClass()) { + $this->updateCache($class); + } + } + + public function removeClass(ClassReflection $class) + { + unset($this->classes[$class->getName()]); + unset($this->interfaces[$class->getName()]); + unset($this->namespaceClasses[$class->getNamespace()][$class->getName()]); + unset($this->namespaceInterfaces[$class->getNamespace()][$class->getName()]); + unset($this->namespaceExceptions[$class->getNamespace()][$class->getName()]); + } + + public function getProjectInterfaces() + { + $interfaces = array(); + foreach ($this->interfaces as $interface) { + if ($interface->isProjectClass()) { + $interfaces[$interface->getName()] = $interface; + } + } + ksort($interfaces); + + return $interfaces; + } + + public function getProjectClasses() + { + $classes = array(); + foreach ($this->classes as $name => $class) { + if ($class->isProjectClass()) { + $classes[$name] = $class; + } + } + ksort($classes); + + return $classes; + } + + public function getClass($name) + { + $name = ltrim($name, '\\'); + + if (isset($this->classes[$name])) { + return $this->classes[$name]; + } + + $this->addClass($class = new LazyClassReflection($name)); + + return $class; + } + + // this must only be used in LazyClassReflection to get the right values + public function loadClass($name) + { + $name = ltrim($name, '\\'); + + if ($this->getClass($name) instanceof LazyClassReflection) { + try { + $this->addClass($this->store->readClass($this, $name)); + } catch (\InvalidArgumentException $e) { + // probably a PHP built-in class + return null; + } + } + + return $this->classes[$name]; + } + + public function initialize() + { + $this->namespaces = array(); + $this->simulatedNamespaces = array(); + $this->interfaces = array(); + $this->classes = array(); + $this->namespaceClasses = array(); + $this->namespaceInterfaces = array(); + $this->namespaceExceptions = array(); + } + + public function read() + { + $this->initialize(); + + foreach ($this->store->readProject($this) as $class) { + $this->addClass($class); + } + } + + public function getBuildDir() + { + return $this->prepareDir($this->sami['build_dir']); + } + + public function getCacheDir() + { + return $this->prepareDir($this->sami['cache_dir']); + } + + public function flushDir($dir) + { + $this->filesystem->remove($dir); + $this->filesystem->mkdir($dir); + file_put_contents($dir.'/SAMI_VERSION', Sami::VERSION); + file_put_contents($dir.'/PROJECT_VERSION', $this->version); + } + + public function seedCache($previous, $current) + { + $this->filesystem->remove($current); + $this->filesystem->mirror($previous, $current); + $this->read(); + } + + static public function isPhpTypeHint($hint) + { + return in_array(strtolower($hint), array('', 'scalar', 'object', 'boolean', 'bool', 'int', 'integer', 'array', 'string', 'mixed', 'void', 'null')); + } + + protected function updateCache(ClassReflection $class) + { + $name = $class->getName(); + + $this->namespaces[$class->getNamespace()] = $class->getNamespace(); + if ($class->isException()) { + $this->namespaceExceptions[$class->getNamespace()][$name] = $class; + } elseif ($class->isInterface()) { + $this->namespaceInterfaces[$class->getNamespace()][$name] = $class; + $this->interfaces[$name] = $class; + } else { + $this->namespaceClasses[$class->getNamespace()][$name] = $class; + } + + if ($this->getConfig('simulate_namespaces')) { + if (false !== $pos = strrpos($name, '_')) { + $this->simulatedNamespaces[str_replace('_', '\\', substr($name, 0, $pos))][$name] = $class; + } else { + $this->simulatedNamespaces[''][$name] = $class; + } + } + } + + protected function prepareDir($dir) + { + $dir = $this->replaceVars($dir); + + if (!is_dir($dir)) { + $this->flushDir($dir); + } + + $samiVersion = null; + if (file_exists($dir.'/SAMI_VERSION')) { + $samiVersion = file_get_contents($dir.'/SAMI_VERSION'); + } + + if ($samiVersion !== Sami::VERSION) { + $this->flushDir($dir); + } + + return $dir; + } + + protected function replaceVars($pattern) + { + return str_replace('%version%', $this->version, $pattern); + } + + protected function parseVersion(Version $version, $previous, $callback = null, $force = false) + { + if ($version->isFrozen() && count($this->classes) > 0) { + return; + } + + if ($force) { + $this->store->flushProject($this); + } + + if ($previous && 0 === count($this->classes)) { + $this->seedCache($previous, $this->getCacheDir()); + } + + $transaction = $this->parser->parse($this, $callback); + + call_user_func($callback, Message::PARSE_VERSION_FINISHED, $transaction); + } + + protected function renderVersion(Version $version, $previous, $callback = null, $force = false) + { + $frozen = $version->isFrozen() && $this->renderer->isRendered($this) && $this->version === file_get_contents($this->getBuildDir().'/PROJECT_VERSION'); + + if ($force && !$frozen) { + $this->flushDir($this->getBuildDir()); + } + + if ($previous && !$this->renderer->isRendered($this)) { + $this->seedCache($previous, $this->getBuildDir()); + } + + $diff = $this->renderer->render($this, $callback); + + call_user_func($callback, Message::RENDER_VERSION_FINISHED, $diff); + } +} diff --git a/Sami/Reflection/ClassReflection.php b/Sami/Reflection/ClassReflection.php new file mode 100644 index 00000000..1cbd0613 --- /dev/null +++ b/Sami/Reflection/ClassReflection.php @@ -0,0 +1,397 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Reflection; + +use Sami\Project; + +class ClassReflection extends Reflection +{ + protected $project; + protected $hash; + protected $namespace; + protected $modifiers; + protected $properties = array(); + protected $methods = array(); + protected $interfaces = array(); + protected $constants = array(); + protected $parent; + protected $file; + protected $interface = false; + protected $projectClass = true; + protected $aliases = array(); + + public function __toString() + { + return $this->name; + } + + public function getClass() + { + return $this; + } + + public function isProjectClass() + { + return $this->projectClass; + } + + public function isPhpClass() + { + try { + $r = new \ReflectionClass($this->name); + + return $r->isInternal(); + } catch (\ReflectionException $e) { + return false; + } + } + + public function setName($name) + { + parent::setName(ltrim($name, '\\')); + } + + public function getShortName() + { + if (false !== $pos = strrpos($this->name, '\\')) { + return substr($this->name, strrpos($this->name, '\\') + 1); + } + + return $this->name; + } + + public function isAbstract() + { + return self::MODIFIER_ABSTRACT === (self::MODIFIER_ABSTRACT & $this->modifiers); + } + + public function isFinal() + { + return self::MODIFIER_FINAL === (self::MODIFIER_FINAL & $this->modifiers); + } + + public function getHash() + { + return $this->hash; + } + + public function setHash($hash) + { + $this->hash = $hash; + } + + public function getFile() + { + return $this->file; + } + + public function setFile($file) + { + $this->file = $file; + } + + public function getProject() + { + return $this->project; + } + + public function setProject(Project $project) + { + $this->project = $project; + } + + public function setNamespace($namespace) + { + $this->namespace = ltrim($namespace, '\\'); + } + + public function getNamespace() + { + return $this->namespace; + } + + public function setModifiers($modifiers) + { + $this->modifiers = $modifiers; + } + + public function addProperty(PropertyReflection $property) + { + $this->properties[$property->getName()] = $property; + $property->setClass($this); + } + + public function getProperties($deep = false) + { + if (false === $deep) { + return $this->properties; + } + + $properties = array(); + if ($this->getParent()) { + foreach ($this->getParent()->getProperties(true) as $name => $property) { + $properties[$name] = $property; + } + } + + foreach ($this->properties as $name => $property) { + $properties[$name] = $property; + } + + return $properties; + } + + /* + * Can be any iterator (so that we can lazy-load the properties) + */ + public function setProperties($properties) + { + $this->properties = $properties; + } + + public function addConstant(ConstantReflection $constant) + { + $this->constants[$constant->getName()] = $constant; + $constant->setClass($this); + } + + public function getConstants($deep = false) + { + if (false === $deep) { + return $this->constants; + } + + $constants = array(); + if ($this->getParent()) { + foreach ($this->getParent()->getConstants(true) as $name => $constant) { + $constants[$name] = $constant; + } + } + + foreach ($this->constants as $name => $constant) { + $constants[$name] = $constant; + } + + return $constants; + } + + public function setConstants($constants) + { + $this->constants = $constants; + } + + public function addMethod(MethodReflection $method) + { + $this->methods[$method->getName()] = $method; + $method->setClass($this); + } + + public function getMethod($name) + { + return isset($this->methods[$name]) ? $this->methods[$name] : false; + } + + public function getParentMethod($name) + { + if ($this->getParent()) { + foreach ($this->getParent()->getMethods() as $n => $method) { + if ($name == $n) { + return $method; + } + } + } + + foreach ($this->getInterfaces() as $interface) { + foreach ($interface->getMethods() as $n => $method) { + if ($name == $n) { + return $method; + } + } + } + } + + public function getMethods($deep = false) + { + if (false === $deep) { + return $this->methods; + } + + $methods = array(); + if ($this->isInterface()) { + foreach ($this->getInterfaces() as $interface) { + foreach ($interface->getMethods() as $name => $method) { + $methods[$name] = $method; + } + } + } + + if ($this->getParent()) { + foreach ($this->getParent()->getMethods() as $name => $method) { + $methods[$name] = $method; + } + } + + foreach ($this->methods as $name => $method) { + $methods[$name] = $method; + } + + return $methods; + } + + public function setMethods($methods) + { + $this->methods = $methods; + } + + public function addInterface($interface) + { + $this->interfaces[$interface] = $interface; + } + + public function getInterfaces($deep = false) + { + $interfaces = array(); + foreach ($this->interfaces as $interface) { + $interfaces[] = $this->project->getClass($interface); + } + + if (false === $deep) { + return $interfaces; + } + + $allInterfaces = $interfaces; + foreach ($interfaces as $interface) { + $allInterfaces = array_merge($allInterfaces, $interface->getInterfaces()); + } + + if ($parent = $this->getParent()) { + $allInterfaces = array_merge($allInterfaces, $parent->getInterfaces()); + } + + return $allInterfaces; + } + + public function setParent($parent) + { + $this->parent = $parent; + } + + public function getParent($deep = false) + { + if (!$this->parent) { + return $deep ? array() : null; + } + + $parent = $this->project->getClass($this->parent); + + if (false === $deep) { + return $parent; + } + + return array_merge(array($parent), $parent->getParent(true)); + } + + public function setInterface($boolean) + { + $this->interface = (Boolean) $boolean; + } + + public function isInterface() + { + return $this->interface; + } + + public function isException() + { + $parent = $this; + while ($parent = $parent->getParent()) { + if ('Exception' == $parent->getName()) { + return true; + } + } + + return false; + } + + public function getAliases() + { + return $this->aliases; + } + + public function setAliases($aliases) + { + $this->aliases = $aliases; + } + + public function toArray() + { + return array( + 'name' => $this->name, + 'line' => $this->line, + 'short_desc' => $this->shortDesc, + 'long_desc' => $this->longDesc, + 'hint' => $this->hint, + 'tags' => $this->tags, + 'namespace' => $this->namespace, + 'file' => $this->file, + 'hash' => $this->hash, + 'parent' => $this->parent, + 'modifiers' => $this->modifiers, + 'is_interface' => $this->interface, + 'aliases' => $this->aliases, + 'interfaces' => $this->interfaces, + 'properties' => array_map(function ($property) { return $property->toArray(); }, $this->properties), + 'methods' => array_map(function ($method) { return $method->toArray(); }, $this->methods), + 'constants' => array_map(function ($constant) { return $constant->toArray(); }, $this->constants), + ); + } + + static public function fromArray(Project $project, $array) + { + $class = new self($array['name'], $array['line']); + $class->shortDesc = $array['short_desc']; + $class->longDesc = $array['long_desc']; + $class->hint = $array['hint']; + $class->tags = $array['tags']; + $class->namespace = $array['namespace']; + $class->hash = $array['hash']; + $class->file = $array['file']; + $class->modifiers = $array['modifiers']; + $class->interface = $array['is_interface']; + $class->aliases = $array['aliases']; + $class->parent = $array['parent']; + $class->interfaces = $array['interfaces']; + $class->constants = $array['constants']; + + $class->setProject($project); + + foreach ($array['methods'] as $method) { + $method = MethodReflection::fromArray($project, $method); + $method->setClass($class); + $class->addMethod($method); + } + + foreach ($array['properties'] as $property) { + $property = PropertyReflection::fromArray($project, $property); + $property->setClass($class); + $class->addProperty($property); + } + + foreach ($array['constants'] as $constant) { + $constant = ConstantReflection::fromArray($project, $constant); + $constant->setClass($class); + $class->addConstant($constant); + } + + return $class; + } +} diff --git a/Sami/Reflection/ConstantReflection.php b/Sami/Reflection/ConstantReflection.php new file mode 100644 index 00000000..b9778e63 --- /dev/null +++ b/Sami/Reflection/ConstantReflection.php @@ -0,0 +1,53 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Reflection; + +use Sami\Project; + +class ConstantReflection extends Reflection +{ + protected $class; + + public function __toString() + { + return $this->class.'::'.$this->name; + } + + public function getClass() + { + return $this->class; + } + + public function setClass(ClassReflection $class) + { + $this->class = $class; + } + + public function toArray() + { + return array( + 'name' => $this->name, + 'line' => $this->line, + 'short_desc' => $this->shortDesc, + 'long_desc' => $this->longDesc, + ); + } + + static public function fromArray(Project $project, $array) + { + $constant = new self($array['name'], $array['line']); + $constant->shortDesc = $array['short_desc']; + $constant->longDesc = $array['long_desc']; + + return $constant; + } +} diff --git a/Sami/Reflection/HintReflection.php b/Sami/Reflection/HintReflection.php new file mode 100644 index 00000000..cd6b07ab --- /dev/null +++ b/Sami/Reflection/HintReflection.php @@ -0,0 +1,54 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Reflection; + +class HintReflection +{ + protected $name; + protected $array; + + public function __construct($name, $array) + { + $this->name = $name; + $this->array = $array; + } + + public function __toString() + { + return $this->name; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function isClass() + { + return $this->name instanceof ClassReflection; + } + + public function isArray() + { + return $this->array; + } + + public function setArray($boolean) + { + $this->array = (Boolean) $boolean; + } +} diff --git a/Sami/Reflection/LazyClassReflection.php b/Sami/Reflection/LazyClassReflection.php new file mode 100644 index 00000000..a36e26b7 --- /dev/null +++ b/Sami/Reflection/LazyClassReflection.php @@ -0,0 +1,237 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Reflection; + +class LazyClassReflection extends ClassReflection +{ + protected $loaded = false; + + public function __construct($name) + { + parent::__construct($name, -1, ''); + } + + public function isProjectClass() + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::isProjectClass(); + } + + public function getShortDesc() + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::getShortDesc(); + } + + public function setShortDesc($shortDesc) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function getLongDesc() + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::getLongDesc(); + } + + public function setLongDesc($longDesc) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function getHint() + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::getHint(); + } + + public function setHint($hint) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function isAbstract() + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::isAbstract(); + } + + public function isFinal() + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::isFinal(); + } + + public function getFile() + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::getFile(); + } + + public function setFile($file) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function setModifiers($modifiers) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function addProperty(PropertyReflection $property) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function getProperties($deep = false) + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::getProperties($deep); + } + + public function setProperties($properties) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function addMethod(MethodReflection $method) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function getParentMethod($name) + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::getParentMethod($name); + } + + public function getMethods($deep = false) + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::getMethods($deep); + } + + public function setMethods($methods) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function addInterface($interface) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function getInterfaces($deep = false) + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::getInterfaces($deep); + } + + public function setParent($parent) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function getParent($deep = false) + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::getParent($deep); + } + + public function setInterface($boolean) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + public function isInterface() + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::isInterface(); + } + + public function isException() + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::isException(); + } + + public function getAliases() + { + if (false === $this->loaded) { + $this->load(); + } + + return parent::getAliases(); + } + + public function setAliases($aliases) + { + throw new \LogicException('A LazyClassReflection instance is read-only.'); + } + + protected function load() + { + $class = $this->project->loadClass($this->name); + + if (null === $class) { + $this->projectClass = false; + } else { + foreach (array_keys(get_class_vars('Sami\Reflection\ClassReflection')) as $property) { + $this->$property = $class->$property; + } + } + + $this->loaded = true; + } +} diff --git a/Sami/Reflection/MethodReflection.php b/Sami/Reflection/MethodReflection.php new file mode 100644 index 00000000..6f9514a7 --- /dev/null +++ b/Sami/Reflection/MethodReflection.php @@ -0,0 +1,171 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Reflection; + +use Sami\Project; + +class MethodReflection extends Reflection +{ + protected $class; + protected $parameters = array(); + protected $byRef; + protected $modifiers; + protected $exceptions = array(); + + public function __toString() + { + return $this->class.'::'.$this->name; + } + + public function setByRef($boolean) + { + $this->byRef = $boolean; + } + + public function isByRef() + { + return $this->byRef; + } + + public function setModifiers($modifiers) + { + $this->modifiers = $modifiers; + } + + public function isPublic() + { + return self::MODIFIER_PUBLIC === (self::MODIFIER_PUBLIC & $this->modifiers); + } + + public function isProtected() + { + return self::MODIFIER_PROTECTED === (self::MODIFIER_PROTECTED & $this->modifiers); + } + + public function isPrivate() + { + return self::MODIFIER_PRIVATE === (self::MODIFIER_PRIVATE & $this->modifiers); + } + + public function isStatic() + { + return self::MODIFIER_STATIC === (self::MODIFIER_STATIC & $this->modifiers); + } + + public function isAbstract() + { + return self::MODIFIER_ABSTRACT === (self::MODIFIER_ABSTRACT & $this->modifiers); + } + + public function isFinal() + { + return self::MODIFIER_FINAL === (self::MODIFIER_FINAL & $this->modifiers); + } + + public function getClass() + { + return $this->class; + } + + public function setClass(ClassReflection $class) + { + $this->class = $class; + } + + public function addParameter(ParameterReflection $parameter) + { + $this->parameters[$parameter->getName()] = $parameter; + $parameter->setMethod($this); + } + + public function getParameters() + { + return $this->parameters; + } + + public function getParameter($name) + { + if (ctype_digit((string) $name)) { + $tmp = array_values($this->parameters); + + return isset($tmp[$name]) ? $tmp[$name] : null; + } + + return isset($this->parameters[$name]) ? $this->parameters[$name] : null; + } + + /* + * Can be any iterator (so that we can lazy-load the parameters) + */ + public function setParameters($parameters) + { + $this->parameters = $parameters; + } + + public function setExceptions($exceptions) + { + $this->exceptions = $exceptions; + } + + public function getExceptions() + { + $exceptions = array(); + foreach ($this->exceptions as $exception) { + $exception[0] = $this->class->getProject()->getClass($exception[0]); + $exceptions[] = $exception; + } + + return $exceptions; + } + + public function getRawExceptions() + { + return $this->exceptions; + } + + public function toArray() + { + return array( + 'name' => $this->name, + 'line' => $this->line, + 'short_desc' => $this->shortDesc, + 'long_desc' => $this->longDesc, + 'hint' => $this->hint, + 'hint_desc' => $this->hintDesc, + 'tags' => $this->tags, + 'modifiers' => $this->modifiers, + 'is_by_ref' => $this->byRef, + 'exceptions' => $this->exceptions, + 'parameters' => array_map(function ($parameter) { return $parameter->toArray(); }, $this->parameters), + ); + } + + static public function fromArray(Project $project, $array) + { + $method = new self($array['name'], $array['line']); + $method->shortDesc = $array['short_desc']; + $method->longDesc = $array['long_desc']; + $method->hint = $array['hint']; + $method->hintDesc = $array['hint_desc']; + $method->tags = $array['tags']; + $method->modifiers = $array['modifiers']; + $method->byRef = $array['is_by_ref']; + $method->exceptions = $array['exceptions']; + + foreach ($array['parameters'] as $parameter) { + $parameter = ParameterReflection::fromArray($project, $parameter); + $method->addParameter($parameter); + } + + return $method; + } +} diff --git a/Sami/Reflection/ParameterReflection.php b/Sami/Reflection/ParameterReflection.php new file mode 100644 index 00000000..c14a2943 --- /dev/null +++ b/Sami/Reflection/ParameterReflection.php @@ -0,0 +1,96 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Reflection; + +use Sami\Project; + +class ParameterReflection extends Reflection +{ + protected $method; + protected $byRef; + protected $modifiers; + protected $default; + + public function __toString() + { + return $this->method.'#'.$this->name; + } + + public function getClass() + { + return $this->method->getClass(); + } + + public function setModifiers($modifiers) + { + $this->modifiers = $modifiers; + } + + public function setByRef($boolean) + { + $this->byRef = $boolean; + } + + public function isByRef() + { + return $this->byRef; + } + + public function setDefault($default) + { + $this->default = $default; + } + + public function getDefault() + { + return $this->default; + } + + public function getMethod() + { + return $this->method; + } + + public function setMethod(MethodReflection $method) + { + $this->method = $method; + } + + public function toArray() + { + return array( + 'name' => $this->name, + 'line' => $this->line, + 'short_desc' => $this->shortDesc, + 'long_desc' => $this->longDesc, + 'hint' => $this->hint, + 'tags' => $this->tags, + 'modifiers' => $this->modifiers, + 'default' => $this->default, + 'is_by_ref' => $this->byRef, + ); + } + + static public function fromArray(Project $project, $array) + { + $parameter = new self($array['name'], $array['line']); + $parameter->shortDesc = $array['short_desc']; + $parameter->longDesc = $array['long_desc']; + $parameter->hint = $array['hint']; + $parameter->tags = $array['tags']; + $parameter->modifiers = $array['modifiers']; + $parameter->default = $array['default']; + $parameter->byRef = $array['is_by_ref']; + + return $parameter; + } +} diff --git a/Sami/Reflection/PropertyReflection.php b/Sami/Reflection/PropertyReflection.php new file mode 100644 index 00000000..150cba92 --- /dev/null +++ b/Sami/Reflection/PropertyReflection.php @@ -0,0 +1,105 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Reflection; + +use Sami\Project; + +class PropertyReflection extends Reflection +{ + protected $class; + protected $modifiers; + protected $default; + + public function __toString() + { + return $this->class.'::$'.$this->name; + } + + public function setModifiers($modifiers) + { + $this->modifiers = $modifiers; + } + + public function isPublic() + { + return self::MODIFIER_PUBLIC === (self::MODIFIER_PUBLIC & $this->modifiers); + } + + public function isProtected() + { + return self::MODIFIER_PROTECTED === (self::MODIFIER_PROTECTED & $this->modifiers); + } + + public function isPrivate() + { + return self::MODIFIER_PRIVATE === (self::MODIFIER_PRIVATE & $this->modifiers); + } + + public function isStatic() + { + return self::MODIFIER_STATIC === (self::MODIFIER_STATIC & $this->modifiers); + } + + public function isFinal() + { + return self::MODIFIER_FINAL === (self::MODIFIER_FINAL & $this->modifiers); + } + + public function setDefault($default) + { + $this->default = $default; + } + + public function getDefault() + { + return $this->default; + } + + public function getClass() + { + return $this->class; + } + + public function setClass(ClassReflection $class) + { + $this->class = $class; + } + + public function toArray() + { + return array( + 'name' => $this->name, + 'line' => $this->line, + 'short_desc' => $this->shortDesc, + 'long_desc' => $this->longDesc, + 'hint' => $this->hint, + 'hint_desc' => $this->hintDesc, + 'tags' => $this->tags, + 'modifiers' => $this->modifiers, + 'default' => $this->default, + ); + } + + static public function fromArray(Project $project, $array) + { + $property = new self($array['name'], $array['line']); + $property->shortDesc = $array['short_desc']; + $property->longDesc = $array['long_desc']; + $property->hint = $array['hint']; + $property->hintDesc = $array['hint_desc']; + $property->tags = $array['tags']; + $property->modifiers = $array['modifiers']; + $property->default = $array['default']; + + return $property; + } +} diff --git a/Sami/Reflection/Reflection.php b/Sami/Reflection/Reflection.php new file mode 100644 index 00000000..f3ae7676 --- /dev/null +++ b/Sami/Reflection/Reflection.php @@ -0,0 +1,153 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Reflection; + +use Sami\Project; + +abstract class Reflection +{ + const MODIFIER_PUBLIC = 1; + const MODIFIER_PROTECTED = 2; + const MODIFIER_PRIVATE = 4; + const MODIFIER_STATIC = 8; + const MODIFIER_ABSTRACT = 16; + const MODIFIER_FINAL = 32; + + protected $name; + protected $line; + protected $shortDesc; + protected $longDesc; + protected $hint; + protected $hintDesc; + protected $tags; + protected $docComment; + + public function __construct($name, $line) + { + $this->name = $name; + $this->line = $line; + $this->tags = array(); + } + + abstract public function getClass(); + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getLine() + { + return $this->line; + } + + public function setLine($line) + { + $this->line = $line; + } + + public function getShortDesc() + { + return $this->shortDesc; + } + + public function setShortDesc($shortDesc) + { + $this->shortDesc = $shortDesc; + } + + public function getLongDesc() + { + return $this->longDesc; + } + + public function setLongDesc($longDesc) + { + $this->longDesc = $longDesc; + } + + public function getHint() + { + if (!$this->hint) { + return array(); + } + + $hints = array(); + $project = $this->getClass()->getProject(); + foreach ($this->hint as $hint) { + $hints[] = new HintReflection(Project::isPhpTypeHint($hint[0]) ? $hint[0] : $project->getClass($hint[0]), $hint[1]); + } + + return $hints; + } + + public function getHintAsString() + { + $str = array(); + foreach ($this->getHint() as $hint) { + $str[] = ($hint->isClass() ? $hint->getName()->getShortName() : $hint->getName()).($hint->isArray() ? '[]' : ''); + } + + return implode('|', $str); + } + + public function hasHint() + { + return $this->hint ? true : false; + } + + public function setHint($hint) + { + $this->hint = $hint; + } + + public function getRawHint() + { + return $this->hint; + } + + public function setHintDesc($desc) + { + $this->hintDesc = $desc; + } + + public function getHintDesc() + { + return $this->hintDesc; + } + + public function setTags($tags) + { + $this->tags = $tags; + } + + public function getTags($name) + { + return isset($this->tags[$name]) ? $this->tags[$name] : array(); + } + + // not serialized as it is only useful when parsing + public function setDocComment($comment) + { + $this->docComment = $comment; + } + + public function getDocComment() + { + return $this->docComment; + } +} diff --git a/Sami/Renderer/Diff.php b/Sami/Renderer/Diff.php new file mode 100644 index 00000000..9a884f98 --- /dev/null +++ b/Sami/Renderer/Diff.php @@ -0,0 +1,107 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Renderer; + +use Sami\Project; + +class Diff +{ + protected $project; + protected $current; + protected $versions; + protected $filename; + protected $alreadyRendered; + protected $previousNamespaces; + protected $currentNamespaces; + + public function __construct(Project $project, $filename) + { + $this->project = $project; + $this->current = new Index($project); + $this->filename = $filename; + + if (file_exists($filename)) { + $this->alreadyRendered = true; + if (false === $this->previous = @unserialize(file_get_contents($filename))) { + $this->alreadyRendered = false; + $this->previous = new Index(); + } + } else { + $this->alreadyRendered = false; + $this->previous = new Index(); + } + + $this->previousNamespaces = array(); + foreach (array_keys($this->previous->getClasses()) as $class) { + $this->previousNamespaces[] = substr($class, 0, strrpos($class, '\\')); + } + $this->previousNamespaces = array_unique($this->previousNamespaces); + + $this->currentNamespaces = array(); + foreach (array_keys($this->current->getClasses()) as $class) { + $this->currentNamespaces[] = substr($class, 0, strrpos($class, '\\')); + } + $this->currentNamespaces = array_unique($this->currentNamespaces); + } + + public function isEmpty() + { + return !$this->areVersionsModified() && (0 == count($this->getModifiedClasses()) + count($this->getRemovedClasses())); + } + + public function save() + { + file_put_contents($this->filename, serialize($this->current)); + } + + public function isAlreadyRendered() + { + return $this->alreadyRendered; + } + + public function areVersionsModified() + { + $versions = array(); + foreach ($this->project->getVersions() as $version) { + $versions[] = (string) $version; + } + + return $versions != $this->previous->getVersions(); + } + + public function getModifiedNamespaces() + { + return array_diff($this->currentNamespaces, $this->previousNamespaces); + } + + public function getRemovedNamespaces() + { + return array_diff($this->previousNamespaces, $this->currentNamespaces); + } + + public function getModifiedClasses() + { + $classes = array(); + foreach ($this->current->getClasses() as $class => $hash) { + if ($hash !== $this->previous->getHash($class)) { + $classes[] = $this->project->getClass($class); + } + } + + return $classes; + } + + public function getRemovedClasses() + { + return array_diff(array_keys($this->previous->getClasses()), array_keys($this->current->getClasses())); + } +} diff --git a/Sami/Renderer/Index.php b/Sami/Renderer/Index.php new file mode 100644 index 00000000..d927a686 --- /dev/null +++ b/Sami/Renderer/Index.php @@ -0,0 +1,62 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Renderer; + +use Sami\Project; + +class Index implements \Serializable +{ + protected $classes; + protected $versions; + + public function __construct(Project $project = null) + { + $this->classes = array(); + if (null !== $project) { + foreach ($project->getProjectClasses() as $class) { + $this->classes[$class->getName()] = $class->getHash(); + } + } + + $this->versions = array(); + if (null !== $project) { + foreach ($project->getVersions() as $version) { + $this->versions[] = (string) $version; + } + } + } + + public function getVersions() + { + return $this->versions; + } + + public function getClasses() + { + return $this->classes; + } + + public function getHash($class) + { + return isset($this->classes[$class]) ? $this->classes[$class] : false; + } + + public function serialize() + { + return serialize(array($this->classes, $this->versions)); + } + + public function unserialize($data) + { + list($this->classes, $this->versions) = unserialize($data); + } +} diff --git a/Sami/Renderer/Renderer.php b/Sami/Renderer/Renderer.php new file mode 100644 index 00000000..1103f5e6 --- /dev/null +++ b/Sami/Renderer/Renderer.php @@ -0,0 +1,210 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Renderer; + +use Symfony\Component\Filesystem\Filesystem; +use Sami\Project; +use Sami\Message; + +class Renderer +{ + protected $twig; + protected $templates; + protected $filesystem; + protected $themes; + protected $theme; + protected $steps; + protected $step; + + public function __construct(\Twig_Environment $twig, ThemeSet $themes) + { + $this->twig = $twig; + $this->themes = $themes; + $this->filesystem = new Filesystem(); + } + + public function isRendered(Project $project) + { + return $this->getDiff($project)->isAlreadyRendered(); + } + + public function render(Project $project, $callback = null) + { + $this->twig->setCache($project->getCacheDir().'/twig'); + + $diff = $this->getDiff($project); + + if ($diff->isEmpty()) { + return $diff; + } + + $this->steps = count($diff->getModifiedClasses()) + + count($diff->getModifiedNamespaces()) + + count($this->getTheme($project)->getTemplates('global')) + + 1; + $this->step = 0; + + $this->theme = $this->getTheme($project); + $dirs = $this->theme->getTemplateDirs(); + // add parent directory to be able to extends the same template as the current one but in the parent theme + foreach ($dirs as $dir) { + $dirs[] = dirname($dir); + } + $this->twig->getLoader()->setPaths(array_unique($dirs)); + + $this->twig->addGlobal('has_namespaces', $project->hasNamespaces()); + $this->twig->addGlobal('page_layout', 'layout/page.twig'); + $this->twig->addGlobal('project', $project); + + $this->renderStaticTemplates($project, $callback); + $this->renderGlobalTemplates($project, $callback); + $this->renderNamespaceTemplates($diff->getModifiedNamespaces(), $project, $callback); + $this->renderClassTemplates($diff->getModifiedClasses(), $project, $callback); + + // cleanup + foreach ($diff->getRemovedClasses() as $class) { + foreach ($this->theme->getTemplates('class') as $target) { + $this->filesystem->remove(sprintf($target, str_replace('\\', '/', $class))); + } + } + + $diff->save(); + + return $diff; + } + + protected function renderStaticTemplates(Project $project, $callback = null) + { + if (null !== $callback) { + call_user_func($callback, Message::RENDER_PROGRESS, array('Static', 'Rendering files', $this->getProgression())); + } + + $dirs = $this->theme->getTemplateDirs(); + foreach ($this->theme->getTemplates('static') as $template => $target) { + foreach (array_reverse($dirs) as $dir) { + if (file_exists($dir.'/'.$template)) { + $this->filesystem->copy($dir.'/'.$template, $project->getBuildDir().'/'.$target); + + continue 2; + } + } + } + } + + protected function renderGlobalTemplates(Project $project, $callback = null) + { + $variables = array( + 'namespaces' => $project->getNamespaces(), + 'interfaces' => $project->getProjectInterfaces(), + 'classes' => $project->getProjectClasses(), + 'items' => $this->getIndex($project), + 'index' => $project->getIndex(), + 'tree' => $project->getTree(), + ); + + foreach ($this->theme->getTemplates('global') as $template => $target) { + if (null !== $callback) { + call_user_func($callback, Message::RENDER_PROGRESS, array('Global', $target, $this->getProgression())); + } + + $this->save($project, $target, $template, $variables); + } + } + + protected function renderNamespaceTemplates(array $namespaces, Project $project, $callback = null) + { + foreach ($namespaces as $namespace) { + if (null !== $callback) { + call_user_func($callback, Message::RENDER_PROGRESS, array('Namespace', $namespace, $this->getProgression())); + } + + $variables = array( + 'namespace' => $namespace, + 'classes' => $project->getNamespaceClasses($namespace), + 'interfaces' => $project->getNamespaceInterfaces($namespace), + 'exceptions' => $project->getNamespaceExceptions($namespace), + ); + foreach ($this->theme->getTemplates('namespace') as $template => $target) { + $this->save($project, sprintf($target, str_replace('\\', '/', $namespace)), $template, $variables); + } + } + } + + protected function renderClassTemplates(array $classes, Project $project, $callback = null) + { + foreach ($classes as $class) { + if (null !== $callback) { + call_user_func($callback, Message::RENDER_PROGRESS, array('Class', $class->getName(), $this->getProgression())); + } + + $variables = array( + 'class' => $class, + 'properties' => $class->getProperties($project->getConfig('include_parent_data')), + 'methods' => $class->getMethods($project->getConfig('include_parent_data')), + 'constants' => $class->getConstants($project->getConfig('include_parent_data')), + ); + foreach ($this->theme->getTemplates('class') as $template => $target) { + $this->save($project, sprintf($target, str_replace('\\', '/', $class->getName())), $template, $variables); + } + } + } + + protected function save(Project $project, $uri, $template, $variables) + { + $this->twig->addGlobal('depth', substr_count($uri, '/')); + + $file = $project->getBuildDir().'/'.$uri; + + if (!is_dir($dir = dirname($file))) { + $this->filesystem->mkdir($dir); + } + + file_put_contents($file, $this->twig->render($template, $variables)); + } + + protected function getIndex(Project $project) + { + $items = array(); + foreach ($project->getProjectClasses() as $class) { + $letter = strtoupper(substr($class->getShortName(), 0, 1)); + $items[$letter][] = array('class', $class); + + foreach ($class->getProperties() as $property) { + $letter = strtoupper(substr($property->getName(), 0, 1)); + $items[$letter][] = array('property', $property); + } + + foreach ($class->getMethods() as $method) { + $letter = strtoupper(substr($method->getName(), 0, 1)); + $items[$letter][] = array('method', $method); + } + } + ksort($items); + + return $items; + } + + protected function getDiff(Project $project) + { + return new Diff($project, $project->getBuildDir().'/renderer.index'); + } + + protected function getTheme(Project $project) + { + return $this->themes->getTheme($project->getConfig('theme')); + } + + protected function getProgression() + { + return floor((++$this->step / $this->steps) * 100); + } +} diff --git a/Sami/Renderer/Theme.php b/Sami/Renderer/Theme.php new file mode 100644 index 00000000..d15722f7 --- /dev/null +++ b/Sami/Renderer/Theme.php @@ -0,0 +1,72 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Renderer; + +class Theme +{ + protected $name; + protected $dir; + protected $parent; + protected $templates; + + public function __construct($name, $dir) + { + $this->name = $name; + $this->dir = $dir; + } + + public function getTemplateDirs() + { + $dirs = array(); + if ($this->parent) { + $dirs = $this->parent->getTemplateDirs(); + } + + array_unshift($dirs, $this->dir); + + return $dirs; + } + + public function setParent(Theme $parent) + { + $this->parent = $parent; + } + + public function getParent() + { + return $this->parent; + } + + public function getName() + { + return $this->name; + } + + public function getTemplates($type) + { + $templates = array(); + if ($this->parent) { + $templates = $this->parent->getTemplates($type); + } + + if (!isset($this->templates[$type])) { + return $templates; + } + + return array_replace($templates, $this->templates[$type]); + } + + public function setTemplates($type, $templates) + { + $this->templates[$type] = $templates; + } +} diff --git a/Sami/Renderer/ThemeSet.php b/Sami/Renderer/ThemeSet.php new file mode 100644 index 00000000..720930ee --- /dev/null +++ b/Sami/Renderer/ThemeSet.php @@ -0,0 +1,67 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Renderer; + +use Symfony\Component\Finder\Finder; +use Symfony\Component\Yaml\Yaml; + +class ThemeSet +{ + protected $themes; + + public function __construct(array $dirs) + { + $this->discover($dirs); + } + + public function getTheme($name) + { + if (!isset($this->themes[$name])) { + throw new \InvalidArgumentException(sprintf('Theme "%s" does not exist.', $name)); + } + + return $this->themes[$name]; + } + + protected function discover(array $dirs) + { + $this->themes = array(); + $parents = array(); + foreach (Finder::create()->name('manifest.yml')->in($dirs) as $manifest) { + $config = Yaml::parse($manifest); + if (!isset($config['name'])) { + throw new \InvalidArgumentException(sprintf('Theme manifest in "%s" must have a "name" entry.', $manifest)); + } + + $this->themes[$config['name']] = $theme = new Theme($config['name'], dirname($manifest)); + + if (isset($config['parent'])) { + $parents[$config['name']] = $config['parent']; + } + + foreach (array('static', 'global', 'namespace', 'class') as $type) { + if (isset($config[$type])) { + $theme->setTemplates($type, $config[$type]); + } + } + } + + // populate parent + foreach ($parents as $name => $parent) { + if (!isset($this->themes[$parent])) { + throw new \LogicException(sprintf('Theme "%s" inherits from an unknomwn "%s" theme.', $name, $parent)); + } + + $this->themes[$name]->setParent($this->themes[$parent]); + } + } +} diff --git a/Sami/Renderer/TwigExtension.php b/Sami/Renderer/TwigExtension.php new file mode 100644 index 00000000..8c1eb4ed --- /dev/null +++ b/Sami/Renderer/TwigExtension.php @@ -0,0 +1,133 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Renderer; + +use Sami\Reflection\ClassReflection; +use Sami\Reflection\MethodReflection; +use Sami\Reflection\PropertyReflection; + +class TwigExtension extends \Twig_Extension +{ + protected $project; + + /** + * Returns a list of filters to add to the existing list. + * + * @return array An array of filters + */ + public function getFilters() + { + return array( + 'desc' => new \Twig_Filter_Method($this, 'parseDesc', array('needs_context' => true)), + 'snippet' => new \Twig_Filter_Method($this, 'getSnippet'), + ); + } + + /** + * Returns a list of functions to add to the existing list. + * + * @return array An array of functions + */ + public function getFunctions() + { + return array( + 'namespace_path' => new \Twig_Function_Method($this, 'pathForNamespace', array('needs_context' => true)), + 'class_path' => new \Twig_Function_Method($this, 'pathForClass', array('needs_context' => true)), + 'method_path' => new \Twig_Function_Method($this, 'pathForMethod', array('needs_context' => true)), + 'property_path' => new \Twig_Function_Method($this, 'pathForProperty', array('needs_context' => true)), + 'path' => new \Twig_Function_Method($this, 'pathForStaticFile', array('needs_context' => true)), + 'abbr_class' => new \Twig_Function_Method($this, 'abbrClass', array('is_safe' => array('html'))), + ); + } + + public function pathForClass(array $context, ClassReflection $class) + { + return $this->relativeUri($context['depth']).str_replace('\\', '/', $class).'.html'; + } + + public function pathForNamespace(array $context, $namespace) + { + return $this->relativeUri($context['depth']).str_replace('\\', '/', $namespace).'.html'; + } + + public function pathForMethod(array $context, MethodReflection $method) + { + return $this->relativeUri($context['depth']).str_replace('\\', '/', $method->getClass()->getName()).'.html#method_'.$method->getName(); + } + + public function pathForProperty(array $context, PropertyReflection $property) + { + return $this->relativeUri($context['depth']).str_replace('\\', '/', $property->getClass()->getName()).'.html#property_'.$property->getName(); + } + + public function pathForStaticFile(array $context, $file) + { + return $this->relativeUri($context['depth']).$file; + } + + public function abbrClass($class) + { + if ($class instanceof ClassReflection) { + $short = $class->getShortName(); + $class = $class->getName(); + } else { + $parts = explode('\\', $class); + + if (1 == count($parts)) { + return $class; + } + + $short = array_pop($parts); + } + + return sprintf("%s", $class, $short); + } + + public function parseDesc(array $context, $desc, ClassReflection $class) + { + // FIXME: the @see argument is more complex than just a class (Class::Method, local method directly, ...) + $that = $this; + $desc = preg_replace_callback('/@see ([^ ]+)/', function ($match) use ($that, $context, $class) { + return 'see '.$match[1]; + }, $desc); + + return $desc; + } + + public function getSnippet($string) + { + if (preg_match('/^(.{50,}?)\s.*/m', $string, $matches)) { + $string = $matches[1]; + } + + return str_replace(array("\n", "\r"), '', strip_tags($string)); + } + + protected function relativeUri($value) + { + if (!$value) { + return ''; + } + + return rtrim(str_repeat('../', $value), '/').'/'; + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return 'sami'; + } +} diff --git a/Sami/Resources/themes/default/classes.twig b/Sami/Resources/themes/default/classes.twig new file mode 100644 index 00000000..fb0f5593 --- /dev/null +++ b/Sami/Resources/themes/default/classes.twig @@ -0,0 +1,33 @@ +{% extends "layout/base.twig" %} + +{% from "macros.twig" import class_link %} + +{% block title %}All Classes | {{ parent() }}{% endblock %} + +{% block body_class 'frame' %} + +{% block header %} +
+

{{ project.config('title') }}

+ + +
+{% endblock %} + +{% block content %} +

Classes

+
    + {% for class in classes %} +
  • + {% if class.isinterface %}{% endif %} + {{ class_link(class, {'target': 'main'}) }} + {% if class.isinterface %}{% endif %} +
  • + {% endfor %} +
+{% endblock %} diff --git a/Sami/Resources/themes/default/index.twig b/Sami/Resources/themes/default/index.twig new file mode 100644 index 00000000..2eca8240 --- /dev/null +++ b/Sami/Resources/themes/default/index.twig @@ -0,0 +1,3 @@ +{% extends "layout/frame.twig" %} + +{% block frame_src 'classes-frame.html' %} diff --git a/Sami/Resources/themes/default/layout/base.twig b/Sami/Resources/themes/default/layout/base.twig new file mode 100644 index 00000000..bc370e7a --- /dev/null +++ b/Sami/Resources/themes/default/layout/base.twig @@ -0,0 +1,21 @@ + + + + + + {% block title project.config('title') %} + {% block head %} + + {% endblock %} + {% if project.config('favicon') %} + + {% endif %} + {% if project.config('base_url') %} + {%- for version in project.versions -%} + + {% endfor -%} + {% endif %} + + {% block html %} + {% endblock %} + diff --git a/Sami/Resources/themes/default/layout/frame.twig b/Sami/Resources/themes/default/layout/frame.twig new file mode 100644 index 00000000..8b4754f2 --- /dev/null +++ b/Sami/Resources/themes/default/layout/frame.twig @@ -0,0 +1,13 @@ +{% extends "layout/base.twig" %} + +{% block html %} + + + + + <body> + Your browser does not support frames. Go to the <a href="namespaces.html">non-frame version</a>. + </body> + + +{% endblock %} diff --git a/Sami/Resources/themes/default/layout/layout.twig b/Sami/Resources/themes/default/layout/layout.twig new file mode 100644 index 00000000..2544f741 --- /dev/null +++ b/Sami/Resources/themes/default/layout/layout.twig @@ -0,0 +1,11 @@ +{% extends "layout/base.twig" %} + +{% block html %} + + {% block header '' %} +
+ {% block content '' %} +
+ {% block footer '' %} + +{% endblock %} diff --git a/Sami/Resources/themes/default/layout/page.twig b/Sami/Resources/themes/default/layout/page.twig new file mode 100644 index 00000000..bdbaddd6 --- /dev/null +++ b/Sami/Resources/themes/default/layout/page.twig @@ -0,0 +1,30 @@ +{% extends "layout/layout.twig" %} + +{% block frame %} +
  • Frames
  • +
  • No Frames
  • +{% endblock %} + +{% block header %} +
    + + +
    {{ project.config('title') }}
    + + {% block content_header '' %} +
    +{% endblock %} + +{% block footer %} + +{% endblock %} diff --git a/Sami/Resources/themes/default/macros.twig b/Sami/Resources/themes/default/macros.twig new file mode 100644 index 00000000..70a3178a --- /dev/null +++ b/Sami/Resources/themes/default/macros.twig @@ -0,0 +1,59 @@ +{% macro attributes(attributes) %} + {%- for key, value in attributes %} {{ key }}="{{ value }}"{% endfor -%} +{% endmacro %} + +{% macro namespace_link(namespace, attributes) -%} + {{ namespace }} +{%- endmacro %} + +{% macro class_link(class, attributes, absolute) -%} + {%- if class.projectclass -%} + + {%- elseif class.phpclass -%} + + {%- endif %} + {{- _self.abbr_class(class, absolute|default(false)) }} + {%- if class.projectclass or class.phpclass %}{% endif %} +{%- endmacro %} + +{% macro method_link(method, attributes, absolute, classonly) -%} + + {{- _self.abbr_class(method.class) }}{% if not classonly|default(false) %}::{{ method.name }}{% endif -%} + +{%- endmacro %} + +{% macro property_link(property, attributes, absolute, classonly) -%} + + {{- _self.abbr_class(property.class) }}{% if not classonly|default(true) %}#{{ property.name }}{% endif -%} + +{%- endmacro %} + +{% macro hint_link(hints, attributes) -%} + {% if hints %} + {%- for hint in hints %} + {%- if hint.class %} + {{- _self.class_link(hint.name) }} + {%- elseif hint.name %} + {{- abbr_class(hint.name) }} + {%- endif %} + {%- if hint.array %}[]{% endif %} + {%- if not loop.last %}|{% endif %} + {%- endfor %} + {%- endif %} +{%- endmacro %} + +{% macro abbr_class(class, absolute) -%} + {{ absolute|default(false) ? class : class.shortname }} +{%- endmacro %} + +{% macro method_parameters_signature(method) -%} + {%- from "macros.twig" import hint_link -%} + ( + {%- for parameter in method.parameters %} + {%- if parameter.hashint %}{{ hint_link(parameter.hint) }} {% endif -%} + ${{ parameter.name }} + {%- if parameter.default %} = {{ parameter.default }}{% endif %} + {%- if not loop.last %}, {% endif %} + {%- endfor -%} + ) +{%- endmacro %} diff --git a/Sami/Resources/themes/default/manifest.yml b/Sami/Resources/themes/default/manifest.yml new file mode 100644 index 00000000..05bf34e5 --- /dev/null +++ b/Sami/Resources/themes/default/manifest.yml @@ -0,0 +1,21 @@ +name: default + +static: + 'stylesheet.css': 'stylesheet.css' + +global: + 'index.twig': 'index.html' + 'namespaces.twig': 'namespaces-frame.html' + 'classes.twig': 'classes-frame.html' + 'pages/opensearch.twig': 'opensearch.xml' + 'pages/index.twig': 'doc-index.html' + 'pages/namespaces.twig': 'namespaces.html' + 'pages/interfaces.twig': 'interfaces.html' + 'pages/classes.twig': 'classes.html' + +namespace: + 'namespace.twig': '%s/namespace-frame.html' + 'pages/namespace.twig': '%s.html' + +class: + 'pages/class.twig': '%s.html' diff --git a/Sami/Resources/themes/default/namespace.twig b/Sami/Resources/themes/default/namespace.twig new file mode 100644 index 00000000..db81dcc2 --- /dev/null +++ b/Sami/Resources/themes/default/namespace.twig @@ -0,0 +1,48 @@ +{% extends "layout/base.twig" %} + +{% from "macros.twig" import class_link, namespace_link %} + +{% block title %}{{ namespace }} | {{ parent() }}{% endblock %} + +{% block body_class 'frame' %} + +{% block header %} +
    +

    {{ project.config('title') }}

    + + +
    +{% endblock %} + +{% block content %} +

    {{ namespace_link(namespace, {'target': 'main'}) }}

    + + {% if classes %} +
      + {% for class in classes %} +
    • {{ class_link(class, {'target': 'main'}) }}
    • + {% endfor %} +
    + {% endif %} + + {% if interfaces %} +

    Interfaces

    +
      + {% for class in interfaces %} +
    • {{ class_link(class, {'target': 'main'}) }}
    • + {% endfor %} +
    + {% endif %} + + {% if exceptions %} +

    Exceptions

    +
      + {% for class in exceptions %} +
    • {{ class_link(class, {'target': 'main'}) }}
    • + {% endfor %} +
    + {% endif %} +{% endblock %} diff --git a/Sami/Resources/themes/default/namespaces.twig b/Sami/Resources/themes/default/namespaces.twig new file mode 100644 index 00000000..e6f40c7c --- /dev/null +++ b/Sami/Resources/themes/default/namespaces.twig @@ -0,0 +1,26 @@ +{% extends "layout/base.twig" %} + +{% block title %}Namespaces | {{ parent() }}{% endblock %} + +{% block body_class 'frame' %} + +{% block header %} +
    +

    {{ project.config('title') }}

    + + +
    +{% endblock %} + +{% block content %} +

    Namespaces

    + + +{% endblock %} diff --git a/Sami/Resources/themes/default/pages/class.twig b/Sami/Resources/themes/default/pages/class.twig new file mode 100644 index 00000000..330d51ff --- /dev/null +++ b/Sami/Resources/themes/default/pages/class.twig @@ -0,0 +1,213 @@ +{% extends page_layout %} + +{% from "macros.twig" import namespace_link, class_link, method_link, hint_link %} + +{% block title %}{{ class.name }} | {{ parent() }}{% endblock %} + +{% block body_class 'class' %} + +{% block content_header %} +
    {% if class.interface %}Interface{% else %}Class{% endif %}
    +

    {{ namespace_link(class.namespace) }}{% if class.namespace %}\{% endif %}{{ class.shortname }}

    +{% endblock %} + +{% block content %} +

    {{ block('class_signature') }}

    + + {% if class.shortdesc or class.longdesc %} +
    +

    {{ class.shortdesc|desc(class)|nl2br }}

    +

    {{ class.longdesc|desc(class)|nl2br }}

    +
    + {% endif %} + + {% if constants %} +

    Constants

    + + {{ block('constants') }} + {% endif %} + + {% if properties %} +

    Properties

    + + {{ block('properties') }} + {% endif %} + + {% if methods %} +

    Methods

    + + {{ block('methods') }} + +

    Details

    + + {{ block('methods_details') }} + {% endif %} +{% endblock %} + +{% block class_signature -%} + {% if not class.interface and class.abstract %}abstract {% endif %} + {%- if class.interface %}interface{% else %}class{% endif %} + {{ class.shortname }} + {%- if class.parent %} + extends {{ class_link(class.parent) }} + {%- endif %} + {%- if class.interfaces|length > 0 %} + implements + {% for interface in class.interfaces %} + {{- class_link(interface) }} + {%- if not loop.last %}, {% endif %} + {%- endfor %} + {%- endif %} +{% endblock %} + +{% block method_signature -%} + {% if method.final %}final{% endif %} + {% if method.abstract %}abstract{% endif %} + {% if method.static %}static{% endif %} + {% if method.public %}public{% endif %} + {% if method.protected %}protected{% endif %} + {% if method.private %}private{% endif %} + {{ hint_link(method.hint) }} + {{ method.name }}{{ block('method_parameters_signature') }} +{%- endblock %} + +{% block method_parameters_signature -%} + {%- from "macros.twig" import method_parameters_signature -%} + {{ method_parameters_signature(method) }} +{%- endblock %} + +{% block parameters %} + + {% for parameter in method.parameters %} + + + + + + {% endfor %} +
    {% if parameter.hint %}{{ hint_link(parameter.hint) }}{% endif %}${{ parameter.name }}{{ parameter.shortdesc|desc(class)|nl2br }}
    +{% endblock %} + +{% block return %} + + + + + +
    {{ hint_link(method.hint) }}{{ method.hintDesc|desc(class)|nl2br }}
    +{% endblock %} + +{% block exceptions %} + + {% for exception in method.exceptions %} + + + + + {% endfor %} +
    {{ class_link(exception[0]) }}{{ exception[1]|desc(class)|nl2br }}
    +{% endblock %} + +{% block see %} + + {% for tag in method.tags('see') %} + + + + + {% endfor %} +
    {{ tag[0] }}{{ tag[1:]|join(' ') }}
    +{% endblock %} + +{% block constants %} + + {% for constant in constants %} + + + + + {% endfor %} +
    {{ constant.name }} +

    {{ constant.shortdesc|desc(class)|nl2br }}

    +

    {{ constant.longdesc|desc(class)|nl2br }}

    +
    +{% endblock %} + +{% block properties %} + + {% for property in properties %} + + + + + + {% endfor %} +
    + {% if property.static %}static{% endif %} + {% if property.protected %}protected{% endif %} + {{ hint_link(property.hint) }} + ${{ property.name }}{{ property.shortdesc|desc(class)|nl2br }}
    +{% endblock %} + +{% block methods %} + + {% for method in methods %} + + + + + + {% endfor %} +
    + {% if method.static %}static {% endif %}{{ hint_link(method.hint) }} + + {{ method.name }}{{ block('method_parameters_signature') }} +

    {{ method.shortdesc|desc(class)|nl2br }}

    +
    + {%- if method.class != class -%} + from {{ method_link(method, {}, false, true) }} + {%- endif -%} +
    +{% endblock %} + +{% block methods_details %} + {% for method in methods %} + {{ block('method') }} + {% endfor %} +{% endblock %} + +{% block method %} +

    +
    {% if method.class != class %}in {{ method_link(method, {}, false, true) }} {% endif %}at line {{ method.line }}
    + {{ block('method_signature') }} +

    +
    +

    {{ method.shortdesc|desc(class)|nl2br }}

    +

    {{ method.longdesc|desc(class)|nl2br }}

    +
    + {% if method.parameters %} +

    Parameters

    + + {{ block('parameters') }} + {% endif %} + + {% if method.hintDesc or method.hint %} +

    Return Value

    + + {{ block('return') }} + {% endif %} + + {% if method.exceptions %} +

    Exceptions

    + + {{ block('exceptions') }} + {% endif %} + + {% if method.tags('see') %} +

    See also

    + + {{ block('see') }} + {% endif %} +
    +
    +{% endblock %} diff --git a/Sami/Resources/themes/default/pages/classes.twig b/Sami/Resources/themes/default/pages/classes.twig new file mode 100644 index 00000000..e8dc8a0c --- /dev/null +++ b/Sami/Resources/themes/default/pages/classes.twig @@ -0,0 +1,28 @@ +{% extends page_layout %} + +{% from "macros.twig" import class_link %} + +{% block title %}Classes | {{ parent() }}{% endblock %} + +{% block body_class 'overview' %} + +{% block content_header %} +

    Classes

    +{% endblock %} + +{% block content %} + + {% for class in classes %} + + + + + {% endfor %} +
    + {% if class.interface %}{% endif %} + {{ class_link(class, {'target': 'main'}, true) }} + {% if class.interface %}{% endif %} + + {{ class.shortdesc|desc(class)|nl2br }} +
    +{% endblock %} diff --git a/Sami/Resources/themes/default/pages/index.twig b/Sami/Resources/themes/default/pages/index.twig new file mode 100644 index 00000000..5eda21f9 --- /dev/null +++ b/Sami/Resources/themes/default/pages/index.twig @@ -0,0 +1,41 @@ +{% extends page_layout %} + +{% from "macros.twig" import class_link, namespace_link, method_link, property_link %} + +{% block title %}Index | {{ parent() }}{% endblock %} + +{% block body_class 'overview' %} + +{% block content_header %} +
    Index
    + + {% for letter in 'A'..'Z' %} + {% if items[letter] is defined and items[letter]|length > 1 %} + {{ letter }} + {% else %} + {{ letter }} + {% endif %} + {% endfor %} +{% endblock %} + +{% block content %} + {% for letter, elements in items -%} +

    {{ letter }}

    +
    + {%- for element in elements %} + {%- set type = element[0] %} + {%- set value = element[1] %} + {%- if 'class' == type -%} +
    {{ class_link(value) }}{% if has_namespaces %} — Class in namespace {{ namespace_link(value.namespace) }}{% endif %}
    +
    {{ value.shortdesc|desc(value)|nl2br }}
    + {%- elseif 'method' == type -%} +
    {{ method_link(value) }}() — Method in class {{ class_link(value.class) }}
    +
    {{ value.shortdesc|desc(value.class)|nl2br }}
    + {%- elseif 'property' == type -%} +
    ${{ property_link(value) }} — Property in class {{ class_link(value.class) }}
    +
    {{ value.shortdesc|desc(value.class)|nl2br }}
    + {%- endif %} + {%- endfor %} +
    + {%- endfor %} +{% endblock %} diff --git a/Sami/Resources/themes/default/pages/interfaces.twig b/Sami/Resources/themes/default/pages/interfaces.twig new file mode 100644 index 00000000..d20fda60 --- /dev/null +++ b/Sami/Resources/themes/default/pages/interfaces.twig @@ -0,0 +1,24 @@ +{% extends page_layout %} + +{% from "macros.twig" import class_link %} + +{% block title %}Interfaces | {{ parent() }}{% endblock %} + +{% block body_class 'overview' %} + +{% block content_header %} +

    Interfaces

    +{% endblock %} + +{% block content %} + + {% for interface in interfaces %} + + + + + {% endfor %} +
    {{ class_link(interface, {'target': 'main'}, true) }} + {{ interface.shortdesc|desc(interface)|nl2br }} +
    +{% endblock %} diff --git a/Sami/Resources/themes/default/pages/namespace.twig b/Sami/Resources/themes/default/pages/namespace.twig new file mode 100644 index 00000000..4a44ce1b --- /dev/null +++ b/Sami/Resources/themes/default/pages/namespace.twig @@ -0,0 +1,49 @@ +{% extends page_layout %} + +{% from "macros.twig" import class_link %} + +{% block title %}{{ namespace }} | {{ parent() }}{% endblock %} + +{% block body_class 'overview' %} + +{% block content_header %} +
    Namespace
    +

    {{ namespace }}

    +{% endblock %} + +{% block content %} + {% if classes %} + + {% for class in classes %} + + + + + {% endfor %} +
    {{ class_link(class) }}{{ class.shortdesc|desc(class)|nl2br }}
    + {% endif %} + + {% if interfaces %} +

    Interfaces

    + + {% for interface in interfaces %} + + + + + {% endfor %} +
    {{ class_link(interface) }}{{ interface.shortdesc|desc(interface)|nl2br }}
    + {% endif %} + + {% if exceptions %} +

    Exceptions

    + + {% for exception in exceptions %} + + + + + {% endfor %} +
    {{ class_link(exception) }}{{ exception.shortdesc|desc(exception)|nl2br }}
    + {% endif %} +{% endblock %} diff --git a/Sami/Resources/themes/default/pages/namespaces.twig b/Sami/Resources/themes/default/pages/namespaces.twig new file mode 100644 index 00000000..54147962 --- /dev/null +++ b/Sami/Resources/themes/default/pages/namespaces.twig @@ -0,0 +1,21 @@ +{% extends page_layout %} + +{% from "macros.twig" import namespace_link %} + +{% block title %}Namespaces | {{ parent() }}{% endblock %} + +{% block body_class 'overview' %} + +{% block content_header %} +

    Namespaces

    +{% endblock %} + +{% block content %} + + {% for namespace in namespaces %} + + + + {% endfor %} +
    {{ namespace_link(namespace) }}
    +{% endblock %} diff --git a/Sami/Resources/themes/default/pages/opensearch.twig b/Sami/Resources/themes/default/pages/opensearch.twig new file mode 100644 index 00000000..57ed32a0 --- /dev/null +++ b/Sami/Resources/themes/default/pages/opensearch.twig @@ -0,0 +1,14 @@ +{% if project.config('base_url') -%} + + + {{ project.config('title') }} ({{ project.version }}) + Searches {{ project.config('title') }} ({{ project.version }}) + {{ project.config('title') }} + {% if project.config('favicon') -%} + {{ project.config('favicon') }} + {% endif %} + + UTF-8 + false + +{% endif %} diff --git a/Sami/Resources/themes/default/stylesheet.css b/Sami/Resources/themes/default/stylesheet.css new file mode 100644 index 00000000..72c3dd82 --- /dev/null +++ b/Sami/Resources/themes/default/stylesheet.css @@ -0,0 +1,212 @@ +/* +Copyright (c) 2010, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.8.1 +*/ +html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{*font-size:100%;}body{font:13px/1.231 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;}select,input,button,textarea,button{font:99% arial,helvetica,clean,sans-serif;}table{font-size:inherit;font:100%;}pre,code,kbd,samp,tt{font-family:monospace;*font-size:108%;line-height:100%;}body{text-align:center;}#doc,#doc2,#doc3,#doc4,.yui-t1,.yui-t2,.yui-t3,.yui-t4,.yui-t5,.yui-t6,.yui-t7{margin:auto;text-align:left;width:57.69em;*width:56.25em;}#doc2{width:73.076em;*width:71.25em;}#doc3{margin:auto 10px;width:auto;}#doc4{width:74.923em;*width:73.05em;}.yui-b{position:relative;}.yui-b{_position:static;}#yui-main .yui-b{position:static;}#yui-main,.yui-g .yui-u .yui-g{width:100%;}.yui-t1 #yui-main,.yui-t2 #yui-main,.yui-t3 #yui-main{float:right;margin-left:-25em;}.yui-t4 #yui-main,.yui-t5 #yui-main,.yui-t6 #yui-main{float:left;margin-right:-25em;}.yui-t1 .yui-b{float:left;width:12.30769em;*width:12.00em;}.yui-t1 #yui-main .yui-b{margin-left:13.30769em;*margin-left:13.05em;}.yui-t2 .yui-b{float:left;width:13.8461em;*width:13.50em;}.yui-t2 #yui-main .yui-b{margin-left:14.8461em;*margin-left:14.55em;}.yui-t3 .yui-b{float:left;width:23.0769em;*width:22.50em;}.yui-t3 #yui-main .yui-b{margin-left:24.0769em;*margin-left:23.62em;}.yui-t4 .yui-b{float:right;width:13.8456em;*width:13.50em;}.yui-t4 #yui-main .yui-b{margin-right:14.8456em;*margin-right:14.55em;}.yui-t5 .yui-b{float:right;width:18.4615em;*width:18.00em;}.yui-t5 #yui-main .yui-b{margin-right:19.4615em;*margin-right:19.125em;}.yui-t6 .yui-b{float:right;width:23.0769em;*width:22.50em;}.yui-t6 #yui-main .yui-b{margin-right:24.0769em;*margin-right:23.62em;}.yui-t7 #yui-main .yui-b{display:block;margin:0 0 1em 0;}#yui-main .yui-b{float:none;width:auto;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf,.yui-gc .yui-u,.yui-gd .yui-g,.yui-g .yui-gc .yui-u,.yui-ge .yui-u,.yui-ge .yui-g,.yui-gf .yui-g,.yui-gf .yui-u{float:right;}.yui-g div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first,.yui-ge div.first,.yui-gf div.first,.yui-g .yui-gc div.first,.yui-g .yui-ge div.first,.yui-gc div.first div.first{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf{width:49.1%;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{width:32%;margin-left:1.99%;}.yui-gb .yui-u{*margin-left:1.9%;*width:31.9%;}.yui-gc div.first,.yui-gd .yui-u{width:66%;}.yui-gd div.first{width:32%;}.yui-ge div.first,.yui-gf .yui-u{width:74.2%;}.yui-ge .yui-u,.yui-gf div.first{width:24%;}.yui-g .yui-gb div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first{margin-left:0;}.yui-g .yui-g .yui-u,.yui-gb .yui-g .yui-u,.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u,.yui-ge .yui-g .yui-u,.yui-gf .yui-g .yui-u{width:49%;*width:48.1%;*margin-left:0;}.yui-g .yui-g .yui-u{width:48.1%;}.yui-g .yui-gb div.first,.yui-gb .yui-gb div.first{*margin-right:0;*width:32%;_width:31.7%;}.yui-g .yui-gc div.first,.yui-gd .yui-g{width:66%;}.yui-gb .yui-g div.first{*margin-right:4%;_margin-right:1.3%;}.yui-gb .yui-gc div.first,.yui-gb .yui-gd div.first{*margin-right:0;}.yui-gb .yui-gb .yui-u,.yui-gb .yui-gc .yui-u{*margin-left:1.8%;_margin-left:4%;}.yui-g .yui-gb .yui-u{_margin-left:1.0%;}.yui-gb .yui-gd .yui-u{*width:66%;_width:61.2%;}.yui-gb .yui-gd div.first{*width:31%;_width:29.5%;}.yui-g .yui-gc .yui-u,.yui-gb .yui-gc .yui-u{width:32%;_float:right;margin-right:0;_margin-left:0;}.yui-gb .yui-gc div.first{width:66%;*float:left;*margin-left:0;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf .yui-u{margin:0;}.yui-gb .yui-gb .yui-u{_margin-left:.7%;}.yui-gb .yui-g div.first,.yui-gb .yui-gb div.first{*margin-left:0;}.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u{*width:48.1%;*margin-left:0;}.yui-gb .yui-gd div.first{width:32%;}.yui-g .yui-gd div.first{_width:29.9%;}.yui-ge .yui-g{width:24%;}.yui-gf .yui-g{width:74.2%;}.yui-gb .yui-ge div.yui-u,.yui-gb .yui-gf div.yui-u{float:right;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf div.first{float:left;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf div.first{*width:24%;_width:20%;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf .yui-u{*width:73.5%;_width:65.5%;}.yui-ge div.first .yui-gd .yui-u{width:65%;}.yui-ge div.first .yui-gd div.first{width:32%;}#hd:after,#bd:after,#ft:after,.yui-g:after,.yui-gb:after,.yui-gc:after,.yui-gd:after,.yui-ge:after,.yui-gf:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#hd,#bd,#ft,.yui-g,.yui-gb,.yui-gc,.yui-gd,.yui-ge,.yui-gf{zoom:1;} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +a { + text-decoration: none; +} + +a:hover { + text-decoration:underline; +} + +body { + text-align: left; + margin: 0; + padding: 0; + background-color: #fff; + color: #000; +} + +td.last { + width: 100%; +} + +table, th, td { + border-bottom: 1px solid #ccc; + vertical-align: top; +} + +table { + margin-bottom: 20px; +} + +td { + padding: 7px 4px; +} + +td p { + padding: 0; + margin: 0; +} + +p, dl, ul, ol, h1, h2, h3, h4, h5, h6 { + margin: 5px 0; +} + +dd { + margin-left: 20px; + margin-bottom: 7px; +} + +ul, ol { + margin-left: 30px; +} + +#title { + font-size: 140%; +} + +h1 { + font-size: 130%; +} + +h2 { + font-size: 120%; + font-weight: bold; + margin: 15px 0; + background-color: #EDF3FE; + padding: 5px; +} + +h3 { + font-size: 110%; +} + +h4 { + font-weight: bold; +} + +/* page header */ + +div.header { + background-color: #EDF3FE; + border-bottom: 1px solid #ccc; + padding: 20px 30px 10px 30px; +} + +#frame div.header { + background-color: #fff; + border: 0; + padding: 10px 15px 5px 15px; +} + +/* menu */ + +div.header ul { + list-style: none; + margin: 0; + float: right; + font-size: 80%; +} + +div.header ul li { + display: inline; + font-size: 100%; + font-weight: bold; + padding: 0 4px; +} + +div.header ul li a { + color: #000; + text-transform: uppercase; +} + +#frame div.header ul { + float: none; +} + +#frame div.header ul li { + padding: 0; + padding-right: 7px; +} + +/* footer */ + +#footer { + margin: 30px; + text-align: right; + font-size: 80%; +} + +/* frames */ + +#frame .content { + margin: 10px; +} + +#frame h1 { + font-size: 120%; +} + +#frame h2 { + font-size: 100%; +} + +#frame ul { + list-style: none; + margin-left: 0; +} + +/* class */ + +div.type { + color: #aaa; + margin: 10px 0 0; +} + +div.location { + font-size: 80%; + float: right; + font-style: italic; +} + +#class h1 { + margin: 0 0 10px; +} + +#class h3 { + font-size: 100%; + margin: 15px 0; +} + +#class h3 strong { + font-size: 120%; +} + +div.details { + padding: 0 0 10px 20px; + border-bottom: 1px solid #ccc; +} + +div.tags { + clear: both; + padding-top: 15px; + font-size: 90%; +} + +td.type { + text-align: right; +} + +#class .content, #overview .content { + padding: 10px 30px; +} + +.description { + margin: 10px 0; + padding: 10px; + background-color: #efefef; +} diff --git a/Sami/Resources/themes/enhanced/README.md b/Sami/Resources/themes/enhanced/README.md new file mode 100644 index 00000000..2204ee50 --- /dev/null +++ b/Sami/Resources/themes/enhanced/README.md @@ -0,0 +1,2 @@ +The JS part of the enhanced theme is based on work from Володя Колесников +(https://github.com/voloko/sdoc -- released under the MIT license) diff --git a/Sami/Resources/themes/enhanced/css/main.css b/Sami/Resources/themes/enhanced/css/main.css new file mode 100644 index 00000000..17608f60 --- /dev/null +++ b/Sami/Resources/themes/enhanced/css/main.css @@ -0,0 +1,214 @@ +/* +Copyright (c) 2009 Vladimir Kolesnikov + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +a { + color: #00F; + text-decoration: none; +} + +a:hover { + color: #77F; + text-decoration: underline; +} + +body, td, p { + font-family: "Bitstream Vera Sans", Verdana, Arial, Helvetica, sans-serif; + background: #FFF; + color: #000; + margin: 0px; + font-size: small; +} + +p { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +#content { + margin: 2em; + margin-left: 3.5em; + margin-right: 3.5em; +} + +#description p { + margin-bottom: 0.5em; +} + +.sectiontitle { + margin-top: 1em; + margin-bottom: 1em; + padding: 0.5em; + padding-left: 2em; + background: #005; + color: #FFF; + font-weight: bold; +} + +.attr-rw { + padding-left: 1em; + padding-right: 1em; + text-align: center; + color: #055; +} + +.attr-name { + font-weight: bold; +} + +.attr-desc { +} + +.attr-desc p { + margin-top: 0; +} + +.attr-value { + font-family: monospace; +} + +.file-title-prefix { + font-size: large; +} + +.file-title { + font-size: large; + font-weight: bold; + background: #005; + color: #FFF; +} + +.banner { + background: #005; + color: #FFF; + border: 1px solid black; + padding: 1em; +} + +.banner td { + background: transparent; + color: #FFF; +} + +h1 a, h2 a, .sectiontitle a, .banner a { + color: #FF0; +} + +h1 a:hover, h2 a:hover, .sectiontitle a:hover, .banner a:hover { + color: #FF7; +} + +.dyn-source { + display: none; + background: #fffde8; + color: #000; + border: #ffe0bb dotted 1px; + margin: 0.5em 2em 0.5em 2em; + padding: 0.5em; +} + +.dyn-source .cmt { + color: #00F; + font-style: italic; +} + +.dyn-source .kw { + color: #070; + font-weight: bold; +} + +.method { + margin-left: 1em; + margin-right: 1em; + margin-bottom: 1em; +} + +.description pre { + padding: 0.5em; + border: #ffe0bb dotted 1px; + background: #fffde8; +} + +.method .title { + font-family: monospace; + font-size: large; + border-bottom: 1px dashed black; + margin-bottom: 0.3em; + padding-bottom: 0.1em; +} + +.method .description, .method .sourcecode { + margin-left: 1em; +} + +.description p, .sourcecode p { + margin-bottom: 0.5em; +} + +.method .sourcecode p.source-link { + text-indent: 0em; + margin-top: 0.5em; +} + +.method .aka { + margin-top: 0.3em; + margin-left: 1em; + font-style: italic; + text-indent: 2em; +} + +h1 { + padding: 1em; + margin-left: -1.5em; + font-size: x-large; + font-weight: bold; + color: #FFF; + background: #007; +} + +h2 { + padding: 0.5em 1em 0.5em 1em; + margin-left: -1.5em; + font-size: large; + font-weight: bold; + color: #FFF; + background: #009; +} + +h3, h4, h5, h6 { + color: #220088; + border-bottom: #5522bb solid 1px; +} + +.sourcecode > pre { + padding: 0.5em; + border: 1px dotted black; + background: #FFE; +} + +dt { + font-weight: bold +} + +dd { + margin-bottom: 0.7em; +} \ No newline at end of file diff --git a/Sami/Resources/themes/enhanced/css/panel.css b/Sami/Resources/themes/enhanced/css/panel.css new file mode 100644 index 00000000..b7565b2f --- /dev/null +++ b/Sami/Resources/themes/enhanced/css/panel.css @@ -0,0 +1,427 @@ +/* +Copyright (c) 2009 Vladimir Kolesnikov + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/* Panel (begin) */ + .panel + { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: #FFF; + z-index: 2; + font-family: "Helvetica Neue", "Arial", sans-serif; + //zoom: 1; + } + + .panel_tree .results, + .panel_results .tree + { + display: none; + } + + /* Header with search box (begin) */ + .panel .header + { + width: 100%; + height: 59px; + border-bottom: 1px solid #666; + position: relative; + left: 0; top: 0; + background: #e8e8e8; + } + + .panel .header div.nav + { + padding-top: 7px; + margin: 0 7px; + } + + .panel .header table + { + height: 29px; + width: 100%; + } + + .panel .header table td + { + vertical-align: middle; + text-align: middle; + } + + .panel .header form + { + float: right; + text-align: bottom; + } + + .panel .header h1 + { + float: left; + font-size: 14px; + font-weight: normal; + color: #777; + text-align: bottom; + } + + .panel .header input + { + width: 100%; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + display: inline-block; + -webkit-appearance: searchfield; + height: 22px; + //height: auto; + } + + /* Header with search box (end) */ + + + /* Results (begin) */ + .panel .result + { + position: absolute; + top: 60px; + bottom: 0; + left: 0; + width: 100%; + //height: expression((this.parentNode.offsetHeight - 31)); + overflow-y: scroll; + overflow-x: hidden; + -overflow-y: hidden; + background: #EDF3FE url(../i/results_bg.png); + z-index: 2; + //zoom:1; + } + + .panel .result ul + { + font-size: 0.8em; + width: 100%; + background: #EDF3FE url(../i/results_bg.png); + //zoom:1; + } + + .panel .result ul li + { + height: 46px; + -height: 50px; + //display: inline; + //width: 100%; + //zoom: 1; + overflow: hidden; + padding: 4px 10px 0 10px; + cursor: pointer; + } + + .panel .result ul li h1 + { + font-size: 13px; + font-weight: normal; + color: #333; + margin-bottom: 2px; + white-space: nowrap; + } + + .panel .result ul li p + { + font-size: 11px; + color: #333; + margin-bottom: 2px; + white-space: nowrap; + } + + .panel .result ul li h1 i, + .panel .result ul li p.snippet + { + color: #999; + } + + .panel .result ul li b + { + color: #000; + } + + .panel .result ul li.current + { + background: #3875D7; + } + + .panel .result ul li.current h1, + .panel .result ul li.current p + { + color: #DDD; + } + + .panel .result ul li.current h1 i, + .panel .result ul li.current p.snippet + { + color: #AAA; + } + + .panel .result ul li.current b + { + color: #FFF; + } + + + .panel .result ul li:hover, + .panel .result ul li.selected + { + background: #d0d0d0; + } + + .panel .result ul li.current:hover + { + background: #2965C0; + } + + .panel .result ul li .badge + { + margin-right: 0.4em; + margin-left: -0.2em; + padding: 0 0.2em; + color: #000; + } + + .panel .result ul li .badge_1 + { + background: #ACDBF4; + } + + .panel .result ul li.current .badge_1 + { + background: #97BFD7; + } + + .panel .result ul li .badge_2 + { + background: #ACF3C3; + } + + .panel .result ul li.current .badge_2 + { + background: #98D7AC; + } + + .panel .result ul li .badge_3 + { + background: #E0F3AC; + } + + .panel .result ul li.current .badge_3 + { + background: #C4D798; + } + + .panel .result ul li .badge_4 + { + background: #D7CA98; + } + + .panel .result ul li.current .badge_4 + { + background: #A6B0AC; + } + + .panel .result ul li .badge_5 + { + background: #F3C8AC; + } + + .panel .result ul li.current .badge_5 + { + background: #D7B198; + } + + .panel .result ul li .badge_6 + { + background: #F3ACC3; + } + + .panel .result ul li.current .badge_6 + { + background: #D798AB; + } + + /* Results (end) */ + + /* Tree (begin) */ /**/ + .panel .tree + { + position: absolute; + top: 60px; + bottom: 0; + left: 0; + width: 100%; + //zoom: 1; + //height: expression((this.parentNode.offsetHeight - 31)); + overflow-y: scroll; + overflow-x: hidden; + -overflow-y: hidden; + background: #EDF3FE url(../i/tree_bg.png); + z-index: 30; + } + + .panel .tree ul + { + background: #EDF3FE url(../i/tree_bg.png); + } + + .panel .tree li + { + cursor: pointer; + overflow: hidden; + //height: 23px; + //display: inline; + //zoom: 1; + //width: 100%; + } + + + .panel .tree li .content + { + padding-left: 18px; + padding-top: 5px; + height: 18px; + overflow: hidden; + position: relative; + } + + .panel .tree li .icon + { + width: 10px; + height: 9px; + background: url(../i/arrows.png); + background-position: 0 -9px; + position: absolute; + left: 1px; + top: 8px; + cursor: default; + } + + .panel .tree li.closed .icon + { + background-position: 0 0; + } + + .panel .tree ul li h1 + { + font-size: 13px; + font-weight: normal; + color: #000; + margin-bottom: 2px; + white-space: nowrap; + } + + .panel .tree ul li p + { + font-size: 11px; + color: #666; + margin-bottom: 2px; + white-space: nowrap; + } + + .panel .tree ul li h1 i + { + color: #999; + font-style: normal; + } + + .panel .tree ul li.empty + { + cursor: text; + } + + .panel .tree ul li.empty h1, + .panel .tree ul li.empty p + { + color: #666; + font-style: italic; + } + + .panel .tree ul li.current + { + background: #3875D7; + } + + .panel .tree ul li.current .icon + { + background-position: -10px -9px; + } + + .panel .tree ul li.current.closed .icon + { + background-position: -10px 0; + } + + .panel .tree ul li.current h1 + { + color: #FFF; + } + + .panel .tree ul li.current p + { + color: #CCC; + } + + .panel .tree ul li.current.empty h1, + .panel .tree ul li.current.empty p + { + color: #999; + } + + .panel .tree ul li:hover + { + background: #d0d0d0; + } + + .panel .tree ul li.current:hover + { + background: #2965C0; + } + + .panel .tree .stopper + { + display: none; + } + /* Tree (end) */ /**/ + +/* Panel (end) */ + + +.panel .loader +{ + text-align: center; + margin-top: 2px; + width: 100%; + height: 59px; +} + +.panel .loader img +{ + vertical-align: middle; +} diff --git a/Sami/Resources/themes/enhanced/css/reset.css b/Sami/Resources/themes/enhanced/css/reset.css new file mode 100644 index 00000000..13f8e0a1 --- /dev/null +++ b/Sami/Resources/themes/enhanced/css/reset.css @@ -0,0 +1,53 @@ +/* http://meyerweb.com/eric/tools/css/reset/ */ +/* v1.0 | 20080212 */ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; + vertical-align: baseline; + background: transparent; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} + +/* remember to define focus styles! */ +:focus { + outline: 0; +} + +/* remember to highlight inserts somehow! */ +ins { + text-decoration: none; +} +del { + text-decoration: line-through; +} + +/* tables still need 'cellspacing="0"' in the markup */ +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/Sami/Resources/themes/enhanced/i/arrows.png b/Sami/Resources/themes/enhanced/i/arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..e54060f44ce73e7acfb92725eabea5440a0f9f24 GIT binary patch literal 477 zcmV<30V4j1P)dbVG7wVRUJ4ZXi@?ZDjy6FEKPPFgfE{K9m3e0X9iQ zK~y-6t(3uW!Y~j;-+EF29Y8t>owx*MmOJ5Y&5#P>PN0L70CZp~_#AB6im~h*@-MRG zr}baWmym=c-2LqC8#0QVq_d4h^%0POdBCG!pN1)M?% zO>`K|PH|?|j2)(00SDm6-OCxbJtwIIc$f?6>;cm)*T8O}P|w9eQE9;oZF~2HLKoWhCOJ#u@GmlVKhuUa>!_-# zEwIP|+X5&Tep-&kS+3~ztj<7nRaJL@ZqKqDaEIZf>}Vs9s_EuaEpi1w7m?Ele92%J zkrytFj-_AR{Yra`ZXtws7%l~~0hQEqX7-C4AHC1fnA2Z5aQ8#{9xv9hBu#Qp(hi|Z Tx-exV00000NkvXXu0mjf5Awjp literal 0 HcmV?d00001 diff --git a/Sami/Resources/themes/enhanced/i/loader.gif b/Sami/Resources/themes/enhanced/i/loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..e2a116c7280b4d9786ddb960fff4439a542866ec GIT binary patch literal 9427 zcmb{2dsq{9x(D#dkbztv3CV)(3ot+uwdFTVJqxVShmF%iSChYufq_uY59ckk}#=s14-cyMrVSXh`$Cfm4i zzl;K3I!UW|;4golS078X8w^r*G9wY0RfzrX+1ty@7sK~kyo=+UEB zu3Q-%9sTB;Z*Jed?dRur`t<4Z=g)U{cRzjlw5X`4wzhU+Vq$7)N~hB?nM?+QAruN} zG@4i}Zfa_>TCJac`stG=PkMTKT3TB6?%kV{lT%$?U0+`x6B833AFos@<#PG*<;yJ= z%Z(d16bc23qGMxYLqkIe2?-4i4X&=PB9SN{Ai&AV35Ma`-rlsdwB+REuCA^vTehU8 zreCc+qzD881VP;0-MziNuU)$~JUpDApMU1enf2?}`}p`AK782O**PmK zi_7Jfl$31WzWvmxQ=Xol<>lo|mMp2PthBec$F=|K5C0cxLUeLcRHQOGKc(4S-#bEybuI zGO!wCy1IBd2LQq0OGE%F8W)>cZm!;bodA)GvBpKPh3nxe2-Kkxk+fF0(0@?JYcNw_ z9#r+C3AD9m3hNpBx&aG;ND{afw?#s9&s33saD0sbBsu5H$56-9`A&{(g>_)4f`!2B zfk;F@B&Wb#qqlEH+sWaKFz*4vWmADxany$S)tb{}Og1`9Ncd6!j%Q>s!>nJ#!ZKdO z(`6_l8I}`XPz2vPLXf6NP6bT|k%J;eU!Siq3!6Mw`u(F%KHQOryb4kUon@}(c&DzZ zGf^BbmOo@MHNI_ zjymgSwI=3wSdn&z1&MHt@@k}`p6`UF^|mYSovAjeg?`8b0}u@rGf09rYSh?K*S=}B z9=#q5VQr=3#~6lPvVaKXfcgB&2i4pEqaGN>Ao=j5(k>jNxZl0Od! z&hZtA%|iQ}bVmgM^7+`RB$24z%p%?L*U9j7b&3}x-LT)*c9J|Sjv;VrEGKg=*J~;$ zfQw9)A#jQkA8i3B$f%TTU{8}&l~ENSxcR%>JoQ($Jq*73&-sO2U$ zVE>xFdMCT3FV+QtbNl7~DVja*i9ghWSm1w4ScMH1ogQq7aL@-P-5 zUcEqAGz_JGjmHU>x=@A><^Mf)I^ilB?nFvm61&Gl$xSUk2rP!bet zd<(Em5$j@u_E6=YFVMxN%5OHq`~t|H3>$uy6#HOS2>;Zs@k<{^&tg=ITGqLT0|K8*TG?}42ACa6z7@r%v*EYY> z*y&KGD4w}Wp%O&nNUy70-awkm5sHSF5r78+owPkhKQZtW}5bC}VfD%_?9-oGB_J|*Z`sjj-7iYuG zJdvaVu|MxPby!8vu!<%JPR@?q4_;f!Is_C~qaw8=czh~48VwE}ZZ95EfRVyrpZqcC zKop$3#G5fZ?*uB$Xai0y%Z;wPL?jJmt2{f&?98muotnfEV+hX+@l%&0AbaPdK7u^| z0B8q&G7={8i31cWM);a;z|dVtdT?!j+}UrsWv`SV@xnrixoW;+a^M~l;_0};18g?% zw!WQ%V2FgiXJw%Xqm(!*U+pR~QV z2qwgjmHHiT2Zd+u&UMn8_!IC|Mh&vDF;^FQYznA1A@-Yc?Vu(DWk+i!?i3IOn}s6! zq5bJ4ARu)2DvGV9Z1m^Ufw7y?nE-|Dm%lytOeK$hBv1+jc*Hc)yhg*9nz=r<3;6&P zQU`eyZC~ehtQsNutl{dSUt9OOun7`UD@5rptZ1?aL;AvB5UCz0k7f-_e2|q@gKDXWIMRgQ#Ua_VH(Y3r4$3M0je0P?cIW<8}BFK;k>`Am7MdKlzNS1MkBbC&9JWpKyo2oKWGpJ$~*09(H$xmg*kF1UkX4p z_M%t_2`%;Z$~kBPB_iJ>A;8~J;tK@!dZp6LGEpKw1Y=u+Vn;8MkgSYnoT`LEL?Q_c zNS2Z$K-Ah5@vebp*$ojL6HY9Q)t+f)i(W{rspLilhU_u=aA;8)P_mI?K@beuKYtzs1j#q$ z&NB;f8mxdL=K$dRnDn&{$E&!rGO}$>T)_6!+PMB|=?6GWkPu7AU7H+wpc)yG1nD?= z4tRp2Ejr@8VF3$FqZ$+5gH; z2v}2zuJjS^F%k;`d^8YOR)avAkOJEPjJMg{)7OkjLQrh~nj`>P=*f7UW}Y)beQjC5 zM)p9?8ng5=ZbS*fXqm6aC@5xRSaMIh#_f&ZFU^p6M2Gk+c!ncUWwIaiD3RFYHRV^a zoavdP6f*q=(%z2>P3# f!Q+M6P$=gM+!TBx5?Gm+$9DBQ392Hbz-n+d*DVM&9$| zm-vh*-9&jZqW9hLjq+Xj>H$U%x;T zZKAVONU`Sa4zl}*nvisVbvO!zm6U!Y;+;pqoZ-^1x@A3_5sH70;Hn}aR@k+Z$d@S_ zcxgv_%ANyklqo4{iR!R9WTQZ&wcQ_)NdV+9sedNjI97FB;|@oVv(N=;g)}M~c@}v5 zlbKFB0Fz#=lJND7B9no22AN1iZ`9zs4zY!cK{iQ53vY&}OB9K%45ZP5%+eMK59hIg zfyjll=vDaI)`IhZ2Bsg#fd4WEY?{Q@lSBi>8yFTdNz)q4^g&y9U;FZvETsO&{0Z zQk5Z}>}6lO^x&FKU!{=VBY`ZyBjv_~Kk!wgWUVk`^2Ms*@hKVwg3d%I(}e6*A*VQ4sv* zM9;yhNa4?Wq4V2-Z3 zcQD|~k35(k`d}oHno18^GA%0Y@>wHeO8R>U>T)~t!qkLtFw$yEP8Ee)yxOdBjqhX# z49pT6sOA`uUpml{H?UwW2iB{nivN~GU{Yh=nw(wPEU!9bc!EF{6`F;jHfkDX0;GY( zH9R67pH4hJEPU7cjR`*A;w!OGTlVA0IVJMlD6>Gg(SR29O%k=^<#R`e?t9zx4sTLF z3xqC=P6(cN%y2>!Wc0UlV6)xVz@L1$Z+d8-#x7TsxTABwfG-zLo_heWLs2AP`__{m zT1@Q1usbM@v zFIW3RvQlSbcDXuVllEdF(zcF4T9WwFi|13uYQe#Y@}K1Q6?eXKzO! zp|H+~hltvsCpL36JYEfjOGL4@SsBSbGrd`IV0Ns~=3^G>#r*(?oVa+{Ji~Hc+mBy@ zK;khS9MoD#fv=ioQFuuU=PgV$9TnX(_^gUnD3(5b`{Ij=B>SK3gVROK9i5W{A&Npq z_SIt+00Q@(6Rp$7*t<{!Y|>PGR{Oja0%YQAlds3@LuVgxFzLXDQ?Yn{$%^D&#T^XOew8*zRka0-`LX%7?-)!B4GF?v{=BQW`41y=lb0%Q_C*t zBb5qr_B2SO&+`o1a`LMYZZ2Qxym;)O?S4APtzehtG8I^`Val8|-9|q%!nS@t34#UDf*3uLv7#)dH7#A&`|t`ykxc{lEuS zPqUg9muNkgJNhA)&YX}5p2-+wPcQjyua5>X!{S80h^n*)&1$#fe&_OQ1sXD@4VS;E z**k_YHeX4keS+#KgYWOR{wb%=e-Zhf5p+x z;9UJRKYhcdBRI}b^O;BeU+dBSXT~`kDNzbMj$tUsE~hAD6850Ele}!#h*!3#T*EG4 zh8;c?W!Dl45W~;$^L16sd1=oSU;)r0 zs}LmCT1wEp^7buiSns>kUOfOWGS*T)E85u+dF3$=(|%KWOtY6^pl>6c`TxVAhD83G zIQw7X*qkyxp4BnE)FBmj|Mi#~&2GU%w%`q~Tg3UROH=(yqHImbXRf?5k9XoB0LnNG z7Ux{v>*c}QY015EodIO@-Yx}YMlHMWwz~8=kL0*+=e=v1m$A8t-$1ukp872|hj5vD24R25r+S->WdHpJ6z^o; z>=mA8nKQS8r1ztFE^O)PZaR5JOMa4tS8aPj-!X$jS_hJo67e^tx74Pf{5tJ5;$2ysG(SfC~8F zvNSRXIocRi&gWCM-r0RJYp=!Ty^RicX&s103*dK(UpTz_q457v=v)X3@8!OIMl@^r zB4&tD*iSSGy&@tO&zpW$$%*$`G(njWQVc}#(n}w#&&RaRxRBJ2JmNgh`#UyPA-Lng z-?Y1nQDX;$<^XE@KRqN~QHZtyg*D0p>{z(Nwai*8h=T1PnP0y3VR;+*mV0=PB~nwT zhlN2StRx0xtP+h#+Xo))wcsZ>6~pAy>F&oz)R;Oz`Ae|PtWOpNly*hU3H~$p|juX`wfun+&v@ey!hMA zM&SjV$7S{E;=m^tI!sYb`UcH0C4o!C;P9>is8^+PAN2#K7+S5V__QP8bBxctl@-AR z33QhCs;_S{qK56G^Y`IpL{gHt5Ao9BwYQ4yk6)VL%PBTAnQt?^i}Lclnr zxD-=E_p#lk1P)z-7o`(QM2W*DSA98gwu`u=TDPx!VO25DJZ>Q(q7ZT41 zMG2feW`t~r6FRgsrMQ@b8+8~_)%x}8Hoy3xGN)wOR2gqRF6124rMAU|oYEDD6&Esk zj;e;VRk)B^wnbG&X_RIV$CU_4A=zBT_2IMDm%9RqIb9#m@A@k-7WrZ{q4U-pb*MieKQZOwNbBn zt?@tV=fCp=`2PAxpRJ!z#_72T;RPg9X09BaYal7@as@94UnPVnST$l4X1clv+`4C( z&KfedZx0V&NBgqB!w@)0f~TWv7@0?zHEn+(UaPg;_v!u816iv1OvhNhV9=8o8U=TS zrWGRzRYaX4*#{YMVXzm1rN zhwtjm+*Y7q#nQP&3!k!f^VSG2+qrYk`^}<@X6E9E7&-=?W({7dmM&YoZspvIRq4{g z)A-}v3L~3X-nQSXpnH=&)Dzh1M z6@hz7<^%HOdTEj&U07OM#L#XqE2NEE-_i1!6Rt}-l-e6*;bdaj7zv2&F;`7ANY&P> z4uGHCbXsr0pohTD8F_=;wr3RjTWN4&-8=dwB~(=CN@|lWi(%Co?sho{`58n9xYsvt z`|N#&`@bgyqjN&OE4-p-SJBxXa$g;mVmrH7%nI2Tr!8jULclCha`A?_m#MX^`D;e! zh|nOb@hg47*1R}J_x9N!@eIf!mCDt%R4)=ZK4A+8B~7AaX;h6RO9n6VA3u1VbS>QH(Oq1JVqRRQJ5v?|SX%T*5=sV@X*j%0b4>iglnSo-W+m|G5op(k~ zI3U|9>#t}IV;{@8KLn&S7m9oBPS;?p!aPt^oLe_|(#wU63}^oZtku#)#nv(Q+>OuL zKeAu^9-^RMiIgs@ekZb9pRz9CccR$v4Jo0&60!2aw{M$E5i28LjtCad5+&N@WWN#m zV3vqt0NHETI>yC`aUy$t|KePxC$~ePh>W&zZtxo)7z~#~#UtQJ1pvA7;=LX{9kTK% zW9};beeHFdDB-Eb-6c~|`@QY~YAXr2`fjW+<#y<^y!255pI6lPa0GI5DaZix*KHZB zLO|r>$HcaQsud@Q$C}~1zkoGbnAqs&G_#W8n^*K7kfC$Lb>&o6dFF~lVFQq~taLsG zdoNGqwzG>~E0P1SOCG+1&7{bj;8(Og-xf&NG_!_@Mn9;gXZz)cvoY2MfHf+*Q5`bA z#A!~6K92+9jb!--o-loHki>3fFcQ9`wh~F!HlyccZi>wPW2V z;yI;+2;c%vAJ1Ez%h5-JVM=G`q;fN-oLoOf9r9v^6y5}DU`{a{~2TUE%QNnlxy-+>!2Kr7WNTT zu%;~9*^W?rej OhM#~I&wK(i`}3bHw&zm- literal 0 HcmV?d00001 diff --git a/Sami/Resources/themes/enhanced/i/results_bg.png b/Sami/Resources/themes/enhanced/i/results_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..199ba692349c83b68ac3479f107cdf2f79c39509 GIT binary patch literal 696 zcmeAS@N?(olHy`uVBq!ia0y~yV15B)r*N9O>AjNQC0nj*yCP#Ll5JNK`P=t|97DzJjSOG~Ei4q{mVF2=s rK*9w^1_lL)hK3QP0qFZ74t^YAov@rGA@oL5Daa?Du6{1-oD!M<--cx* literal 0 HcmV?d00001 diff --git a/Sami/Resources/themes/enhanced/i/tree_bg.png b/Sami/Resources/themes/enhanced/i/tree_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..7d236633d723288991f4cafe4ee2908c6746d0e7 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^tU#>C!3HEBuv``ZQY^(zo*^7SP{WbZ0pxQQctjR6 zFi7tKVa8qC7wiTK%9OZ9lmzFem6RtIr7}3Cl$uzQnV+W+l9`*zV61OwqHkdNIMQwkP=SW0i(`n!#NDtd0LD|QyIg>2rmC~YhJDc>S6G7^>bP0l+XkKJ?K7d literal 0 HcmV?d00001 diff --git a/Sami/Resources/themes/enhanced/index.twig b/Sami/Resources/themes/enhanced/index.twig new file mode 100644 index 00000000..317d14bc --- /dev/null +++ b/Sami/Resources/themes/enhanced/index.twig @@ -0,0 +1,3 @@ +{% extends "layout/frame.twig" %} + +{% block frame_src 'panel.html' %} diff --git a/Sami/Resources/themes/enhanced/js/jquery-1.3.2.min.js b/Sami/Resources/themes/enhanced/js/jquery-1.3.2.min.js new file mode 100644 index 00000000..b1ae21d8 --- /dev/null +++ b/Sami/Resources/themes/enhanced/js/jquery-1.3.2.min.js @@ -0,0 +1,19 @@ +/* + * jQuery JavaScript Library v1.3.2 + * http://jquery.com/ + * + * Copyright (c) 2009 John Resig + * Dual licensed under the MIT and GPL licenses. + * http://docs.jquery.com/License + * + * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) + * Revision: 6246 + */ +(function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var F=o(I||[]);F.context=document;F.selector=E;return F}}else{return o(H).find(E)}}else{if(o.isFunction(E)){return o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return this.setArray(o.isArray(E)?E:o.makeArray(E))},selector:"",jquery:"1.3.2",size:function(){return this.length},get:function(E){return E===g?Array.prototype.slice.call(this):this[E]},pushStack:function(F,H,E){var G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" ":"")+E}else{if(H){G.selector=this.selector+"."+H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return this},each:function(F,E){return o.each(this,F,E)},index:function(E){return o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return E},wrapAll:function(E){if(this[0]){var F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var G=this;while(G.firstChild){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return this.each(function(){o(this).wrapAll(E)})},append:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return this.prevObject||o([])},push:[].push,sort:[].sort,splice:[].splice,find:function(E){if(this.length===1){var F=this.pushStack([],"find",E);F.length=0;o.find(E,this[0],F);return F}else{return this.pushStack(o.unique(o.map(this,function(G){return o.find(E,G)})),"find",E)}},clone:function(G){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var I=this.outerHTML;if(!I){var J=this.ownerDocument.createElement("div");J.appendChild(this.cloneNode(true));I=J.innerHTML}return o.clean([I.replace(/ jQuery\d+="(?:\d+|null)"/g,"").replace(/^\s*/,"")])[0]}else{return this.cloneNode(true)}});if(G===true){var H=this.find("*").andSelf(),F=0;E.find("*").andSelf().each(function(){if(this.nodeName!==H[F].nodeName){return}var I=o.data(H[F],"events");for(var K in I){for(var J in I[K]){o.event.add(this,K,I[K][J],I[K][J].data)}}F++})}return E},filter:function(E){return this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return F.nodeType===1})),"filter",E)},closest:function(E){var G=o.expr.match.POS.test(E)?o(E):null,F=0;return this.map(function(){var H=this;while(H&&H.ownerDocument){if(G?G.index(H)>-1:o(H).is(E)){o.data(H,"closest",F);return H}H=H.parentNode;F++}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return this.pushStack(o.multiFilter(E,this,true),"not",E)}else{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return !!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return !!E&&this.is("."+E)},val:function(K){if(K===g){var E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var F=H?I:0,J=H?I+1:M.length;F=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(this,"select")){var N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return E===g?(this[0]?this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g,""):null):this.empty().append(E)},replaceWith:function(E){return this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=o.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild;if(H){for(var G=0,E=this.length;G1||G>0?I.cloneNode(true):I)}}if(F){o.each(F,z)}}return this;function K(N,O){return M&&o.nodeName(N,"table")&&o.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};o.fn.init.prototype=o.fn;function z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function e(){return +new Date}o.extend=o.fn.extend=function(){var J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H-1}},swap:function(H,G,I){var E={};for(var F in G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in G){H.style[F]=E[F]}},css:function(H,F,J,E){if(F=="width"||F=="height"){var L,G={position:"absolute",visibility:"hidden",display:"block"},K=F=="width"?["Left","Right"]:["Top","Bottom"];function I(){L=F=="width"?H.offsetWidth:H.offsetHeight;if(E==="border"){return}o.each(K,function(){if(!E){L-=parseFloat(o.curCSS(H,"padding"+this,true))||0}if(E==="margin"){L+=parseFloat(o.curCSS(H,"margin"+this,true))||0}else{L-=parseFloat(o.curCSS(H,"border"+this+"Width",true))||0}})}if(H.offsetWidth!==0){I()}else{o.swap(H,G,I)}return Math.max(0,Math.round(L))}return o.curCSS(H,F,J)},curCSS:function(I,F,G){var L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var J=F.replace(/\-(\w)/g,function(N,O){return O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return L},clean:function(F,K,I){K=K||document;if(typeof K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.createElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,S){if(typeof S==="number"){S+=""}if(!S){return}if(typeof S==="string"){S=S.replace(/(<(\w+)[^>]*?)\/>/g,function(U,V,T){return T.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?U:V+">"});var O=S.replace(/^\s+/,"").substring(0,10).toLowerCase();var Q=!O.indexOf("",""]||!O.indexOf("",""]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"","
    "]||!O.indexOf("",""]||(!O.indexOf("",""]||!O.indexOf("",""]||!o.support.htmlSerialize&&[1,"div
    ","
    "]||[0,"",""];L.innerHTML=Q[1]+S+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var R=/"&&!R?L.childNodes:[];for(var M=N.length-1;M>=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(S)){L.insertBefore(K.createTextNode(S.match(/^\s*/)[0]),L.firstChild)}S=o.makeArray(L.childNodes)}if(S.nodeType){G.push(S)}else{G=o.merge(G,S)}});if(I){for(var J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type property can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return N.toUpperCase()});if(L){J[G]=K}return J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var F=G.length;if(F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return E},inArray:function(G,H){for(var E=0,F=H.length;E0?this.clone(true):this).get();o.fn[F].apply(o(L[K]),I);J=J.concat(I)}return this.pushStack(J,E,G)}});o.each({removeAttr:function(E){o.attr(this,E,"");if(this.nodeType==1){this.removeAttribute(E)}},addClass:function(E){o.className.add(this,E)},removeClass:function(E){o.className.remove(this,E)},toggleClass:function(F,E){if(typeof E!=="boolean"){E=!o.className.has(this,F)}o.className[E?"add":"remove"](this,F)},remove:function(E){if(!E||o.filter(E,[this]).length){o("*",this).add([this]).each(function(){o.event.remove(this);o.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){o(this).children().remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete o.cache[H][E];E="";for(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return G},dequeue:function(H,G){var E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return F===g&&H[1]?this.data(H[0]):F}else{return this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){return this.each(function(){o.dequeue(this,E)})}}); +/* + * Sizzle CSS Selector Engine - v0.9.3 + * Copyright 2009, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){var R=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,L=0,H=Object.prototype.toString;var F=function(Y,U,ab,ac){ab=ab||[];U=U||document;if(U.nodeType!==1&&U.nodeType!==9){return[]}if(!Y||typeof Y!=="string"){return ab}var Z=[],W,af,ai,T,ad,V,X=true;R.lastIndex=0;while((W=R.exec(Y))!==null){Z.push(W[1]);if(W[2]){V=RegExp.rightContext;break}}if(Z.length>1&&M.exec(Y)){if(Z.length===2&&I.relative[Z[0]]){af=J(Z[0]+Z[1],U)}else{af=I.relative[Z[0]]?[U]:F(Z.shift(),U);while(Z.length){Y=Z.shift();if(I.relative[Y]){Y+=Z.shift()}af=J(Y,af)}}}else{var ae=ac?{expr:Z.pop(),set:E(ac)}:F.find(Z.pop(),Z.length===1&&U.parentNode?U.parentNode:U,Q(U));af=F.filter(ae.expr,ae.set);if(Z.length>0){ai=E(af)}else{X=false}while(Z.length){var ah=Z.pop(),ag=ah;if(!I.relative[ah]){ah=""}else{ag=Z.pop()}if(ag==null){ag=U}I.relative[ah](ai,ag,Q(U))}}if(!ai){ai=af}if(!ai){throw"Syntax error, unrecognized expression: "+(ah||Y)}if(H.call(ai)==="[object Array]"){if(!X){ab.push.apply(ab,ai)}else{if(U.nodeType===1){for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&(ai[aa]===true||ai[aa].nodeType===1&&K(U,ai[aa]))){ab.push(af[aa])}}}else{for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&ai[aa].nodeType===1){ab.push(af[aa])}}}}}else{E(ai,ab)}if(V){F(V,U,ab,ac);if(G){hasDuplicate=false;ab.sort(G);if(hasDuplicate){for(var aa=1;aa":function(Z,U,aa){var X=typeof U==="string";if(X&&!/\W/.test(U)){U=aa?U:U.toUpperCase();for(var V=0,T=Z.length;V=0)){if(!V){T.push(Y)}}else{if(V){U[X]=false}}}}return false},ID:function(T){return T[1].replace(/\\/g,"")},TAG:function(U,T){for(var V=0;T[V]===false;V++){}return T[V]&&Q(T[V])?U[1]:U[1].toUpperCase()},CHILD:function(T){if(T[1]=="nth"){var U=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(T[2]=="even"&&"2n"||T[2]=="odd"&&"2n+1"||!/\D/.test(T[2])&&"0n+"+T[2]||T[2]);T[2]=(U[1]+(U[2]||1))-0;T[3]=U[3]-0}T[0]=L++;return T},ATTR:function(X,U,V,T,Y,Z){var W=X[1].replace(/\\/g,"");if(!Z&&I.attrMap[W]){X[1]=I.attrMap[W]}if(X[2]==="~="){X[4]=" "+X[4]+" "}return X},PSEUDO:function(X,U,V,T,Y){if(X[1]==="not"){if(X[3].match(R).length>1||/^\w/.test(X[3])){X[3]=F(X[3],null,null,U)}else{var W=F.filter(X[3],U,V,true^Y);if(!V){T.push.apply(T,W)}return false}}else{if(I.match.POS.test(X[0])||I.match.CHILD.test(X[0])){return true}}return X},POS:function(T){T.unshift(true);return T}},filters:{enabled:function(T){return T.disabled===false&&T.type!=="hidden"},disabled:function(T){return T.disabled===true},checked:function(T){return T.checked===true},selected:function(T){T.parentNode.selectedIndex;return T.selected===true},parent:function(T){return !!T.firstChild},empty:function(T){return !T.firstChild},has:function(V,U,T){return !!F(T[3],V).length},header:function(T){return/h\d/i.test(T.nodeName)},text:function(T){return"text"===T.type},radio:function(T){return"radio"===T.type},checkbox:function(T){return"checkbox"===T.type},file:function(T){return"file"===T.type},password:function(T){return"password"===T.type},submit:function(T){return"submit"===T.type},image:function(T){return"image"===T.type},reset:function(T){return"reset"===T.type},button:function(T){return"button"===T.type||T.nodeName.toUpperCase()==="BUTTON"},input:function(T){return/input|select|textarea|button/i.test(T.nodeName)}},setFilters:{first:function(U,T){return T===0},last:function(V,U,T,W){return U===W.length-1},even:function(U,T){return T%2===0},odd:function(U,T){return T%2===1},lt:function(V,U,T){return UT[3]-0},nth:function(V,U,T){return T[3]-0==U},eq:function(V,U,T){return T[3]-0==U}},filter:{PSEUDO:function(Z,V,W,aa){var U=V[1],X=I.filters[U];if(X){return X(Z,W,V,aa)}else{if(U==="contains"){return(Z.textContent||Z.innerText||"").indexOf(V[3])>=0}else{if(U==="not"){var Y=V[3];for(var W=0,T=Y.length;W=0)}}},ID:function(U,T){return U.nodeType===1&&U.getAttribute("id")===T},TAG:function(U,T){return(T==="*"&&U.nodeType===1)||U.nodeName===T},CLASS:function(U,T){return(" "+(U.className||U.getAttribute("class"))+" ").indexOf(T)>-1},ATTR:function(Y,W){var V=W[1],T=I.attrHandle[V]?I.attrHandle[V](Y):Y[V]!=null?Y[V]:Y.getAttribute(V),Z=T+"",X=W[2],U=W[4];return T==null?X==="!=":X==="="?Z===U:X==="*="?Z.indexOf(U)>=0:X==="~="?(" "+Z+" ").indexOf(U)>=0:!U?Z&&T!==false:X==="!="?Z!=U:X==="^="?Z.indexOf(U)===0:X==="$="?Z.substr(Z.length-U.length)===U:X==="|="?Z===U||Z.substr(0,U.length+1)===U+"-":false},POS:function(X,U,V,Y){var T=U[2],W=I.setFilters[T];if(W){return W(X,V,U,Y)}}}};var M=I.match.POS;for(var O in I.match){I.match[O]=RegExp(I.match[O].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var E=function(U,T){U=Array.prototype.slice.call(U);if(T){T.push.apply(T,U);return T}return U};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(N){E=function(X,W){var U=W||[];if(H.call(X)==="[object Array]"){Array.prototype.push.apply(U,X)}else{if(typeof X.length==="number"){for(var V=0,T=X.length;V";var T=document.documentElement;T.insertBefore(U,T.firstChild);if(!!document.getElementById(V)){I.find.ID=function(X,Y,Z){if(typeof Y.getElementById!=="undefined"&&!Z){var W=Y.getElementById(X[1]);return W?W.id===X[1]||typeof W.getAttributeNode!=="undefined"&&W.getAttributeNode("id").nodeValue===X[1]?[W]:g:[]}};I.filter.ID=function(Y,W){var X=typeof Y.getAttributeNode!=="undefined"&&Y.getAttributeNode("id");return Y.nodeType===1&&X&&X.nodeValue===W}}T.removeChild(U)})();(function(){var T=document.createElement("div");T.appendChild(document.createComment(""));if(T.getElementsByTagName("*").length>0){I.find.TAG=function(U,Y){var X=Y.getElementsByTagName(U[1]);if(U[1]==="*"){var W=[];for(var V=0;X[V];V++){if(X[V].nodeType===1){W.push(X[V])}}X=W}return X}}T.innerHTML="";if(T.firstChild&&typeof T.firstChild.getAttribute!=="undefined"&&T.firstChild.getAttribute("href")!=="#"){I.attrHandle.href=function(U){return U.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var T=F,U=document.createElement("div");U.innerHTML="

    ";if(U.querySelectorAll&&U.querySelectorAll(".TEST").length===0){return}F=function(Y,X,V,W){X=X||document;if(!W&&X.nodeType===9&&!Q(X)){try{return E(X.querySelectorAll(Y),V)}catch(Z){}}return T(Y,X,V,W)};F.find=T.find;F.filter=T.filter;F.selectors=T.selectors;F.matches=T.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){(function(){var T=document.createElement("div");T.innerHTML="
    ";if(T.getElementsByClassName("e").length===0){return}T.lastChild.className="e";if(T.getElementsByClassName("e").length===1){return}I.order.splice(1,0,"CLASS");I.find.CLASS=function(U,V,W){if(typeof V.getElementsByClassName!=="undefined"&&!W){return V.getElementsByClassName(U[1])}}})()}function P(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W0){X=T;break}}}T=T[U]}ad[W]=X}}}var K=document.compareDocumentPosition?function(U,T){return U.compareDocumentPosition(T)&16}:function(U,T){return U!==T&&(U.contains?U.contains(T):true)};var Q=function(T){return T.nodeType===9&&T.documentElement.nodeName!=="HTML"||!!T.ownerDocument&&Q(T.ownerDocument)};var J=function(T,aa){var W=[],X="",Y,V=aa.nodeType?[aa]:aa;while((Y=I.match.PSEUDO.exec(T))){X+=Y[0];T=T.replace(I.match.PSEUDO,"")}T=I.relative[T]?T+"*":T;for(var Z=0,U=V.length;Z0||T.offsetHeight>0};F.selectors.filters.animated=function(T){return o.grep(o.timers,function(U){return T===U.elem}).length};o.multiFilter=function(V,T,U){if(U){V=":not("+V+")"}return F.matches(V,T)};o.dir=function(V,U){var T=[],W=V[U];while(W&&W!=document){if(W.nodeType==1){T.push(W)}W=W[U]}return T};o.nth=function(X,T,V,W){T=T||1;var U=0;for(;X;X=X[V]){if(X.nodeType==1&&++U==T){break}}return X};o.sibling=function(V,U){var T=[];for(;V;V=V.nextSibling){if(V.nodeType==1&&V!=U){T.push(V)}}return T};return;l.Sizzle=F})();o.event={add:function(I,F,H,K){if(I.nodeType==3||I.nodeType==8){return}if(I.setInterval&&I!=l){I=l}if(!H.guid){H.guid=this.guid++}if(K!==g){var G=H;H=this.proxy(G);H.data=K}var E=o.data(I,"events")||o.data(I,"events",{}),J=o.data(I,"handle")||o.data(I,"handle",function(){return typeof o!=="undefined"&&!o.event.triggered?o.event.handle.apply(arguments.callee.elem,arguments):g});J.elem=I;o.each(F.split(/\s+/),function(M,N){var O=N.split(".");N=O.shift();H.type=O.slice().sort().join(".");var L=E[N];if(o.event.specialAll[N]){o.event.specialAll[N].setup.call(I,K,O)}if(!L){L=E[N]={};if(!o.event.special[N]||o.event.special[N].setup.call(I,K,O)===false){if(I.addEventListener){I.addEventListener(N,J,false)}else{if(I.attachEvent){I.attachEvent("on"+N,J)}}}}L[H.guid]=H;o.event.global[N]=true});I=null},guid:1,global:{},remove:function(K,H,J){if(K.nodeType==3||K.nodeType==8){return}var G=o.data(K,"events"),F,E;if(G){if(H===g||(typeof H==="string"&&H.charAt(0)==".")){for(var I in G){this.remove(K,I+(H||""))}}else{if(H.type){J=H.handler;H=H.type}o.each(H.split(/\s+/),function(M,O){var Q=O.split(".");O=Q.shift();var N=RegExp("(^|\\.)"+Q.slice().sort().join(".*\\.")+"(\\.|$)");if(G[O]){if(J){delete G[O][J.guid]}else{for(var P in G[O]){if(N.test(G[O][P].type)){delete G[O][P]}}}if(o.event.specialAll[O]){o.event.specialAll[O].teardown.call(K,Q)}for(F in G[O]){break}if(!F){if(!o.event.special[O]||o.event.special[O].teardown.call(K,Q)===false){if(K.removeEventListener){K.removeEventListener(O,o.data(K,"handle"),false)}else{if(K.detachEvent){K.detachEvent("on"+O,o.data(K,"handle"))}}}F=null;delete G[O]}}})}for(F in G){break}if(!F){var L=o.data(K,"handle");if(L){L.elem=null}o.removeData(K,"events");o.removeData(K,"handle")}}},trigger:function(I,K,H,E){var G=I.type||I;if(!E){I=typeof I==="object"?I[h]?I:o.extend(o.Event(G),I):o.Event(G);if(G.indexOf("!")>=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var J,E;K=arguments[0]=o.event.fix(K||l.event);K.currentTarget=this;var L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||this.guid++;return E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return new o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function k(){return false}function u(){return true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.result}},toggle:function(G){var E=arguments,F=1;while(F=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("
    ").append(M.responseText.replace(//g,"")).find(E):M.responseText)}if(K){F.each(K,[M.responseText,L,M])}}});return this},serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?o.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password|search/i.test(this.type))}).map(function(E,F){var G=o(this).val();return G==null?null:o.isArray(G)?o.map(G,function(I,H){return{name:F.name,value:I}}):{name:F.name,value:G}}).get()}});o.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(E,F){o.fn[F]=function(G){return this.bind(F,G)}});var r=e();o.extend({get:function(E,G,H,F){if(o.isFunction(G)){H=G;G=null}return o.ajax({type:"GET",url:E,data:G,success:H,dataType:F})},getScript:function(E,F){return o.get(E,null,F,"script")},getJSON:function(E,F,G){return o.get(E,F,G,"json")},post:function(E,G,H,F){if(o.isFunction(G)){H=G;G={}}return o.ajax({type:"POST",url:E,data:G,success:H,dataType:F})},ajaxSetup:function(E){o.extend(o.ajaxSettings,E)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(M){M=o.extend(true,M,o.extend(true,{},o.ajaxSettings,M));var W,F=/=\?(&|$)/g,R,V,G=M.type.toUpperCase();if(M.data&&M.processData&&typeof M.data!=="string"){M.data=o.param(M.data)}if(M.dataType=="jsonp"){if(G=="GET"){if(!M.url.match(F)){M.url+=(M.url.match(/\?/)?"&":"?")+(M.jsonp||"callback")+"=?"}}else{if(!M.data||!M.data.match(F)){M.data=(M.data?M.data+"&":"")+(M.jsonp||"callback")+"=?"}}M.dataType="json"}if(M.dataType=="json"&&(M.data&&M.data.match(F)||M.url.match(F))){W="jsonp"+r++;if(M.data){M.data=(M.data+"").replace(F,"="+W+"$1")}M.url=M.url.replace(F,"="+W+"$1");M.dataType="script";l[W]=function(X){V=X;I();L();l[W]=g;try{delete l[W]}catch(Y){}if(H){H.removeChild(T)}}}if(M.dataType=="script"&&M.cache==null){M.cache=false}if(M.cache===false&&G=="GET"){var E=e();var U=M.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+E+"$2");M.url=U+((U==M.url)?(M.url.match(/\?/)?"&":"?")+"_="+E:"")}if(M.data&&G=="GET"){M.url+=(M.url.match(/\?/)?"&":"?")+M.data;M.data=null}if(M.global&&!o.active++){o.event.trigger("ajaxStart")}var Q=/^(\w+:)?\/\/([^\/?#]+)/.exec(M.url);if(M.dataType=="script"&&G=="GET"&&Q&&(Q[1]&&Q[1]!=location.protocol||Q[2]!=location.host)){var H=document.getElementsByTagName("head")[0];var T=document.createElement("script");T.src=M.url;if(M.scriptCharset){T.charset=M.scriptCharset}if(!W){var O=false;T.onload=T.onreadystatechange=function(){if(!O&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){O=true;I();L();T.onload=T.onreadystatechange=null;H.removeChild(T)}}}H.appendChild(T);return g}var K=false;var J=M.xhr();if(M.username){J.open(G,M.url,M.async,M.username,M.password)}else{J.open(G,M.url,M.async)}try{if(M.data){J.setRequestHeader("Content-Type",M.contentType)}if(M.ifModified){J.setRequestHeader("If-Modified-Since",o.lastModified[M.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}J.setRequestHeader("X-Requested-With","XMLHttpRequest");J.setRequestHeader("Accept",M.dataType&&M.accepts[M.dataType]?M.accepts[M.dataType]+", */*":M.accepts._default)}catch(S){}if(M.beforeSend&&M.beforeSend(J,M)===false){if(M.global&&!--o.active){o.event.trigger("ajaxStop")}J.abort();return false}if(M.global){o.event.trigger("ajaxSend",[J,M])}var N=function(X){if(J.readyState==0){if(P){clearInterval(P);P=null;if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}}else{if(!K&&J&&(J.readyState==4||X=="timeout")){K=true;if(P){clearInterval(P);P=null}R=X=="timeout"?"timeout":!o.httpSuccess(J)?"error":M.ifModified&&o.httpNotModified(J,M.url)?"notmodified":"success";if(R=="success"){try{V=o.httpData(J,M.dataType,M)}catch(Z){R="parsererror"}}if(R=="success"){var Y;try{Y=J.getResponseHeader("Last-Modified")}catch(Z){}if(M.ifModified&&Y){o.lastModified[M.url]=Y}if(!W){I()}}else{o.handleError(M,J,R)}L();if(X){J.abort()}if(M.async){J=null}}}};if(M.async){var P=setInterval(N,13);if(M.timeout>0){setTimeout(function(){if(J&&!K){N("timeout")}},M.timeout)}}try{J.send(M.data)}catch(S){o.handleError(M,J,null,S)}if(!M.async){N()}function I(){if(M.success){M.success(V,R)}if(M.global){o.event.trigger("ajaxSuccess",[J,M])}}function L(){if(M.complete){M.complete(J,R)}if(M.global){o.event.trigger("ajaxComplete",[J,M])}if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}return J},handleError:function(F,H,E,G){if(F.error){F.error(H,E,G)}if(F.global){o.event.trigger("ajaxError",[H,F,G])}},active:0,httpSuccess:function(F){try{return !F.status&&location.protocol=="file:"||(F.status>=200&&F.status<300)||F.status==304||F.status==1223}catch(E){}return false},httpNotModified:function(G,E){try{var H=G.getResponseHeader("Last-Modified");return G.status==304||H==o.lastModified[E]}catch(F){}return false},httpData:function(J,H,G){var F=J.getResponseHeader("content-type"),E=H=="xml"||!H&&F&&F.indexOf("xml")>=0,I=E?J.responseXML:J.responseText;if(E&&I.documentElement.tagName=="parsererror"){throw"parsererror"}if(G&&G.dataFilter){I=G.dataFilter(I,H)}if(typeof I==="string"){if(H=="script"){o.globalEval(I)}if(H=="json"){I=l["eval"]("("+I+")")}}return I},param:function(E){var G=[];function H(I,J){G[G.length]=encodeURIComponent(I)+"="+encodeURIComponent(J)}if(o.isArray(E)||E.jquery){o.each(E,function(){H(this.name,this.value)})}else{for(var F in E){if(o.isArray(E[F])){o.each(E[F],function(){H(F,this)})}else{H(F,o.isFunction(E[F])?E[F]():E[F])}}}return G.join("&").replace(/%20/g,"+")}});var m={},n,d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function t(F,E){var G={};o.each(d.concat.apply([],d.slice(0,E)),function(){G[this]=F});return G}o.fn.extend({show:function(J,L){if(J){return this.animate(t("show",3),J,L)}else{for(var H=0,F=this.length;H").appendTo("body");K=I.css("display");if(K==="none"){K="block"}I.remove();m[G]=K}o.data(this[H],"olddisplay",K)}}for(var H=0,F=this.length;H=0;H--){if(G[H].elem==this){if(E){G[H](true)}G.splice(H,1)}}});if(!E){this.dequeue()}return this}});o.each({slideDown:t("show",1),slideUp:t("hide",1),slideToggle:t("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(E,F){o.fn[E]=function(G,H){return this.animate(F,G,H)}});o.extend({speed:function(G,H,F){var E=typeof G==="object"?G:{complete:F||!F&&H||o.isFunction(G)&&G,duration:G,easing:F&&H||H&&!o.isFunction(H)&&H};E.duration=o.fx.off?0:typeof E.duration==="number"?E.duration:o.fx.speeds[E.duration]||o.fx.speeds._default;E.old=E.complete;E.complete=function(){if(E.queue!==false){o(this).dequeue()}if(o.isFunction(E.old)){E.old.call(this)}};return E},easing:{linear:function(G,H,E,F){return E+F*G},swing:function(G,H,E,F){return((-Math.cos(G*Math.PI)/2)+0.5)*F+E}},timers:[],fx:function(F,E,G){this.options=E;this.elem=F;this.prop=G;if(!E.orig){E.orig={}}}});o.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(o.fx.step[this.prop]||o.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(F){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var E=parseFloat(o.css(this.elem,this.prop,F));return E&&E>-10000?E:parseFloat(o.curCSS(this.elem,this.prop))||0},custom:function(I,H,G){this.startTime=e();this.start=I;this.end=H;this.unit=G||this.unit||"px";this.now=this.start;this.pos=this.state=0;var E=this;function F(J){return E.step(J)}F.elem=this.elem;if(F()&&o.timers.push(F)&&!n){n=setInterval(function(){var K=o.timers;for(var J=0;J=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var E=true;for(var F in this.options.curAnim){if(this.options.curAnim[F]!==true){E=false}}if(E){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(o.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){o(this.elem).hide()}if(this.options.hide||this.options.show){for(var I in this.options.curAnim){o.attr(this.elem.style,I,this.options.orig[I])}}this.options.complete.call(this.elem)}return false}else{var J=G-this.startTime;this.state=J/this.options.duration;this.pos=o.easing[this.options.easing||(o.easing.swing?"swing":"linear")](this.state,J,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};o.extend(o.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(E){o.attr(E.elem.style,"opacity",E.now)},_default:function(E){if(E.elem.style&&E.elem.style[E.prop]!=null){E.elem.style[E.prop]=E.now+E.unit}else{E.elem[E.prop]=E.now}}}});if(document.documentElement.getBoundingClientRect){o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}var G=this[0].getBoundingClientRect(),J=this[0].ownerDocument,F=J.body,E=J.documentElement,L=E.clientTop||F.clientTop||0,K=E.clientLeft||F.clientLeft||0,I=G.top+(self.pageYOffset||o.boxModel&&E.scrollTop||F.scrollTop)-L,H=G.left+(self.pageXOffset||o.boxModel&&E.scrollLeft||F.scrollLeft)-K;return{top:I,left:H}}}else{o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}o.offset.initialized||o.offset.initialize();var J=this[0],G=J.offsetParent,F=J,O=J.ownerDocument,M,H=O.documentElement,K=O.body,L=O.defaultView,E=L.getComputedStyle(J,null),N=J.offsetTop,I=J.offsetLeft;while((J=J.parentNode)&&J!==K&&J!==H){M=L.getComputedStyle(J,null);N-=J.scrollTop,I-=J.scrollLeft;if(J===G){N+=J.offsetTop,I+=J.offsetLeft;if(o.offset.doesNotAddBorder&&!(o.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(J.tagName))){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}F=G,G=J.offsetParent}if(o.offset.subtractsBorderForOverflowNotVisible&&M.overflow!=="visible"){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}E=M}if(E.position==="relative"||E.position==="static"){N+=K.offsetTop,I+=K.offsetLeft}if(E.position==="fixed"){N+=Math.max(H.scrollTop,K.scrollTop),I+=Math.max(H.scrollLeft,K.scrollLeft)}return{top:N,left:I}}}o.offset={initialize:function(){if(this.initialized){return}var L=document.body,F=document.createElement("div"),H,G,N,I,M,E,J=L.style.marginTop,K='
    ';M={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(E in M){F.style[E]=M[E]}F.innerHTML=K;L.insertBefore(F,L.firstChild);H=F.firstChild,G=H.firstChild,I=H.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(G.offsetTop!==5);this.doesAddBorderForTableAndCells=(I.offsetTop===5);H.style.overflow="hidden",H.style.position="relative";this.subtractsBorderForOverflowNotVisible=(G.offsetTop===-5);L.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(L.offsetTop===0);L.style.marginTop=J;L.removeChild(F);this.initialized=true},bodyOffset:function(E){o.offset.initialized||o.offset.initialize();var G=E.offsetTop,F=E.offsetLeft;if(o.offset.doesNotIncludeMarginInBodyOffset){G+=parseInt(o.curCSS(E,"marginTop",true),10)||0,F+=parseInt(o.curCSS(E,"marginLeft",true),10)||0}return{top:G,left:F}}};o.fn.extend({position:function(){var I=0,H=0,F;if(this[0]){var G=this.offsetParent(),J=this.offset(),E=/^body|html$/i.test(G[0].tagName)?{top:0,left:0}:G.offset();J.top-=j(this,"marginTop");J.left-=j(this,"marginLeft");E.top+=j(G,"borderTopWidth");E.left+=j(G,"borderLeftWidth");F={top:J.top-E.top,left:J.left-E.left}}return F},offsetParent:function(){var E=this[0].offsetParent||document.body;while(E&&(!/^body|html$/i.test(E.tagName)&&o.css(E,"position")=="static")){E=E.offsetParent}return o(E)}});o.each(["Left","Top"],function(F,E){var G="scroll"+E;o.fn[G]=function(H){if(!this[0]){return null}return H!==g?this.each(function(){this==l||this==document?l.scrollTo(!F?H:o(l).scrollLeft(),F?H:o(l).scrollTop()):this[G]=H}):this[0]==l||this[0]==document?self[F?"pageYOffset":"pageXOffset"]||o.boxModel&&document.documentElement[G]||document.body[G]:this[0][G]}});o.each(["Height","Width"],function(I,G){var E=I?"Left":"Top",H=I?"Right":"Bottom",F=G.toLowerCase();o.fn["inner"+G]=function(){return this[0]?o.css(this[0],F,false,"padding"):null};o.fn["outer"+G]=function(K){return this[0]?o.css(this[0],F,false,K?"margin":"border"):null};var J=G.toLowerCase();o.fn[J]=function(K){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+G]||document.body["client"+G]:this[0]==document?Math.max(document.documentElement["client"+G],document.body["scroll"+G],document.documentElement["scroll"+G],document.body["offset"+G],document.documentElement["offset"+G]):K===g?(this.length?o.css(this[0],J):null):this.css(J,typeof K==="string"?K:K+"px")}})})(); \ No newline at end of file diff --git a/Sami/Resources/themes/enhanced/js/searchdoc.js b/Sami/Resources/themes/enhanced/js/searchdoc.js new file mode 100644 index 00000000..08537ec4 --- /dev/null +++ b/Sami/Resources/themes/enhanced/js/searchdoc.js @@ -0,0 +1,651 @@ +/* +Copyright (c) 2009 Vladimir Kolesnikov + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +Searchdoc = {}; + +// navigation.js ------------------------------------------ + +Searchdoc.Navigation = new function() { + this.initNavigation = function() { + var _this = this; + + $(document).keydown(function(e) { + _this.onkeydown(e); + }).keyup(function(e) { + _this.onkeyup(e); + }); + + this.navigationActive = true; + } + + this.setNavigationActive = function(state) { + this.navigationActive = state; + this.clearMoveTimeout(); + } + + + this.onkeyup = function(e) { + if (!this.navigationActive) return; + switch(e.keyCode) { + case 37: //Event.KEY_LEFT: + case 38: //Event.KEY_UP: + case 39: //Event.KEY_RIGHT: + case 40: //Event.KEY_DOWN: + case 73: // i - qwerty + case 74: // j + case 75: // k + case 76: // l + case 67: // c - dvorak + case 72: // h + case 84: // t + case 78: // n + this.clearMoveTimeout(); + break; + } + } + + this.onkeydown = function(e) { + if (!this.navigationActive) return; + switch(e.keyCode) { + case 37: //Event.KEY_LEFT: + case 74: // j (qwerty) + case 72: // h (dvorak) + if (this.moveLeft()) e.preventDefault(); + break; + case 38: //Event.KEY_UP: + case 73: // i (qwerty) + case 67: // c (dvorak) + if (e.keyCode == 38 || e.ctrlKey) { + if (this.moveUp()) e.preventDefault(); + this.startMoveTimeout(false); + } + break; + case 39: //Event.KEY_RIGHT: + case 76: // l (qwerty) + case 78: // n (dvorak) + if (this.moveRight()) e.preventDefault(); + break; + case 40: //Event.KEY_DOWN: + case 75: // k (qwerty) + case 84: // t (dvorak) + if (e.keyCode == 40 || e.ctrlKey) { + if (this.moveDown()) e.preventDefault(); + this.startMoveTimeout(true); + } + break; + case 9: //Event.KEY_TAB: + case 13: //Event.KEY_RETURN: + if (this.$current) this.select(this.$current); + break; + } + if (e.ctrlKey && e.shiftKey) this.select(this.$current); + } + + this.clearMoveTimeout = function() { + clearTimeout(this.moveTimeout); + this.moveTimeout = null; + } + + this.startMoveTimeout = function(isDown) { + if (!$.browser.mozilla && !$.browser.opera) return; + if (this.moveTimeout) this.clearMoveTimeout(); + var _this = this; + + var go = function() { + if (!_this.moveTimeout) return; + _this[isDown ? 'moveDown' : 'moveUp'](); + _this.moveTimout = setTimeout(go, 100); + } + this.moveTimeout = setTimeout(go, 200); + } + + this.moveRight = function() { + } + + this.moveLeft = function() { + } + + this.move = function(isDown) { + } + + this.moveUp = function() { + return this.move(false); + } + + this.moveDown = function() { + return this.move(true); + } +} + + +// scrollIntoView.js -------------------------------------- + +function scrollIntoView(element, view) { + var offset, viewHeight, viewScroll, height; + offset = element.offsetTop; + height = element.offsetHeight; + viewHeight = view.offsetHeight; + viewScroll = view.scrollTop; + if (offset - viewScroll + height > viewHeight) { + view.scrollTop = offset - viewHeight + height; + } + if (offset < viewScroll) { + view.scrollTop = offset; + } +} + + +// searcher.js -------------------------------------------- + +Searchdoc.Searcher = function(data) { + this.data = data; + this.handlers = []; +} + +Searchdoc.Searcher.prototype = new function() { + var CHUNK_SIZE = 1000, // search is performed in chunks of 1000 for non-bloking user input + MAX_RESULTS = 100, // do not try to find more than 100 results + huid = 1, suid = 1, + runs = 0; + + + this.find = function(query) { + var queries = splitQuery(query), + regexps = buildRegexps(queries), + highlighters = buildHilighters(queries), + state = { from: 0, pass: 0, limit: MAX_RESULTS, n: suid++}, + _this = this; + this.currentSuid = state.n; + + if (!query) return; + + var run = function() { + // stop current search thread if new search started + if (state.n != _this.currentSuid) return; + + var results = performSearch(_this.data, regexps, queries, highlighters, state), + hasMore = (state.limit > 0 && state.pass < 3); + + triggerResults.call(_this, results, !hasMore); + if (hasMore) { + setTimeout(run, 2); + } + runs++; + }; + runs = 0; + + // start search thread + run(); + } + + /* ----- Events ------ */ + this.ready = function(fn) { + fn.huid = huid; + this.handlers.push(fn); + } + + /* ----- Utilities ------ */ + function splitQuery(query) { + return jQuery.grep(query.split(/(\s+|\(\)?)/), function(string) { return string.match(/\S/) }); + } + + function buildRegexps(queries) { + return jQuery.map(queries, function(query) { return new RegExp(query.replace(/(.)/g, '([$1])([^$1]*?)'), 'i') }); + } + + function buildHilighters(queries) { + return jQuery.map(queries, function(query) { + return jQuery.map( query.split(''), function(l, i){ return '\u0001$' + (i*2+1) + '\u0002$' + (i*2+2) } ).join('') + }); + } + + // function longMatchRegexp(index, longIndex, regexps) { + // for (var i = regexps.length - 1; i >= 0; i--){ + // if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) return false; + // }; + // return true; + // } + + + /* ----- Mathchers ------ */ + function matchPass1(index, longIndex, queries, regexps) { + if (index.indexOf(queries[0]) != 0) return false; + for (var i=1, l = regexps.length; i < l; i++) { + if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) return false; + }; + return true; + } + + function matchPass2(index, longIndex, queries, regexps) { + if (index.indexOf(queries[0]) == -1) return false; + for (var i=1, l = regexps.length; i < l; i++) { + if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) return false; + }; + return true; + } + + function matchPassRegexp(index, longIndex, queries, regexps) { + if (!index.match(regexps[0])) return false; + for (var i=1, l = regexps.length; i < l; i++) { + if (!index.match(regexps[i]) && !longIndex.match(regexps[i])) return false; + }; + return true; + } + + + /* ----- Highlighters ------ */ + function highlightRegexp(info, queries, regexps, highlighters) { + var result = createResult(info); + for (var i=0, l = regexps.length; i < l; i++) { + result.title = result.title.replace(regexps[i], highlighters[i]); + if (i > 0) + result.namespace = result.namespace.replace(regexps[i], highlighters[i]); + }; + return result; + } + + function hltSubstring(string, pos, length) { + return string.substring(0, pos) + '\u0001' + string.substring(pos, pos + length) + '\u0002' + string.substring(pos + length); + } + + function highlightQuery(info, queries, regexps, highlighters) { + var result = createResult(info), pos = 0, lcTitle = result.title.toLowerCase(); + pos = lcTitle.indexOf(queries[0]); + if (pos != -1) { + result.title = hltSubstring(result.title, pos, queries[0].length); + } + for (var i=1, l = regexps.length; i < l; i++) { + result.title = result.title.replace(regexps[i], highlighters[i]); + result.namespace = result.namespace.replace(regexps[i], highlighters[i]); + }; + return result; + } + + function createResult(info) { + var result = {}; + result.title = info[0]; + result.namespace = info[1]; + result.path = info[2]; + result.params = info[3]; + result.snippet = info[4]; + result.badge = info[6]; + return result; + } + + /* ----- Searching ------ */ + function performSearch(data, regexps, queries, highlighters, state) { + var searchIndex = data.searchIndex, // search by title first and then by source + longSearchIndex = data.longSearchIndex, + info = data.info, + result = [], + i = state.from, + l = searchIndex.length, + togo = CHUNK_SIZE, + matchFunc, hltFunc; + + while (state.pass < 3 && state.limit > 0 && togo > 0) { + if (state.pass == 0) { + matchFunc = matchPass1; + hltFunc = highlightQuery; + } else if (state.pass == 1) { + matchFunc = matchPass2; + hltFunc = highlightQuery; + } else if (state.pass == 2) { + matchFunc = matchPassRegexp; + hltFunc = highlightRegexp; + } + + for (; togo > 0 && i < l && state.limit > 0; i++, togo--) { + if (info[i].n == state.n) continue; + if (matchFunc(searchIndex[i], longSearchIndex[i], queries, regexps)) { + info[i].n = state.n; + result.push(hltFunc(info[i], queries, regexps, highlighters)); + state.limit--; + } + }; + if (searchIndex.length <= i) { + state.pass++; + i = state.from = 0; + } else { + state.from = i; + } + } + return result; + } + + function triggerResults(results, isLast) { + jQuery.each(this.handlers, function(i, fn) { fn.call(this, results, isLast) }) + } +} + + + + +// panel.js ----------------------------------------------- + +Searchdoc.Panel = function(element, data, tree, frame) { + this.$element = $(element); + this.$input = $('input', element).eq(0); + this.$result = $('.result ul', element).eq(0); + this.frame = frame; + this.$current = null; + this.$view = this.$result.parent(); + this.data = data; + this.searcher = new Searchdoc.Searcher(data.index); + this.tree = new Searchdoc.Tree($('.tree', element), tree, this); + this.init(); +} + +Searchdoc.Panel.prototype = $.extend({}, Searchdoc.Navigation, new function() { + var suid = 1; + + this.init = function() { + var _this = this; + var observer = function() { + _this.search(_this.$input[0].value); + }; + this.$input.keyup(observer); + this.$input.click(observer); // mac's clear field + + this.searcher.ready(function(results, isLast) { + _this.addResults(results, isLast); + }) + + this.$result.click(function(e) { + _this.$current.removeClass('current'); + _this.$current = $(e.target).closest('li').addClass('current'); + _this.select(); + _this.$input.focus(); + }); + + this.initNavigation(); + this.setNavigationActive(false); + } + + this.search = function(value, selectFirstMatch) { + value = jQuery.trim(value).toLowerCase(); + this.selectFirstMatch = selectFirstMatch; + if (value) { + this.$element.removeClass('panel_tree').addClass('panel_results'); + this.tree.setNavigationActive(false); + this.setNavigationActive(true); + } else { + this.$element.addClass('panel_tree').removeClass('panel_results'); + this.tree.setNavigationActive(true); + this.setNavigationActive(false); + } + if (value != this.lastQuery) { + this.lastQuery = value; + this.firstRun = true; + this.searcher.find(value); + } + } + + this.addResults = function(results, isLast) { + var target = this.$result.get(0); + if (this.firstRun && (results.length > 0 || isLast)) { + this.$current = null; + this.$result.empty(); + } + for (var i=0, l = results.length; i < l; i++) { + target.appendChild(renderItem.call(this, results[i])); + }; + if (this.firstRun && results.length > 0) { + this.firstRun = false; + this.$current = $(target.firstChild); + this.$current.addClass('current'); + if (this.selectFirstMatch) this.select(); + scrollIntoView(this.$current[0], this.$view[0]) + } + if (jQuery.browser.msie) this.$element[0].className += ''; + } + + this.open = function(src) { + this.frame.location.href = src; + if (this.frame.highlight) this.frame.highlight(src); + } + + this.select = function() { + this.open(this.$current.data('path')); + } + + this.move = function(isDown) { + if (!this.$current) return; + var $next = this.$current[isDown ? 'next' : 'prev'](); + if ($next.length) { + this.$current.removeClass('current'); + $next.addClass('current'); + scrollIntoView($next[0], this.$view[0]); + this.$current = $next; + } + return true; + } + + function renderItem(result) { + var li = document.createElement('li'), + html = '', badge = result.badge; + html += '

    ' + hlt(result.title); + if (result.params) html += '' + result.params + ''; + html += '

    '; + html += '

    '; + if (typeof badge != 'undefined') { + html += '' + escapeHTML(this.data.badges[badge] || 'unknown') + ''; + } + html += hlt(result.namespace) + '

    '; + if (result.snippet) html += '

    ' + escapeHTML(result.snippet) + '

    '; + li.innerHTML = html; + jQuery.data(li, 'path', result.path); + return li; + } + + function hlt(html) { + return escapeHTML(html).replace(/\u0001/g, '').replace(/\u0002/g, '') + } + + function escapeHTML(html) { + return html.replace(/[&<>]/g, function(c) { + return '&#' + c.charCodeAt(0) + ';'; + }); + } + +}); + +// tree.js ------------------------------------------------ + +Searchdoc.Tree = function(element, tree, panel) { + this.$element = $(element); + this.$list = $('ul', element); + this.tree = tree; + this.panel = panel; + this.init(); +} + +Searchdoc.Tree.prototype = $.extend({}, Searchdoc.Navigation, new function() { + this.init = function() { + var stopper = document.createElement('li'); + stopper.className = 'stopper'; + this.$list[0].appendChild(stopper); + for (var i=0, l = this.tree.length; i < l; i++) { + buildAndAppendItem.call(this, this.tree[i], 0, stopper); + }; + var _this = this; + this.$list.click(function(e) { + var $target = $(e.target), + $li = $target.closest('li'); + if ($target.hasClass('icon')) { + _this.toggle($li); + } else { + _this.select($li); + } + }) + + this.initNavigation(); + if (jQuery.browser.msie) document.body.className += ''; + } + + this.select = function($li) { + this.highlight($li); + var path = $li[0].searchdoc_tree_data.path; + if (path) this.panel.open(path); + } + + this.highlight = function($li) { + if (this.$current) this.$current.removeClass('current'); + this.$current = $li.addClass('current'); + } + + this.toggle = function($li) { + var closed = !$li.hasClass('closed'), + children = $li[0].searchdoc_tree_data.children; + $li.toggleClass('closed'); + for (var i=0, l = children.length; i < l; i++) { + toggleVis.call(this, $(children[i].li), !closed); + }; + } + + this.moveRight = function() { + if (!this.$current) { + this.highlight(this.$list.find('li:first')); + return; + } + if (this.$current.hasClass('closed')) { + this.toggle(this.$current); + } + } + + this.moveLeft = function() { + if (!this.$current) { + this.highlight(this.$list.find('li:first')); + return; + } + if (!this.$current.hasClass('closed')) { + this.toggle(this.$current); + } else { + var level = this.$current[0].searchdoc_tree_data.level; + if (level == 0) return; + var $next = this.$current.prevAll('li.level_' + (level - 1) + ':visible:first'); + this.$current.removeClass('current'); + $next.addClass('current'); + scrollIntoView($next[0], this.$element[0]); + this.$current = $next; + } + } + + this.move = function(isDown) { + if (!this.$current) { + this.highlight(this.$list.find('li:first')); + return true; + } + var next = this.$current[0]; + if (isDown) { + do { + next = next.nextSibling; + if (next && next.style && next.style.display != 'none') break; + } while(next); + } else { + do { + next = next.previousSibling; + if (next && next.style && next.style.display != 'none') break; + } while(next); + } + if (next && next.className.indexOf('stopper') == -1) { + this.$current.removeClass('current'); + $(next).addClass('current'); + scrollIntoView(next, this.$element[0]); + this.$current = $(next); + } + return true; + } + + function toggleVis($li, show) { + var closed = $li.hasClass('closed'), + children = $li[0].searchdoc_tree_data.children; + $li.css('display', show ? '' : 'none') + if (!show && this.$current && $li[0] == this.$current[0]) { + this.$current.removeClass('current'); + this.$current = null; + } + for (var i=0, l = children.length; i < l; i++) { + toggleVis.call(this, $(children[i].li), show && !closed); + }; + } + + function buildAndAppendItem(item, level, before) { + var li = renderItem(item, level), + list = this.$list[0]; + item.li = li; + list.insertBefore(li, before); + for (var i=0, l = item[3].length; i < l; i++) { + buildAndAppendItem.call(this, item[3][i], level + 1, before); + }; + return li; + } + + function renderItem(item, level) { + var li = document.createElement('li'), + cnt = document.createElement('div'), + h1 = document.createElement('h1'), + p = document.createElement('p'), + icon, i; + + li.appendChild(cnt); + li.style.paddingLeft = getOffset(level); + cnt.className = 'content'; + if (!item[1]) li.className = 'empty '; + cnt.appendChild(h1); + // cnt.appendChild(p); + h1.appendChild(document.createTextNode(item[0])); + // p.appendChild(document.createTextNode(item[4])); + if (item[2]) { + i = document.createElement('i'); + i.appendChild(document.createTextNode(item[2])); + h1.appendChild(i); + } + if (item[3].length > 0) { + icon = document.createElement('div'); + icon.className = 'icon'; + cnt.appendChild(icon); + } + + // user direct assignement instead of $() + // it's 8x faster + // $(li).data('path', item[1]) + // .data('children', item[3]) + // .data('level', level) + // .css('display', level == 0 ? '' : 'none') + // .addClass('level_' + level) + // .addClass('closed'); + li.searchdoc_tree_data = { + path: item[1], + children: item[3], + level: level + } + li.style.display = level == 0 ? '' : 'none'; + li.className += 'level_' + level + ' closed'; + return li; + } + + function getOffset(level) { + return 5 + 18*level + 'px'; + } +}); diff --git a/Sami/Resources/themes/enhanced/layout/page.twig b/Sami/Resources/themes/enhanced/layout/page.twig new file mode 100644 index 00000000..ddcb891f --- /dev/null +++ b/Sami/Resources/themes/enhanced/layout/page.twig @@ -0,0 +1,4 @@ +{% extends "default/layout/page.twig" %} + +{% block frame %} +{% endblock %} diff --git a/Sami/Resources/themes/enhanced/manifest.yml b/Sami/Resources/themes/enhanced/manifest.yml new file mode 100644 index 00000000..a92b0589 --- /dev/null +++ b/Sami/Resources/themes/enhanced/manifest.yml @@ -0,0 +1,18 @@ +name: enhanced +parent: default + +static: + 'css/main.css': 'css/main.css' + 'css/panel.css': 'css/panel.css' + 'css/reset.css': 'css/reset.css' + 'i/arrows.png': 'i/arrows.png' + 'i/loader.gif': 'i/loader.gif' + 'i/results_bg.png': 'i/results_bg.png' + 'i/tree_bg.png': 'i/tree_bg.png' + 'js/jquery-1.3.2.min.js': 'js/jquery-1.3.2.min.js' + 'js/searchdoc.js': 'js/searchdoc.js' + +global: + 'panel.twig': 'panel.html', + 'search_index.twig': 'search_index.js', + 'tree.twig': 'tree.js', diff --git a/Sami/Resources/themes/enhanced/panel.twig b/Sami/Resources/themes/enhanced/panel.twig new file mode 100644 index 00000000..89706b35 --- /dev/null +++ b/Sami/Resources/themes/enhanced/panel.twig @@ -0,0 +1,80 @@ + + + + {{ project.config('title') }} + + + + + + + + +
    +
    + loading... +
    +
    + +
    +
    +
      +
    +
    +
    +
      +
    +
    +
    + + diff --git a/Sami/Resources/themes/enhanced/search_index.twig b/Sami/Resources/themes/enhanced/search_index.twig new file mode 100644 index 00000000..5c759bab --- /dev/null +++ b/Sami/Resources/themes/enhanced/search_index.twig @@ -0,0 +1,39 @@ +{% from "macros.twig" import method_parameters_signature %} + +{%- autoescape false -%} +var search_data = { + 'index': { + 'searchIndex': {{ index['searchIndex']|json_encode }}, + 'info': [ + {%- for item in index['info'] -%} + [ + {%- if 1 == item[0] %} + {{- item[1].shortname|json_encode }}, + {{- item[1].namespace|json_encode }}, + {{- class_path(item[1])|json_encode }}, + {{- (item[1].parent ? ' < ' ~ item[1].parent.shortname : '')|json_encode }}, + {{- item[1].shortdesc|snippet|json_encode }}, + {{- 1 }} + {%- elseif 2 == item[0] %} + {{- (item[1].class.shortname ~ '::' ~ item[1].name)|json_encode }}, + {{- item[1].class.name|json_encode }}, + {{- method_path(item[1])|json_encode }}, + {{- method_parameters_signature(item[1])|json_encode }}, + {{- item[1].shortdesc|snippet|json_encode }}, + {{- 2 }} + {%- elseif 3 == item[0] %} + {{- item[1]|json_encode }}, + {{- '""' }}, + {{- namespace_path(item[1])|json_encode }}, + {{- '""' }}, + {{- '""' }}, + {{- 3 }} + {%- endif -%} + ] + {{- loop.last ? '' : ',' }} + {%- endfor -%} + ] + } +} +search_data['index']['longSearchIndex'] = search_data['index']['searchIndex'] +{%- endautoescape %} diff --git a/Sami/Resources/themes/enhanced/tree.twig b/Sami/Resources/themes/enhanced/tree.twig new file mode 100644 index 00000000..7e2292c2 --- /dev/null +++ b/Sami/Resources/themes/enhanced/tree.twig @@ -0,0 +1,17 @@ +var tree = {{ _self.element(tree) }} + +{% macro element(tree) %} + {%- autoescape false -%} + [ + {%- for element in tree -%} + [ + {{- element[0]|json_encode }}, + {{- (not element[2] ? class_path(element[1]) : namespace_path(element[1]))|json_encode }}, + {{- (not element[2] and element[1].parent ? ' < ' ~ element[1].parent.shortname : '')|json_encode }}, + {{ _self.element(element[2]) -}} + ] + {{- loop.last ? '' : ',' }} + {%- endfor %} + ] + {%- endautoescape %} +{% endmacro %} diff --git a/Sami/Sami.php b/Sami/Sami.php new file mode 100644 index 00000000..b1b9c2b1 --- /dev/null +++ b/Sami/Sami.php @@ -0,0 +1,157 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami; + +use Sami\Tree; +use Sami\Indexer; +use Sami\Parser\CodeParser; +use Sami\Parser\Parser; +use Sami\Parser\NodeVisitor; +use Sami\Parser\ParserContext; +use Sami\Parser\DocBlockParser; +use Sami\Parser\Filter\DefaultFilter; +use Sami\Parser\ClassTraverser; +use Sami\Parser\ClassVisitor; +use Sami\Store\JsonStore; +use Sami\Renderer\Renderer; +use Sami\Renderer\ThemeSet; +use Sami\Renderer\TwigExtension; +use Sami\Version\Version; +use Sami\Version\SingleVersionCollection; + +class Sami extends \Pimple +{ + const VERSION = '0.8'; + + public function __construct($iterator = null, array $config = array()) + { + $sc = $this; + + if (null !== $iterator) { + $this['files'] = $iterator; + } + + $this['_versions'] = $this->share(function () use ($sc) { + $versions = isset($sc['versions']) ? $sc['versions'] : $sc['version']; + + if (is_string($versions)) { + $versions = new Version($versions); + } + + if ($versions instanceof Version) { + $versions = new SingleVersionCollection($versions); + } + + return $versions; + }); + + $this['project'] = $this->share(function () use ($sc) { + return new Project($sc['_versions'], $sc['store'], $sc['parser'], $sc['renderer'], $sc['tree'], $sc['indexer'], $sc); + }); + + $this['parser'] = $this->share(function () use ($sc) { + return new Parser($sc['files'], $sc['store'], $sc['code_parser'], $sc['traverser']); + }); + + $this['indexer'] = $this->share(function () use ($sc) { + return new Indexer(); + }); + + $this['tree'] = $this->share(function () use ($sc) { + return new Tree(); + }); + + $this['parser_context'] = $this->share(function () use ($sc) { + return new ParserContext($sc['filter'], $sc['docblock_parser'], $sc['pretty_printer']); + }); + + $this['docblock_parser'] = $this->share(function () use ($sc) { + return new DocBlockParser(); + }); + + $this['php_parser'] = $this->share(function () { + return new \PHPParser_Parser(new \PHPParser_Lexer()); + }); + + $this['php_traverser'] = $this->share(function () use ($sc) { + $traverser = new \PHPParser_NodeTraverser(); + $traverser->addVisitor(new \PHPParser_NodeVisitor_NameResolver()); + $traverser->addVisitor(new NodeVisitor($sc['parser_context'])); + + return $traverser; + }); + + $this['code_parser'] = $this->share(function () use ($sc) { + return new CodeParser($sc['parser_context'], $sc['php_parser'], $sc['php_traverser']); + }); + + $this['pretty_printer'] = $this->share(function () use ($sc) { + return new \PHPParser_PrettyPrinter_Zend(); + }); + + $this['filter'] = $this->share(function () use ($sc) { + return new DefaultFilter(); + }); + + $this['store'] = $this->share(function () use ($sc) { + return new JsonStore(); + }); + + $this['renderer'] = $this->share(function () use ($sc) { + return new Renderer($sc['twig'], $sc['themes']); + }); + + $this['traverser'] = $this->share(function () use ($sc) { + $visitors = array( + new ClassVisitor\InheritdocClassVisitor(), + ); + + return new ClassTraverser($visitors); + }); + + $this['themes'] = $this->share(function () use ($sc) { + $templates = $sc['template_dirs']; + $templates[] = __DIR__.'/Resources/themes'; + + return new ThemeSet($templates); + }); + + $this['twig'] = $this->share(function () use ($sc) { + $twig = new \Twig_Environment(new \Twig_Loader_Filesystem(array('/')), array( + 'strict_variables' => true, + 'debug' => true, + 'auto_relad' => true, + 'cache' => false, + )); + $twig->addExtension(new TwigExtension()); + + return $twig; + }); + + $this['theme'] = 'enhanced'; + $this['title'] = 'API'; + $this['version'] = 'master'; + $this['template_dirs'] = array(); + $this['build_dir'] = getcwd().'/build'; + $this['cache_dir'] = getcwd().'/cache'; + + // simulate namespaces for projects based on the PEAR naming conventions + $this['simulate_namespaces'] = false; + + // include parent properties and methods on class pages + $this['include_parent_data'] = true; + + foreach ($config as $key => $value) { + $this[$key] = $value; + } + } +} diff --git a/Sami/Store/JsonStore.php b/Sami/Store/JsonStore.php new file mode 100644 index 00000000..8bd488f2 --- /dev/null +++ b/Sami/Store/JsonStore.php @@ -0,0 +1,84 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Store; + +use Sami\Reflection\ClassReflection; +use Sami\Project; +use Sami\Sami; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Filesystem\Filesystem; + +class JsonStore implements StoreInterface +{ + /** + * @return ReflectionClass A ReflectionClass instance + * + * @throws \InvalidArgumentException if the class does not exist in the store + */ + public function readClass(Project $project, $name) + { + if (!file_exists($this->getFilename($project, $name))) { + throw new \InvalidArgumentException(sprintf('File "%s" for class "%s" does not exist.', $this->getFilename($project, $name), $name)); + } + + return ClassReflection::fromArray($project, json_decode(file_get_contents($this->getFilename($project, $name)), true)); + } + + public function removeClass(Project $project, $name) + { + if (!file_exists($this->getFilename($project, $name))) { + throw new \RuntimeException(sprintf('Unable to remove the "%s" class.', $name)); + } + + unlink($this->getFilename($project, $name)); + } + + public function writeClass(Project $project, ClassReflection $class) + { + file_put_contents($this->getFilename($project, $class->getName()), json_encode($class->toArray())); + } + + public function readProject(Project $project) + { + $classes = array(); + foreach (Finder::create()->name('c_*.json')->in($this->getStoreDir($project)) as $file) { + $classes[] = ClassReflection::fromArray($project, json_decode(file_get_contents($file), true)); + } + + return $classes; + } + + public function flushProject(Project $project) + { + $filesystem = new Filesystem(); + $filesystem->remove($this->getStoreDir($project)); + } + + protected function getFilename($project, $name) + { + $dir = $this->getStoreDir($project); + + return $dir.'/c_'.md5($name).'.json'; + } + + protected function getStoreDir(Project $project) + { + $dir = $project->getCacheDir().'/store'; + + if (!is_dir($dir)) { + $filesystem = new Filesystem(); + $filesystem->mkdir($dir); + } + + return $dir; + } +} diff --git a/Sami/Store/StoreInterface.php b/Sami/Store/StoreInterface.php new file mode 100644 index 00000000..62c206e8 --- /dev/null +++ b/Sami/Store/StoreInterface.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Store; + +use Sami\Reflection\ClassReflection; +use Sami\Project; + +interface StoreInterface +{ + function readClass(Project $project, $name); + + function writeClass(Project $project, ClassReflection $class); + + function removeClass(Project $project, $name); + + function readProject(Project $project); + + function flushProject(Project $project); +} diff --git a/Sami/Tests/Parser/DocBlockParserTest.php b/Sami/Tests/Parser/DocBlockParserTest.php new file mode 100644 index 00000000..713da315 --- /dev/null +++ b/Sami/Tests/Parser/DocBlockParserTest.php @@ -0,0 +1,133 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Tests\Parser; + +use Sami\Parser\DocBlockParser; +use Sami\Parser\Node\DocBlockNode; + +class DocBlockParserTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getParseTests + */ + public function testParse($comment, $expected) + { + $parser = new DocBlockParser(); + + $this->assertEquals($this->createDocblock($expected), $parser->parse($comment)); + } + + public function getParseTests() + { + return array( + array(' + /** + */ + ', + array(), + ), + array(' + /** + * The short desc. + */ + ', + array('shortdesc' => 'The short desc.'), + ), + array('/** The short desc. */', + array('shortdesc' => 'The short desc.'), + ), + array(' + /** + * The short desc on two + * lines. + */ + ', + array('shortdesc' => 'The short desc on two lines.'), + ), + array(' + /** + * The short desc. + * + * And a long desc. + */ + ', + array('shortdesc' => 'The short desc.', 'longdesc' => 'And a long desc.'), + ), + array(' + /** + * The short desc on two + * lines. + * + * And a long desc on + * several lines too. + * + * With another paragraph. + */ + ', + array('shortdesc' => 'The short desc on two lines.', 'longdesc' => "And a long desc on\n several lines too.\n\n With another paragraph."), + ), + array(' + /** + * The short desc with a @tag embedded. And the long desc with a @tag embedded too. + */ + ', + array('shortdesc' => 'The short desc with a @tag embedded.', 'longdesc' => 'And the long desc with a @tag embedded too.'), + ), + array(' + /** + * @see http://symfony.com/ + */ + ', + array('tags' => array('see' => 'http://symfony.com/')), + ), + array(' + /** + * @author fabien@example.com + */ + ', + array('tags' => array('author' => 'fabien@example.com')), + ), + array(' + /** + * @author Fabien + * @author Thomas + */ + ', + array('tags' => array('author' => array('Fabien ', 'Thomas '))), + ), + ); + } + + private function createDocblock(array $elements) + { + $docblock = new DocBlockNode(); + foreach ($elements as $key => $value) { + switch ($key) { + case 'tags': + foreach ($value as $tag => $value) { + if (!is_array($value)) { + $value = array($value); + } + foreach ($value as $v) { + $docblock->addTag($tag, $v); + } + } + break; + default: + $method = 'set'.$key; + $docblock->$method($value); + } + } + + return $docblock; + } +} diff --git a/Sami/Tree.php b/Sami/Tree.php new file mode 100644 index 00000000..cd3aa2ca --- /dev/null +++ b/Sami/Tree.php @@ -0,0 +1,80 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami; + +use Sami\Project; + +class Tree +{ + public function getTree(Project $project) + { + $namespaces = array(); + $ns = $project->getConfig('simulate_namespaces') ? $project->getSimulatedNamespaces() : $project->getNamespaces(); + foreach ($ns as $namespace) { + if (false !== $pos = strpos($namespace, '\\')) { + $namespaces[substr($namespace, 0, strpos($namespace, '\\'))][] = $namespace; + } else { + $namespaces[$namespace][] = $namespace; + } + } + + return $this->generateClassTreeLevel($project, 1, $namespaces, array()); + } + + protected function generateClassTreeLevel(Project $project, $level, array $namespaces, array $classes) + { + ++$level; + + $tree = array(); + foreach ($namespaces as $namespace => $subnamespaces) { + // classes + if ($project->getConfig('simulate_namespaces')) { + $cl = $project->getSimulatedNamespaceAllClasses($namespace); + } else { + $cl = $project->getNamespaceAllClasses($namespace); + } + + // subnamespaces + $ns = array(); + foreach ($subnamespaces as $subnamespace) { + $parts = explode('\\', $subnamespace); + if (!isset($parts[$level - 1])) { + continue; + } + + $ns[implode('\\', array_slice($parts, 0, $level))][] = $subnamespace; + } + + $parts = explode('\\', $namespace); + $url = ''; + if (!$project->getConfig('simulate_namespaces')) { + $url = $parts[count($parts) - 1] && $project->hasNamespace($namespace) ? $namespace : ''; + } + $short = $parts[count($parts) - 1] ? $parts[count($parts) - 1] : '[Global Namespace]'; + + $tree[] = array($short, $url, $this->generateClassTreeLevel($project, $level, $ns, $cl)); + } + + foreach ($classes as $class) { + if ($project->getConfig('simulate_namespaces')) { + $parts = explode('_', $class->getShortName()); + $short = array_pop($parts); + } else { + $short = $class->getShortName(); + } + + $tree[] = array($short, $class, array()); + } + + return $tree; + } +} diff --git a/Sami/Version/GitVersionCollection.php b/Sami/Version/GitVersionCollection.php new file mode 100644 index 00000000..68d5b0f0 --- /dev/null +++ b/Sami/Version/GitVersionCollection.php @@ -0,0 +1,112 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Version; + +use Symfony\Component\Process\ProcessBuilder; +use Symfony\Component\Finder\Glob; + +class GitVersionCollection extends VersionCollection +{ + protected $sorter; + protected $filter; + protected $repo; + protected $gitPath; + + public function __construct($repo) + { + $this->repo = $repo; + $this->filter = function ($version) { + foreach (array('PR', 'RC', 'BETA', 'ALPHA') as $str) { + if (strpos($version, $str)) { + return false; + } + } + + return true; + }; + $this->sorter = function ($a, $b) { + return version_compare($a, $b, '>'); + }; + $this->gitPath = 'git'; + } + + protected function switchVersion(Version $version) + { + $this->execute(array('co', (string) $version)); + } + + public function setGitPath($path) + { + $this->gitPath = $gitPath; + } + + public function setFilter(\Closure $filter) + { + $this->filter = $filter; + } + + public function setSorter(\Closure $sorter) + { + $this->sorter = $sorter; + } + + public function addFromTags($filter = null) + { + $tags = array_filter(explode("\n", $this->execute(array('tag')))); + + $versions = array_filter($tags, $this->filter); + if (null !== $filter) { + if (!$filter instanceof \Closure) { + $regexes = array(); + foreach ((array) $filter as $f) { + $regexes[] = Glob::toRegex($f); + } + $filter = function ($version) use ($regexes) { + foreach ($regexes as $regex) { + if (preg_match($regex, $version)) { + return true; + } + } + + return false; + }; + } + + $versions = array_filter($versions, $filter); + } + usort($versions, $this->sorter); + + foreach ($versions as $version) { + $version = new Version($version); + $version->setFrozen(true); + $this->add($version); + } + + return $this; + } + + protected function execute($arguments) + { + array_unshift($arguments, $this->gitPath); + + $builder = new ProcessBuilder($arguments); + $builder->setWorkingDirectory($this->repo); + $process = $builder->getProcess(); + $process->run(); + + if (!$process->isSuccessful()) { + throw new \RuntimeException(sprintf('Unable to run the command (%s).', $process->getErrorOutput())); + } + + return $process->getOutput(); + } +} diff --git a/Sami/Version/SingleVersionCollection.php b/Sami/Version/SingleVersionCollection.php new file mode 100644 index 00000000..0ca17e2b --- /dev/null +++ b/Sami/Version/SingleVersionCollection.php @@ -0,0 +1,28 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Version; + +class SingleVersionCollection extends VersionCollection +{ + public function add($version, $longname = null) + { + if (count($this->versions)) { + throw new \LogicException('A SingleVersionCollection can only contain one Version'); + } + + parent::add($version, $longname); + } + + protected function switchVersion(Version $version) + { + } +} diff --git a/Sami/Version/Version.php b/Sami/Version/Version.php new file mode 100644 index 00000000..33d97d28 --- /dev/null +++ b/Sami/Version/Version.php @@ -0,0 +1,56 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Version; + +class Version +{ + protected $isFrozen; + protected $name; + protected $longname; + + public function __construct($name, $longname = null) + { + $this->name = $name; + $this->longname = null === $longname ? $name : $longname; + $this->isFrozen = false; + } + + public function __toString() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getName() + { + return $this->name; + } + + public function getLongName() + { + return $this->longname; + } + + public function setFrozen($isFrozen) + { + $this->isFrozen = (Boolean) $isFrozen; + } + + public function isFrozen() + { + return $this->isFrozen; + } +} diff --git a/Sami/Version/VersionCollection.php b/Sami/Version/VersionCollection.php new file mode 100644 index 00000000..abfe8ed7 --- /dev/null +++ b/Sami/Version/VersionCollection.php @@ -0,0 +1,98 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Sami\Version; + +use Sami\Project; + +abstract class VersionCollection implements \Iterator, \Countable +{ + protected $versions; + protected $indice; + protected $project; + + public function __construct($versions) + { + $this->add($versions); + } + + abstract protected function switchVersion(Version $version); + + static public function create() + { + $r = new \ReflectionClass(get_called_class()); + + return $r->newInstanceArgs(func_get_args()); + } + + public function setProject(Project $project) + { + $this->project = $project; + } + + public function add($version, $longname = null) + { + if (is_array($version)) { + foreach ($version as $v) { + $this->add($v); + } + } else { + if (!$version instanceof Version) { + $version = new Version($version, $longname); + } + + $this->versions[] = $version; + } + + return $this; + } + + public function getVersions() + { + return $this->versions; + } + + public function key() + { + return $this->indice; + } + + public function current() + { + return $this->versions[$this->indice]; + } + + public function next() + { + ++$this->indice; + } + + public function rewind() + { + $this->indice = 0; + } + + public function valid() + { + if ($this->indice < count($this->versions)) { + $this->switchVersion($this->current()); + + return true; + } + + return false; + } + + public function count() + { + return count($this->versions); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..8d25deb0 --- /dev/null +++ b/composer.json @@ -0,0 +1,30 @@ +{ + "name": "sami/sami", + "type": "application", + "description": "Sami, an API documentation generator", + "keywords": ["phpdoc"], + "homepage": "http://sami.sensiolabs.org", + "version": "1.0.0", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "require": { + "php": ">=5.3.0", + "pimple/pimple": "1.0.0", + "twig/twig": "master-dev", + "nikic/php-parser": "master-dev", + "symfony/console": "2.1.*", + "symfony/finder": "2.1.*", + "symfony/filesystem": "2.1.*", + "symfony/yaml": "2.1.*", + "symfony/process": "2.1.*" + }, + "autoload": { + "psr-0": { "Sami": "." } + }, + "bin": ["sami.php"] +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..2e997034 --- /dev/null +++ b/composer.lock @@ -0,0 +1,95 @@ +{ + "hash": "65e1369b903ec2f643dc46401208c0a9", + "packages": [ + { + "package": "nikic/php-parser", + "version": "dev-master", + "source-reference": "3f66b7ecacfd5e88a40341fd44694276e413ec64" + }, + { + "package": "pimple/pimple", + "version": "1.0.0" + }, + { + "package": "symfony/console", + "version": "dev-master", + "source-reference": "ec3d45fafea157507b7dce339ce5a7c29de7642d", + "alias-pretty-version": "2.1.x-dev", + "alias-version": "2.1.9999999.9999999-dev" + }, + { + "package": "symfony/console", + "version": "dev-master", + "source-reference": "ec3d45fafea157507b7dce339ce5a7c29de7642d" + }, + { + "package": "symfony/filesystem", + "version": "dev-master", + "source-reference": "2af6b95879d54296964db46b567533d3e91ac8e2", + "alias-pretty-version": "2.1.x-dev", + "alias-version": "2.1.9999999.9999999-dev" + }, + { + "package": "symfony/filesystem", + "version": "dev-master", + "source-reference": "2af6b95879d54296964db46b567533d3e91ac8e2" + }, + { + "package": "symfony/finder", + "version": "dev-master", + "source-reference": "3c9379f3726401a1faf274204bd0ef3f5b24258a", + "alias-pretty-version": "2.1.x-dev", + "alias-version": "2.1.9999999.9999999-dev" + }, + { + "package": "symfony/finder", + "version": "dev-master", + "source-reference": "3c9379f3726401a1faf274204bd0ef3f5b24258a" + }, + { + "package": "symfony/process", + "version": "dev-master", + "source-reference": "106f2db51cecf2fe5ee59e428b8f4a918c4034fa" + }, + { + "package": "symfony/process", + "version": "dev-master", + "source-reference": "106f2db51cecf2fe5ee59e428b8f4a918c4034fa", + "alias-pretty-version": "2.1.x-dev", + "alias-version": "2.1.9999999.9999999-dev" + }, + { + "package": "symfony/yaml", + "version": "dev-master", + "source-reference": "2b7a2e067f250bf5fbdcab5d2c8679a085f1693c", + "alias-pretty-version": "2.1.x-dev", + "alias-version": "2.1.9999999.9999999-dev" + }, + { + "package": "symfony/yaml", + "version": "dev-master", + "source-reference": "2b7a2e067f250bf5fbdcab5d2c8679a085f1693c" + }, + { + "package": "twig/twig", + "version": "dev-master", + "source-reference": "d661c2a2f654ba8f2bddeeac0b8a80dcd59649ab" + }, + { + "package": "twig/twig", + "version": "dev-master", + "source-reference": "d661c2a2f654ba8f2bddeeac0b8a80dcd59649ab", + "alias-pretty-version": "1.9.x-dev", + "alias-version": "1.9.9999999.9999999-dev" + } + ], + "packages-dev": null, + "aliases": [ + + ], + "minimum-stability": "dev", + "stability-flags": { + "twig/twig": 20, + "nikic/php-parser": 20 + } +} diff --git a/examples/sf2.php b/examples/sf2.php new file mode 100644 index 00000000..7cc7d658 --- /dev/null +++ b/examples/sf2.php @@ -0,0 +1,29 @@ +files() + ->name('*.php') + ->exclude('Resources') + ->exclude('Tests') + ->in($dir = '/path/to/symfony/src') +; + +$versions = GitVersionCollection::create($dir) + ->addFromTags('v2.0.*') + ->add('2.0', '2.0 branch') + ->add('master', 'master branch') +; + +return new Sami($iterator, array( + 'theme' => 'symfony', + 'versions' => $versions, + 'title' => 'Symfony2 API', + 'build_dir' => __DIR__.'/../build/sf2/%version%', + 'cache_dir' => __DIR__.'/../cache/sf2/%version%', + 'template_dirs' => array(__DIR__.'/themes/symfony'), + 'default_opened_level' => 2, +)); diff --git a/examples/swiftmailer.php b/examples/swiftmailer.php new file mode 100644 index 00000000..483a2e7c --- /dev/null +++ b/examples/swiftmailer.php @@ -0,0 +1,26 @@ +files() + ->name('*.php') + ->in($dir = '/path/to/swiftmailer/lib/classes') +; + +$versions = GitVersionCollection::create($dir) + ->addFromTags(function ($version) { return preg_match('/^v?4\.\d+\.\d+$/', $version); }) + ->add('master', 'master branch') +; + +return new Sami($iterator, array( + 'theme' => 'enhanced', + 'versions' => $versions, + 'title' => 'Swiftmailer API', + 'build_dir' => __DIR__.'/../build/swiftmailer/%version%', + 'cache_dir' => __DIR__.'/../cache/swiftmailer/%version%', + 'simulate_namespaces' => true, + 'default_opened_level' => 1, +)); diff --git a/examples/twig.php b/examples/twig.php new file mode 100644 index 00000000..419a023e --- /dev/null +++ b/examples/twig.php @@ -0,0 +1,10 @@ + 'enhanced', + 'title' => 'Twig 1.6 API', + 'build_dir' => __DIR__.'/../build/twig', + 'cache_dir' => __DIR__.'/../cache/twig', + 'simulate_namespaces' => true, + 'default_opened_level' => 1, +)); diff --git a/examples/zf2.php b/examples/zf2.php new file mode 100644 index 00000000..bcf4d815 --- /dev/null +++ b/examples/zf2.php @@ -0,0 +1,18 @@ +files() + ->name('*.php') + ->in('/path/to/zf2/library') +; + +return new Sami($iterator, array( + 'title' => 'ZF2 API (for master)', + 'theme' => 'enhanced', + 'build_dir' => __DIR__.'/../build/zf2', + 'cache_dir' => __DIR__.'/../cache/zf2', + 'include_parent_data' => false, +)); diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..7c4e139d --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + + ./Sami/Tests + + + diff --git a/sami.php b/sami.php new file mode 100755 index 00000000..7102b0a5 --- /dev/null +++ b/sami.php @@ -0,0 +1,14 @@ +#!/usr/bin/env php +run();