diff --git a/composer.json b/composer.json index 594c8c7..a0cf1af 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "require": { "php": ">=5.4", "erusev/parsedown": "~1.0", - "league/plates": "~3.1" + "league/plates": "~3.1", + "guzzlehttp/guzzle": "~5.3" }, "autoload": { "psr-4": {"Todaymade\\Daux\\": "libs/"} diff --git a/composer.lock b/composer.lock index 542feec..8be7745 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "183de90aad5bcf722f046eb01453cdc0", + "hash": "82c1c29dbd870c4ee2ea79c0561ea5d8", "packages": [ { "name": "erusev/parsedown", @@ -45,6 +45,165 @@ ], "time": "2014-05-14 10:14:49" }, + { + "name": "guzzlehttp/guzzle", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "f3c8c22471cb55475105c14769644a49c3262b93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f3c8c22471cb55475105c14769644a49c3262b93", + "reference": "f3c8c22471cb55475105c14769644a49c3262b93", + "shasum": "" + }, + "require": { + "guzzlehttp/ringphp": "^1.1", + "php": ">=5.4.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.0", + "psr/log": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2015-05-20 03:47:55" + }, + { + "name": "guzzlehttp/ringphp", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/RingPHP.git", + "reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/RingPHP/zipball/dbbb91d7f6c191e5e405e900e3102ac7f261bc0b", + "reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b", + "shasum": "" + }, + "require": { + "guzzlehttp/streams": "~3.0", + "php": ">=5.4.0", + "react/promise": "~2.0" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "ext-curl": "Guzzle will use specific adapters if cURL is present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Ring\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.", + "time": "2015-05-20 03:37:09" + }, + { + "name": "guzzlehttp/streams", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/streams.git", + "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/streams/zipball/47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", + "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Provides a simple abstraction over streams of data", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "stream" + ], + "time": "2014-10-12 19:18:40" + }, { "name": "league/plates", "version": "3.1.0", @@ -96,6 +255,50 @@ "views" ], "time": "2014-10-21 12:06:12" + }, + { + "name": "react/promise", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "365fcee430dfa4ace1fbc75737ca60ceea7eeeef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/365fcee430dfa4ace1fbc75737ca60ceea7eeeef", + "reference": "365fcee430dfa4ace1fbc75737ca60ceea7eeeef", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@googlemail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "time": "2014-12-30 13:32:42" } ], "packages-dev": [], diff --git a/generate.php b/generate.php index e7766b5..5099db7 100644 --- a/generate.php +++ b/generate.php @@ -67,9 +67,22 @@ require_once("vendor/autoload.php"); \Todaymade\Daux\Daux::initConstants(); -$global_config = (isset($argv[1]))? $argv[1] : null; -$destination = (isset($argv[2]))? $argv[2] : null; +$rules = [ + 'config|c-s' => 'Configuration file', + 'format|f-s' => 'Output format, html or confluence (default:html)', + + //HTML + 'destination|d-s' => 'Destination folder, relative to the working directory (default:static)', +]; + +$options = new \Todaymade\Daux\Generator\Getopt($rules, $argv); + +$default = [ + 'config' => null, + 'format' => 'html', + 'destination' => null, +]; $generator = new \Todaymade\Daux\Generator\Generator(); -$generator->generate($global_config, $destination); +$generator->generate($options->getOptions() + $default); diff --git a/libs/DauxHelper.php b/libs/DauxHelper.php index aca6901..cee077a 100644 --- a/libs/DauxHelper.php +++ b/libs/DauxHelper.php @@ -50,6 +50,21 @@ class DauxHelper return $theme; } + public static function getCleanPath($path) { + $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path); + $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen'); + $absolutes = array(); + foreach ($parts as $part) { + if ('.' == $part) continue; + if ('..' == $part) { + array_pop($absolutes); + } else { + $absolutes[] = $part; + } + } + return implode(DIRECTORY_SEPARATOR, $absolutes); + } + public static function pathinfo($path) { preg_match('%^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$%im', $path, $m); diff --git a/libs/Format/Confluence/Api.php b/libs/Format/Confluence/Api.php new file mode 100644 index 0000000..1e3f6d5 --- /dev/null +++ b/libs/Format/Confluence/Api.php @@ -0,0 +1,90 @@ +base_url = $base_url; + $this->user = $user; + $this->pass = $pass; + $this->setSpace($space_id); + } + + public function setSpace($space_id) { + $this->space = $space_id; + } + + /** + * /rest/api/content/{id}/child/{type} + * + * @param $rootPage + * @return mixed + */ + public function getHierarchy($rootPage) { + $hiera = $this->getClient()->get("content/$rootPage/child/page?expand=version,body.storage")->json(); + + $children = []; + foreach($hiera['results'] as $result) { + $children[$result['title']] = [ + "id" => $result['id'], + "title" => $result['title'], + "version" => $result['version']['number'], + "content" => $result['body']['storage']['value'], + "children" => $this->getHierarchy($result['id']) + ]; + } + + return $children; + } + + public function createPage($parent_id, $title, $content) { + + $body = [ + 'type' => 'page', + 'space' => ['key' => $this->space], + 'ancestors' => [['type' => 'page', 'id' => $parent_id]], + 'title' => $title, + 'body' => ['storage' => ['value' => $content, 'representation' => 'storage']] + ]; + + $response = $this->getClient()->post('content', [ 'json' => $body ])->json(); + + return $response['id']; + } + + public function updatePage($parent_id, $page_id, $newVersion, $title, $content) { + $body = [ + 'type' => 'page', + 'space' => ['key' => $this->space], + 'ancestors' => [['type' => 'page', 'id' => $parent_id]], + 'version' => ['number' => $newVersion, "minorEdit" => false], + 'title' => $title, + 'body' => ['storage' => ['value' => $content, 'representation' => 'storage']] + ]; + + $this->getClient()->put("content/$page_id", [ 'json' => $body ])->json(); + } + + public function deletePage($page_id) { + return $this->getClient()->delete('content/' . $page_id)->json(); + } + + protected function getClient() { + + $options = [ + 'base_url' => $this->base_url . 'rest/api/', + 'defaults' => [ + 'auth' => [$this->user, $this->pass] + ] + ]; + + return new Client($options); + } +} diff --git a/libs/Format/Confluence/Generator.php b/libs/Format/Confluence/Generator.php new file mode 100644 index 0000000..0592fba --- /dev/null +++ b/libs/Format/Confluence/Generator.php @@ -0,0 +1,150 @@ +getParams()['confluence']; + + $this->prefix = trim($confluence['prefix']) . " "; + + $main_title = $this->prefix . $daux->getParams()['title']; + + $params = $daux->getParams(); + + echo "Generating Tree...\n"; + $tree = $this->generateRecursive($daux->tree, $params); + $tree['title'] = $main_title; + + echo "Getting already published pages...\n"; + $this->client = new Api($confluence['base_url'], $confluence['user'], $confluence['pass'], $confluence['space_id']); + $all_published = $this->client->getHierarchy($confluence['ancestor_id']); + + echo "Finding Root Page...\n"; + $published = []; + foreach ($all_published as $page) { + if ($page['title'] == $main_title) { + $published = $page; + break; + } + } + + echo "Create placeholder pages...\n"; + $published = $this->createRecursive($confluence['ancestor_id'], $tree, $published); + + echo "Publishing updates...\n"; + $this->updateRecursive($confluence['ancestor_id'], $tree, $published); + + echo "Done !\n"; + } + + private function createRecursive($parent_id, $entry, $published) + { + //TODO :: remove deleted pages + + if (!array_key_exists('id', $published)) { + if (array_key_exists('page', $entry)) { + echo "Creating: " . $entry['file']->getUrl() . "\n"; + $published['id'] = $this->client->createPage($parent_id, $entry['title'], "The content will come very soon !"); + } else { + echo "Creating Placeholder page: " . $entry['title'] . "\n"; + $published['id'] = $this->client->createPage($parent_id, $entry['title'], ""); + } + } + + if (array_key_exists('children', $entry)) { + foreach($entry['children'] as $child) { + $pub = []; + if (array_key_exists('children', $published) && array_key_exists($child['title'], $published['children'])) { + $pub = $published['children'][$child['title']]; + } + + $published['children'][$child['title']] = $this->createRecursive($published['id'], $child, $pub); + } + } + + return $published; + } + + private function updateRecursive($parent_id, $entry, $published) + { + if (array_key_exists('id', $published) && array_key_exists('page', $entry)) { + echo "Updating: " . $entry['file']->getUrl() . "\n"; + try { + $this->client->updatePage( + $parent_id, + $published['id'], + $published['version'] + 1, + $entry['title'], + $entry['page']->getContent() + ); + } catch (ClientException $e) { + echo "-> Failed with message: " . $e->getResponse()->json()['message'] . "\n"; + } + } + + if (array_key_exists('children', $entry)) { + foreach($entry['children'] as $child) { + $pub = []; + if (array_key_exists('children', $published) && array_key_exists($child['title'], $published['children'])) { + $pub = $published['children'][$child['title']]; + } + + $this->updateRecursive($published['id'], $child, $pub); + } + } + } + + private function generateRecursive(Entry $tree, array $params, $base_url = '') + { + $final = ['title' => $this->prefix . $tree->getTitle()]; + $params['base_url'] = $params['base_page'] = $base_url; + + $params['image'] = str_replace('', $base_url, $params['image']); + if ($base_url !== '') { + $params['entry_page'] = $tree->getFirstPage(); + } + foreach ($tree->value as $key => $node) { + if ($node instanceof Directory) { + $final['children'][$this->prefix . $node->getTitle()] = $this->generateRecursive($node, $params, '../' . $base_url); + } else if ($node instanceof Content) { + + $params['request'] = $node->getUrl(); + $params['file_uri'] = $node->getName(); + + $data = [ + 'title' => $this->prefix . $node->getTitle(), + 'file' => $node, + 'page' => MarkdownPage::fromFile($node, $params), + ]; + + if ($key == 'index.html') { + $final['title'] = $this->prefix . $tree->getTitle(); + $final['file'] = $node; + $final['page'] = $data['page']; + } else { + $final['children'][$data['title']] = $data; + } + } + } + + return $final; + } +} diff --git a/libs/Format/Confluence/MarkdownPage.php b/libs/Format/Confluence/MarkdownPage.php new file mode 100644 index 0000000..a7aa340 --- /dev/null +++ b/libs/Format/Confluence/MarkdownPage.php @@ -0,0 +1,50 @@ +]*src=['\"]([^\"]*)['\"][^>]*>/", + function ($matches) { + return str_replace($matches[1], $this->findImage($matches[1]), $matches[0]); + }, + $page + ); + + return $page; + } + + private function findImage($src) + { + //for protocol relative or http requests : keep the original one + if (substr($src, 0, strlen("http")) === "http" || substr($src, 0, strlen("//")) === "//") { + return $src; + } + + //Get the path to the file, relative to the root of the documentation + $url = DauxHelper::getCleanPath(dirname($this->file->getUrl()) . '/' . $src); + + //Get any file corresponding to the right one + $file = DauxHelper::getFile($this->params['tree'], $url); + + + if ($file === false) { + return $src; + } + + $encoded = base64_encode(file_get_contents($file->getPath())); + $extension = pathinfo($file->getPath(), PATHINFO_EXTENSION); + + if (!in_array($extension, ['jpg', 'jpeg', 'png', 'gif'])) { + return $src; + } + + return "data:image/$extension;base64,$encoded"; + } +} diff --git a/libs/Format/HTML/Generator.php b/libs/Format/HTML/Generator.php new file mode 100644 index 0000000..6a4b1ea --- /dev/null +++ b/libs/Format/HTML/Generator.php @@ -0,0 +1,60 @@ +getParams(); + if (is_null($destination)) { + $destination = $daux->local_base . DS . 'static'; + } + + echo "Copying Static assets ...\n"; + Helper::copyAssets($destination, $daux->local_base); + + echo "Generating ...\n"; + $this->generateRecursive($daux->tree, $destination, $params); + echo "Done !\n"; + } + + private function generateRecursive($tree, $output_dir, $params, $base_url = '') + { + $params['base_url'] = $params['base_page'] = $base_url; + + // Rebase Theme + $params['theme'] = DauxHelper::getTheme( + $params['theme-name'], + $params['base_url'], + $params['local_base'], + $base_url + ); + + $params['image'] = str_replace('', $base_url, $params['image']); + if ($base_url !== '') { + $params['entry_page'] = $tree->getFirstPage(); + } + foreach ($tree->value as $key => $node) { + if ($node instanceof Directory) { + $new_output_dir = $output_dir . DS . $key; + @mkdir($new_output_dir); + $this->generateRecursive($node, $new_output_dir, $params, '../' . $base_url); + } elseif ($node instanceof Content) { + echo "- " . $node->getUrl() . "\n"; + $params['request'] = $node->getUrl(); + $params['file_uri'] = $node->getName(); + + $page = MarkdownPage::fromFile($node, $params); + file_put_contents($output_dir . DS . $key, $page->getContent()); + } else { + echo "- " . $node->getUrl() . "\n"; + copy($node->getPath(), $output_dir . DS . $key); + } + } + } +} diff --git a/libs/Generator/Generator.php b/libs/Generator/Generator.php index b87daa3..859a719 100644 --- a/libs/Generator/Generator.php +++ b/libs/Generator/Generator.php @@ -1,63 +1,24 @@ initialize($global_config); + $daux->initialize(null, $options['config']); - $params = $daux->getParams(); - if (is_null($destination)) { - $destination = $daux->local_base . DS . 'static'; + switch(strtolower($options['format'])) { + case 'confluence': + (new ConfluenceGenerator())->generate($daux, $options['destination']); + break; + case 'html': + default: + (new HTMLGenerator())->generate($daux, $options['destination']); } - echo "Copying Static assets ...\n"; - Helper::copyAssets($destination, $daux->local_base); - - echo "Generating ...\n"; - $this->generateRecursive($daux->tree, $destination, $params); - echo "Done !\n"; - } - - private function generateRecursive($tree, $output_dir, $params, $base_url = '') - { - $params['base_url'] = $params['base_page'] = $base_url; - - // Rebase Theme - $params['theme'] = DauxHelper::getTheme( - $params['theme-name'], - $params['base_url'], - $params['local_base'], - $base_url - ); - - $params['image'] = str_replace('', $base_url, $params['image']); - if ($base_url !== '') { - $params['entry_page'] = $tree->getFirstPage(); - } - foreach ($tree->value as $key => $node) { - if ($node instanceof Directory) { - $new_output_dir = $output_dir . DS . $key; - @mkdir($new_output_dir); - $this->generateRecursive($node, $new_output_dir, $params, '../' . $base_url); - } elseif ($node instanceof Content) { - echo "- " . $node->getUrl() . "\n"; - $params['request'] = $node->getUrl(); - $params['file_uri'] = $node->getName(); - - $page = MarkdownPage::fromFile($node, $params); - file_put_contents($output_dir . DS . $key, $page->getContent()); - } else { - echo "- " . $node->getUrl() . "\n"; - copy($node->getPath(), $output_dir . DS . $key); - } - } } } diff --git a/libs/Generator/Getopt.php b/libs/Generator/Getopt.php new file mode 100644 index 0000000..eac8320 --- /dev/null +++ b/libs/Generator/Getopt.php @@ -0,0 +1,331 @@ +argv = $argv?: $_SERVER['argv']; + + $this->progname = $this->argv[0]; + $this->addRules($rules); + } + + /** + * Return a list of options that have been seen in the current argv. + * + * @return array + */ + public function getOptions() + { + $this->parse(); + return $this->options; + } + + /** + * Return the state of the option seen on the command line of the + * current application invocation. + * + * This function returns true, or the parameter value to the option, if any. + * If the option was not given, this function returns false. + * + * @param string $flag + * @return mixed + */ + public function getOption($flag) + { + $this->parse(); + + $flag = strtolower($flag); + + if (isset($this->ruleMap[$flag])) { + $flag = $this->ruleMap[$flag]; + if (isset($this->options[$flag])) { + return $this->options[$flag]; + } + } + return; + } + + /** + * Return a useful option reference, formatted for display in an + * error message. + * + * Note that this usage information is provided in most Exceptions + * generated by this class. + * + * @return string + */ + public function getUsageMessage() + { + $usage = "Usage: {$this->progname} [ options ]\n"; + $maxLen = 20; + $lines = array(); + foreach ($this->rules as $rule) { + if (isset($rule['isFreeformFlag'])) { + continue; + } + $flags = array(); + if (is_array($rule['alias'])) { + foreach ($rule['alias'] as $flag) { + $flags[] = (strlen($flag) == 1 ? '-' : '--') . $flag; + } + } + $linepart['name'] = implode('|', $flags); + if (isset($rule['param']) && $rule['param'] != 'none') { + $linepart['name'] .= '=""'; + switch ($rule['param']) { + case 'optional': + $linepart['name'] .= " (optional)"; + break; + case 'required': + $linepart['name'] .= " (required)"; + break; + } + } + if (strlen($linepart['name']) > $maxLen) { + $maxLen = strlen($linepart['name']); + } + $linepart['help'] = ''; + if (isset($rule['help'])) { + $linepart['help'] .= $rule['help']; + } + $lines[] = $linepart; + } + foreach ($lines as $linepart) { + $usage .= sprintf( + "%s %s\n", + str_pad($linepart['name'], $maxLen), + $linepart['help'] + ); + } + return $usage; + } + + /** + * Parse command-line arguments and find both long and short + * options. + * + * Also find option parameters, and remaining arguments after + * all options have been parsed. + * + * @return self + */ + public function parse() + { + if ($this->parsed === true) { + return $this; + } + + if (in_array('--help', $this->argv)) { + echo $this->getUsageMessage(); + exit; + } + + $this->options = array(); + + $long = []; + $short = ''; + foreach ($this->rules as $rule) { + foreach($rule['alias'] as $alias) { + + $prepared = $alias; + if ($rule['param'] == 'optional') { + $prepared .= '::'; + } elseif ($rule['param'] == 'required') { + $prepared .= ':'; + } + + if (strlen($alias) == 1) { + $short .= $prepared; + } else { + $long[] = $prepared; + } + } + } + + $result = getopt($short, $long); + + foreach ($result as $key => $value) { + $this->options[$this->ruleMap[$key]] = $value; + } + + $this->parsed = true; + + return $this; + } + + /** + * Define legal options using the Zend-style format. + * + * @param array $rules + * @throws InvalidArgumentException + */ + protected function addRules($rules) + { + foreach ($rules as $ruleCode => $helpMessage) { + // this may have to translate the long parm type if there + // are any complaints that =string will not work (even though that use + // case is not documented) + if (in_array(substr($ruleCode, -2, 1), array('-', '='))) { + $flagList = substr($ruleCode, 0, -2); + $delimiter = substr($ruleCode, -2, 1); + } else { + $flagList = $ruleCode; + $delimiter = $paramType = null; + } + + $flagList = strtolower($flagList); + + $flags = explode('|', $flagList); + $rule = array(); + $mainFlag = $flags[0]; + foreach ($flags as $flag) { + if (empty($flag)) { + throw new InvalidArgumentException("Blank flag not allowed in rule \"$ruleCode\"."); + } + + if (isset($this->ruleMap[$flag]) || (strlen($flag) != 1 && isset($this->rules[$flag]))) { + throw new InvalidArgumentException("Option \"-$flag\" is being defined more than once."); + } + + $this->ruleMap[$flag] = $mainFlag; + $rule['alias'][] = $flag; + } + $rule['param'] = 'none'; + if (isset($delimiter)) { + $rule['param'] = $delimiter == self::PARAM_REQUIRED? 'required' : 'optional'; + } + + $rule['help'] = $helpMessage; + $this->rules[$mainFlag] = $rule; + } + } +}