├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bin └── bookdown ├── composer.json ├── phpunit.php ├── phpunit.xml.dist ├── src ├── Command.php ├── Config │ ├── ConfigFactory.php │ ├── IndexConfig.php │ └── RootConfig.php ├── Container.php ├── Content │ ├── Heading.php │ ├── HeadingFactory.php │ ├── IndexPage.php │ ├── Page.php │ ├── PageFactory.php │ ├── RootPage.php │ ├── TocHeading.php │ └── TocHeadingIterator.php ├── Exception.php ├── Fsio.php ├── Process │ ├── Conversion │ │ ├── ConversionProcess.php │ │ └── ConversionProcessBuilder.php │ ├── CopyImage │ │ ├── CopyImageProcess.php │ │ └── CopyImageProcessBuilder.php │ ├── Copyright │ │ ├── CopyrightProcess.php │ │ └── CopyrightProcessBuilder.php │ ├── Headings │ │ ├── HeadingsProcess.php │ │ └── HeadingsProcessBuilder.php │ ├── Index │ │ ├── IndexProcess.php │ │ └── IndexProcessBuilder.php │ ├── ProcessBuilderInterface.php │ ├── ProcessInterface.php │ ├── Rendering │ │ ├── RenderingProcess.php │ │ └── RenderingProcessBuilder.php │ └── Toc │ │ ├── TocProcess.php │ │ └── TocProcessBuilder.php ├── Service │ ├── Collector.php │ ├── Processor.php │ ├── ProcessorBuilder.php │ ├── Service.php │ └── Timer.php └── Stdlog.php ├── templates ├── body.php ├── core.php ├── head.php ├── main.php ├── navfooter.php ├── navheader.php └── toc.php └── tests ├── BookFixture.php ├── BookImageFixture.php ├── BookNumberingFixture.php ├── BookTocFixture.php ├── CommandTest.php ├── Config ├── ConfigFactoryTest.php ├── IndexConfigTest.php └── RootConfigTest.php ├── ContainerTest.php ├── Content ├── ContentTest.php └── HeadingTest.php ├── FakeFsio.php ├── FsioTest.php ├── Process ├── ConversionProcessTest.php ├── CopyImageProcessTest.php ├── Fake │ ├── FakeProcess.php │ └── FakeProcessUnimplementedBuilder.php ├── HeadingsProcessTest.php ├── IndexProcessTest.php ├── RenderingProcessTest.php └── TocProcessTest.php └── Service ├── CollectorTest.php ├── ProcessorBuilderTest.php └── ProcessorTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | composer.lock 3 | vendor/ 4 | _site/ 5 | tests/tmp 6 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: ["src/*"] 3 | tools: 4 | external_code_coverage: true 5 | php_code_coverage: true 6 | php_sim: true 7 | php_mess_detector: true 8 | php_pdepend: true 9 | php_analyzer: true 10 | php_cpd: true 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: php 3 | php: 4 | - 5.6 5 | - hhvm 6 | - 7.0 7 | - 7.1 8 | install: 9 | - composer self-update 10 | - composer install 11 | script: 12 | - ./vendor/bin/phpunit --coverage-clover=coverage.clover 13 | after_script: 14 | - wget https://scrutinizer-ci.com/ocular.phar 15 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/). 6 | 7 | Thanks to all [contributors](https://github.com/bookdown/Bookdown.Bookdown/graphs/contributors)! 8 | 9 | ## 1.1.1 10 | 11 | - Fixed a warning in TocHeading 12 | 13 | - ConversionProcess::readOrigin() now returns string, not null, when no origin; 14 | this soothes strictness in CommonMark. 15 | 16 | ## 1.1.0 17 | 18 | - Added "bookdown/themes" as a Composer dependency. This makes it easy to theme 19 | your Bookdown pages. 20 | 21 | - Improvements to how "href" and "id" attributed are handled. 22 | 23 | - Improvements to nested TOC lists. 24 | 25 | - Convert relative .md hrefs to .html, so that links to .md files will work in 26 | un-converted Markdown sources, but when converted to HTML by Bookdown the same 27 | links will point to the rendered HTML file. 28 | 29 | ## 1.0.0 30 | 31 | - Now buids a JSON search index file for Lunr, et al. 32 | 33 | - Fixed error with toc-depth headings on single pages. 34 | 35 | - Removed tocDepth=1 as a special case, and added Page::getLevel(). 36 | 37 | - Does not stop build process if some images are missing. 38 | 39 | - Fixed #48 by wrapping html content in special div. 40 | 41 | - Added support to disable numbering. 42 | 43 | - Replaced Monolog with an internal psr/log implementation (Stdlog). 44 | 45 | - Updated commonmark and webuni dependencies. 46 | 47 | - Dropped support for PHP 5.5.x and earlier. 48 | 49 | - Now complies with pds/skeleton. 50 | 51 | - Updated tests and docs. 52 | 53 | ## 1.0.0-beta1 54 | 55 | This is the first beta release, with numerous fixes, improvements, and 56 | additions. It is a "Google Beta" release; this means we are paying special 57 | attention to avoiding BC breaks, but one or two may be necessary. 58 | 59 | - Override values in bookdown.json are no longer relative to the bookdown.json 60 | directory. 61 | 62 | - International and multibyte characters are now rendered correctly (cf. #12, 63 | #13, #34, et al.). 64 | 65 | - Add UTF8 meta in the template header. 66 | 67 | - Added `--root-href` as a command-line option. 68 | 69 | - Added a "copy images" process to download/copy images to target path. 70 | 71 | - Added CommonMark extension support. 72 | 73 | - Added Markdown table support via webuni/commonmark-table-extension. 74 | 75 | - Added TOC depth support for multiple books and index pages. 76 | 77 | - Added "copyright" as a bookdown.json entry. 78 | 79 | - Fixed header id attributes to be valid for href anchors, making them 80 | compatible with Javascript and CSS. 81 | 82 | - Various updates to the README and other documentation. 83 | 84 | Thanks, as always, to all our contributors; special thanks to Sandro Keil, who 85 | delivered several important features in this release. 86 | 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are happy to review any contributions you want to make. When contributing, please: 4 | 5 | - Fork from, and compare pull requests against, the `1.x` branch. 6 | 7 | - Adhere to the [PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md). 8 | 9 | - Include tests. We strive for 100% test coverage at all times. 10 | 11 | - Describe any backwards-compatiblity breaking changes. The project is reaching stability, so BC breaks are very important to note. 12 | 13 | The time between submitting a contribution and its review may be extensive. Do not be discouraged if there is not immediate feedback. 14 | 15 | Thanks! 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2019, Paul M. Jones 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bookdown 2 | 3 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/bookdown/Bookdown.Bookdown/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/bookdown/Bookdown.Bookdown/?branch=master) 4 | [![Code Coverage](https://scrutinizer-ci.com/g/bookdown/Bookdown.Bookdown/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/bookdown/Bookdown.Bookdown/?branch=master) 5 | [![Build Status](https://scrutinizer-ci.com/g/bookdown/Bookdown.Bookdown/badges/build.png?b=master)](https://scrutinizer-ci.com/g/bookdown/Bookdown.Bookdown/build-status/master) 6 | 7 | Bookdown generates [DocBook](http://docbook.org)-like HTML output using [Markdown](http://daringfireball.net/projects/markdown/) and JSON files instead of XML. 8 | 9 | Bookdown is especially well-suited for publishing project documentation to GitHub Pages. 10 | 11 | Read more about it at . 12 | 13 | ## Current Work 14 | [tobiju/bookdown-bootswatch-templates](https://github.com/tobiju/bookdown-bootswatch-templates "Bootswatch styles and syntax highlighting") 15 | is now part of Bookdown. You can use it by setting the `"template": "bookdown/themes",` in your `bookdown.json` 16 | 17 | 18 | ## Templates 19 | 20 | This is a list of custom bookdown.io templates 21 | * [bdudelsack/bookdown-template](https://github.com/bdudelsack/bookdown-template "Template for the bookdown project using Bootstrap and HighlightJS") 22 | 23 | ## Tests 24 | 25 | To run the tests after `composer install`, issue `./vendor/bin/phpunit` at the package root. 26 | 27 | ## Todo 28 | 29 | (In no particular order.) 30 | 31 | - new `bookdown.json` elements 32 | 33 | - `"numbering"`: indicates how to number the pages at this level (decimal, upper-alpha, lower-alpha, upper-roman, lower-roman) 34 | 35 | - `"authors"`: name, note, email, and website of book authors 36 | 37 | - `"editors"`: name, note, email, and website of book editors 38 | 39 | - `"beforeToc"`: indicates a Markdown file to place on the index page before the TOC 40 | 41 | - `"afterToc"`: indicates a Markdown file to place on the index page after the TOC 42 | 43 | - `"subtitle"`: indicates a subtitle on an index page 44 | 45 | - navigational elements 46 | 47 | - sidebar of siblings at the current level 48 | 49 | - breadcrumb-trail of parents leading to the current page 50 | 51 | - features 52 | 53 | - Automatically add a "date/time generated" value to the root config object and display on the root page 54 | 55 | - Display authors, editors, etc. on root page 56 | 57 | - A command to take a PHPDocumentor structure.xml file and convert it to a Bookdown origin structure (Markdown files + bookdown.json files) 58 | 59 | - A process to rewrite links on generated pages (this is for books collected from multiple different sources, and for changing origin `*.md` links to target `*.html` links) 60 | 61 | - Pre-process and post-process behavior to copy and/or remove site files 62 | 63 | - Treat the root page as different from other indexes, allow it to be a nice "front page" for sites 64 | -------------------------------------------------------------------------------- /bin/bookdown: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | newCommand($GLOBALS); 28 | $code = $command(); 29 | exit($code); 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bookdown/bookdown", 3 | "type": "library", 4 | "description": "Provides DocBook-like rendering of Markdown files.", 5 | "keywords": [ 6 | "docbook", 7 | "markdown", 8 | "manual", 9 | "documentation", 10 | "static site" 11 | ], 12 | "homepage": "https://github.com/bookdown/Bookdown.Bookdown", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Bookdown.Bookdown Contributors", 17 | "homepage": "https://github.com/bookdown/Bookdown.Bookdown/contributors" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=5.6.0", 22 | "aura/cli": "~2.0", 23 | "aura/html": "~2.0", 24 | "aura/view": "~2.0", 25 | "league/commonmark": "~0.0", 26 | "psr/log": "~1.0", 27 | "bookdown/themes": "~1.0" 28 | }, 29 | "require-dev": { 30 | "webuni/commonmark-table-extension": "~0.6", 31 | "webuni/commonmark-attributes-extension": "~0.5", 32 | "phpunit/phpunit": "~5.0", 33 | "pds/skeleton": "~1.0" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Bookdown\\Bookdown\\": "src/" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Bookdown\\Bookdown\\": "tests/" 43 | } 44 | }, 45 | "bin": [ 46 | "bin/bookdown" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /phpunit.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests 5 | 6 | 7 | 8 | 9 | ./src 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Command.php: -------------------------------------------------------------------------------- 1 | context = $context; 69 | $this->logger = $logger; 70 | $this->service = $service; 71 | } 72 | 73 | /** 74 | * 75 | * Runs this command. 76 | * 77 | * @return int 78 | * 79 | */ 80 | public function __invoke() 81 | { 82 | try { 83 | list($rootConfigFile, $rootConfigOverrides) = $this->init(); 84 | $this->service->__invoke($rootConfigFile, $rootConfigOverrides); 85 | return 0; 86 | } catch (AnyException $e) { 87 | $this->logger->error($e->getMessage()); 88 | $code = $e->getCode() ? $e->getCode() : 1; 89 | return $code; 90 | } 91 | } 92 | 93 | /** 94 | * 95 | * Initializes this command. 96 | * 97 | * @return array The names of the root-level config file, and their 98 | * command-line option overrides. 99 | * 100 | */ 101 | protected function init() 102 | { 103 | $getopt = $this->context->getopt(array( 104 | 'template:', 105 | 'target:', 106 | 'root-href:' 107 | )); 108 | 109 | if ($getopt->hasErrors()) { 110 | $errors = $getopt->getErrors(); 111 | $error = array_shift($errors); 112 | throw $error; 113 | } 114 | 115 | $rootConfigFile = $getopt->get(1); 116 | if (! $rootConfigFile) { 117 | throw new Exception( 118 | "Please enter the path to a bookdown.json file as the first argument." 119 | ); 120 | } 121 | 122 | $rootConfigOverrides = $getopt->get(); 123 | return array($rootConfigFile, $rootConfigOverrides); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Config/ConfigFactory.php: -------------------------------------------------------------------------------- 1 | rootConfigOverrides = $rootConfigOverrides; 44 | } 45 | 46 | /** 47 | * 48 | * Returns a new index-level config object. 49 | * 50 | * @param string $file The path of the configuration file. 51 | * 52 | * @param string $data The contents of the configuration file. 53 | * 54 | * @return IndexConfig 55 | * 56 | */ 57 | public function newIndexConfig($file, $data) 58 | { 59 | return new IndexConfig($file, $data); 60 | } 61 | 62 | /** 63 | * 64 | * Returns a new root-level config object. 65 | * 66 | * @param string $file The path of the configuration file. 67 | * 68 | * @param string $data The contents of the configuration file. 69 | * 70 | * @return RootConfig 71 | * 72 | */ 73 | public function newRootConfig($file, $data) 74 | { 75 | $config = new RootConfig($file, $data); 76 | $config->setOverrides($this->rootConfigOverrides); 77 | return $config; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Config/IndexConfig.php: -------------------------------------------------------------------------------- 1 | initFile($file); 97 | $this->initJson($data); 98 | $this->init(); 99 | } 100 | 101 | /** 102 | * 103 | * Initializes this config object. 104 | * 105 | */ 106 | protected function init() 107 | { 108 | $this->initDir(); 109 | $this->initTitle(); 110 | $this->initContent(); 111 | $this->initTocDepth(); 112 | } 113 | 114 | /** 115 | * 116 | * Initializes the $file and $isRemote properties. 117 | * 118 | * @param string $file The path to the file. 119 | * 120 | */ 121 | protected function initFile($file) 122 | { 123 | $this->file = $file; 124 | $this->isRemote = strpos($file, '://') !== false; 125 | } 126 | 127 | /** 128 | * 129 | * Initializes the $dir property. 130 | * 131 | */ 132 | protected function initDir() 133 | { 134 | $this->dir = dirname($this->file) . DIRECTORY_SEPARATOR; 135 | } 136 | 137 | /** 138 | * 139 | * Initializes the $json property. 140 | * 141 | * @param string $data The contents of the config file. 142 | * 143 | * @throws Exception on error. 144 | * 145 | */ 146 | protected function initJson($data) 147 | { 148 | $this->json = json_decode($data); 149 | if (! $this->json) { 150 | throw new Exception("Malformed JSON in '{$this->file}'."); 151 | } 152 | } 153 | 154 | /** 155 | * 156 | * Initializes the $title property. 157 | * 158 | * @throws Exception on error. 159 | * 160 | */ 161 | protected function initTitle() 162 | { 163 | if (empty($this->json->title)) { 164 | throw new Exception("No title set in '{$this->file}'."); 165 | } 166 | $this->title = $this->json->title; 167 | } 168 | 169 | /** 170 | * 171 | * Initializes the $content property. 172 | * 173 | * @throws Exception on error. 174 | * 175 | */ 176 | protected function initContent() 177 | { 178 | $content = empty($this->json->content) 179 | ? array() 180 | : $this->json->content; 181 | 182 | if (! $content) { 183 | throw new Exception("No content listed in '{$this->file}'."); 184 | } 185 | 186 | if (! is_array($content)) { 187 | throw new Exception("Content must be an array in '{$this->file}'."); 188 | } 189 | 190 | foreach ($content as $key => $val) { 191 | $this->initContentItem($val); 192 | } 193 | } 194 | 195 | /** 196 | * 197 | * Initializes a $content property element from an origin location. 198 | * 199 | * @param mixed $origin An origin location for a content item. If an object, 200 | * it's an override filename and a page path; if a string, it's a page path 201 | * or a bookdown.json file pointing to another page index. 202 | * 203 | * @throws Exception on error. 204 | * 205 | */ 206 | protected function initContentItem($origin) 207 | { 208 | if (is_object($origin)) { 209 | $spec = (array) $origin; 210 | $name = key($spec); 211 | $origin = current($spec); 212 | return $this->addContent($name, $origin); 213 | } 214 | 215 | if (! is_string($origin)) { 216 | throw new Exception("Content origin must be object or string in '{$this->file}'."); 217 | } 218 | 219 | if (substr($origin, -13) == 'bookdown.json') { 220 | $name = basename(dirname($origin)); 221 | return $this->addContent($name, $origin); 222 | } 223 | 224 | $name = basename($origin); 225 | $pos = strrpos($name, '.'); 226 | if ($pos !== false) { 227 | $name = substr($name, 0, $pos); 228 | } 229 | return $this->addContent($name, $origin); 230 | } 231 | 232 | /** 233 | * 234 | * Initializes the $tocDepth property. 235 | * 236 | */ 237 | protected function initTocDepth() 238 | { 239 | $this->tocDepth = empty($this->json->tocDepth) 240 | ? 0 241 | : (int) $this->json->tocDepth; 242 | } 243 | 244 | /** 245 | * 246 | * Adds a $content item name and path. 247 | * 248 | * @param string $name The item name. 249 | * 250 | * @param string $origin The origin of the content item. 251 | * 252 | * @throws Exception on error. 253 | * 254 | */ 255 | protected function addContent($name, $origin) 256 | { 257 | if ($name == 'index') { 258 | throw new Exception("Disallowed 'index' content name in '{$this->file}'."); 259 | } 260 | 261 | if (isset($this->content[$name])) { 262 | throw new Exception("Content name '{$name}' already set in '{$this->file}'."); 263 | } 264 | 265 | $this->content[$name] = $this->fixPath($origin); 266 | } 267 | 268 | /** 269 | * 270 | * Returns a validated and sanitized content origin path. 271 | * 272 | * @param string $path The origin path. 273 | * 274 | * @return string 275 | * 276 | * @throws Exception on error. 277 | * 278 | */ 279 | protected function fixPath($path) 280 | { 281 | if (strpos($path, '://') !== false) { 282 | return $path; 283 | } 284 | 285 | $lead = substr($path, 0, 1); 286 | 287 | if ($this->isRemote() && $lead === DIRECTORY_SEPARATOR) { 288 | throw new Exception( 289 | "Cannot handle absolute content path '{$path}' in remote '{$this->file}'." 290 | ); 291 | } 292 | 293 | if ($lead === DIRECTORY_SEPARATOR) { 294 | return $path; 295 | } 296 | 297 | return $this->getDir() . ltrim($path, DIRECTORY_SEPARATOR); 298 | } 299 | 300 | /** 301 | * 302 | * Was this config file retrieved from a remote location? 303 | * 304 | * @return bool 305 | * 306 | */ 307 | public function isRemote() 308 | { 309 | return $this->isRemote; 310 | } 311 | 312 | /** 313 | * 314 | * The path to the config file. 315 | * 316 | * @return string 317 | * 318 | */ 319 | public function getFile() 320 | { 321 | return $this->file; 322 | } 323 | 324 | /** 325 | * 326 | * The directory of the config file. 327 | * 328 | * @return string 329 | * 330 | */ 331 | public function getDir() 332 | { 333 | return $this->dir; 334 | } 335 | 336 | /** 337 | * 338 | * The title for the index. 339 | * 340 | * @return string 341 | * 342 | */ 343 | public function getTitle() 344 | { 345 | return $this->title; 346 | } 347 | 348 | /** 349 | * 350 | * The pages (and sub-indexes) for the index. 351 | * 352 | * @return array 353 | * 354 | */ 355 | public function getContent() 356 | { 357 | return $this->content; 358 | } 359 | 360 | /** 361 | * 362 | * The TOC depth level for the index. 363 | * 364 | * @return string 365 | * 366 | */ 367 | public function getTocDepth() 368 | { 369 | return $this->tocDepth; 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/Container.php: -------------------------------------------------------------------------------- 1 | stdout = $stdout; 94 | $this->stderr = $stderr; 95 | $this->fsioClass = $fsioClass; 96 | } 97 | 98 | /** 99 | * 100 | * Returns a new Bookdown command object. 101 | * 102 | * @param array $globals Typically the PHP $GLOBALS array. 103 | * 104 | * @return Command 105 | * 106 | */ 107 | public function newCommand($globals) 108 | { 109 | return new Command( 110 | $this->getCliFactory()->newContext($globals), 111 | $this->getLogger(), 112 | $this->newService() 113 | ); 114 | } 115 | 116 | /** 117 | * 118 | * Returns a new Bookdown service layer object. 119 | * 120 | * @return Service\Service 121 | * 122 | */ 123 | public function newService() 124 | { 125 | return new Service\Service( 126 | $this->newCollector(), 127 | $this->newProcessorBuilder(), 128 | $this->newTimer() 129 | ); 130 | } 131 | 132 | /** 133 | * 134 | * Returns a new Bookdown page-collector. 135 | * 136 | * @return Service\Collector 137 | * 138 | */ 139 | public function newCollector() 140 | { 141 | return new Service\Collector( 142 | $this->getLogger(), 143 | $this->getFsio(), 144 | new Config\ConfigFactory(), 145 | new Content\PageFactory() 146 | ); 147 | } 148 | 149 | /** 150 | * 151 | * Returns a new Bookdown builder for processor objects. 152 | * 153 | * @return Service\ProcessorBuilder 154 | * 155 | */ 156 | public function newProcessorBuilder() 157 | { 158 | return new Service\ProcessorBuilder( 159 | $this->getLogger(), 160 | $this->getFsio() 161 | ); 162 | } 163 | 164 | /** 165 | * 166 | * Returns a new Bookdown timer. 167 | * 168 | * @return Service\Timer 169 | * 170 | */ 171 | public function newTimer() 172 | { 173 | return new Service\Timer($this->getLogger()); 174 | } 175 | 176 | /** 177 | * 178 | * Returns the shared CLI factory object. 179 | * 180 | * @return CliFactory 181 | * 182 | */ 183 | public function getCliFactory() 184 | { 185 | if (! $this->cliFactory) { 186 | $this->cliFactory = new CliFactory(); 187 | } 188 | return $this->cliFactory; 189 | } 190 | 191 | /** 192 | * 193 | * Returns the shared logger instance. 194 | * 195 | * @return LoggerInterface 196 | * 197 | */ 198 | public function getLogger() 199 | { 200 | if (! $this->logger) { 201 | $this->logger = new Stdlog($this->stdout, $this->stderr); 202 | } 203 | 204 | return $this->logger; 205 | } 206 | 207 | /** 208 | * 209 | * Returns the shared filesystem I/O object. 210 | * 211 | * @return Fsio 212 | * 213 | */ 214 | public function getFsio() 215 | { 216 | if (! $this->fsio) { 217 | $class = $this->fsioClass; 218 | $this->fsio = new $class(); 219 | } 220 | return $this->fsio; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Content/Heading.php: -------------------------------------------------------------------------------- 1 | number = $number; 91 | $this->title = $title; 92 | $this->href = $href; 93 | $this->id = $id; 94 | $this->level = substr_count($number, '.'); 95 | $this->hrefAnchor = $hrefAnchor; 96 | } 97 | 98 | /** 99 | * 100 | * Returns the heading number. 101 | * 102 | * @return string 103 | * 104 | */ 105 | public function getNumber() 106 | { 107 | return $this->number; 108 | } 109 | 110 | /** 111 | * 112 | * Returns the heading title. 113 | * 114 | * @return string 115 | * 116 | */ 117 | public function getTitle() 118 | { 119 | return $this->title; 120 | } 121 | 122 | /** 123 | * 124 | * Returns the ID attribute value. 125 | * 126 | * @return string 127 | * 128 | */ 129 | public function getId() 130 | { 131 | return $this->id; 132 | } 133 | 134 | /** 135 | * 136 | * Returns the HREF attribute value. 137 | * 138 | * @return string 139 | * 140 | */ 141 | public function getHref() 142 | { 143 | $href = $this->href; 144 | 145 | if (null !== $this->getId() && !strstr($href, '#')) { 146 | $href .= $this->getHrefAnchor(); 147 | } 148 | 149 | return $href; 150 | } 151 | 152 | /** 153 | * @return string 154 | */ 155 | public function getHrefAnchor() 156 | { 157 | if (null === $this->hrefAnchor) { 158 | $this->hrefAnchor = '#' . $this->getAnchor(); 159 | } 160 | 161 | return $this->hrefAnchor; 162 | } 163 | 164 | /** 165 | * 166 | * Return a valid anchor string tag to use as html id attribute. 167 | * 168 | * @return string|null 169 | * 170 | */ 171 | public function getAnchor() 172 | { 173 | $anchor = null; 174 | 175 | if (null !== $this->getNumber()) { 176 | $anchor = str_replace('.', '-', trim($this->getNumber(), '.')); 177 | } 178 | return $anchor; 179 | } 180 | 181 | /** 182 | * 183 | * Returns the TOC depth level for this heading. 184 | * 185 | * @return int 186 | * 187 | */ 188 | public function getLevel() 189 | { 190 | return $this->level; 191 | } 192 | 193 | /** 194 | * 195 | * Returns the properties of this heading as an array. 196 | * 197 | * @return array 198 | * 199 | */ 200 | public function asArray() 201 | { 202 | return get_object_vars($this); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/Content/HeadingFactory.php: -------------------------------------------------------------------------------- 1 | config = $config; 74 | $this->name = $name; 75 | $this->parent = $parent; 76 | $this->count = $count; 77 | $this->setTitle($config->getTitle()); 78 | } 79 | 80 | /** 81 | * 82 | * Returns the config object. 83 | * 84 | * @return IndexConfig 85 | * 86 | */ 87 | public function getConfig() 88 | { 89 | return $this->config; 90 | } 91 | 92 | /** 93 | * 94 | * Returns the href attribute for this page. 95 | * 96 | * @return IndexConfig 97 | * 98 | */ 99 | public function getHref() 100 | { 101 | $base = $this->getParent()->getHref(); 102 | return $base . $this->getName() . '/'; 103 | } 104 | 105 | /** 106 | * 107 | * Adds a child page covered by this index. 108 | * 109 | * @param Page $child The child page. 110 | * 111 | */ 112 | public function addChild(Page $child) 113 | { 114 | $this->children[] = $child; 115 | } 116 | 117 | /** 118 | * 119 | * Returns the child pages covered by this index. 120 | * 121 | * @return array 122 | * 123 | */ 124 | public function getChildren() 125 | { 126 | return $this->children; 127 | } 128 | 129 | /** 130 | * 131 | * Returns the target file path for output from this index. 132 | * 133 | * @return string 134 | * 135 | */ 136 | public function getTarget() 137 | { 138 | $base = rtrim( 139 | dirname($this->getParent()->getTarget()), 140 | DIRECTORY_SEPARATOR 141 | ); 142 | 143 | return $base 144 | . DIRECTORY_SEPARATOR . $this->getName() 145 | . DIRECTORY_SEPARATOR . 'index.html'; 146 | } 147 | 148 | /** 149 | * 150 | * Sets the TOC entries for this index. 151 | * 152 | * @param array $tocEntries The TOC entries. 153 | * 154 | */ 155 | public function setTocEntries(array $tocEntries) 156 | { 157 | $this->tocEntries = $tocEntries; 158 | } 159 | 160 | /** 161 | * 162 | * Does this index have any TOC entries? 163 | * 164 | * @return bool 165 | * 166 | */ 167 | public function hasTocEntries() 168 | { 169 | return (bool) $this->tocEntries; 170 | } 171 | 172 | /** 173 | * 174 | * Returns the TOC entries for this index. 175 | * 176 | * @return array 177 | * 178 | */ 179 | public function getTocEntries() 180 | { 181 | return $this->tocEntries; 182 | } 183 | 184 | 185 | /** 186 | * @param TocHeadingIterator $nestedTocEntries 187 | */ 188 | public function setNestedTocEntries(TocHeadingIterator $nestedTocEntries) 189 | { 190 | $this->nestedTocEntries = $nestedTocEntries; 191 | } 192 | 193 | /** 194 | * @return bool 195 | */ 196 | public function hasNestedTocEntries() 197 | { 198 | return (bool)$this->nestedTocEntries; 199 | } 200 | 201 | /** 202 | * @return TocHeadingIterator 203 | */ 204 | public function getNestedTocEntries() 205 | { 206 | return $this->nestedTocEntries; 207 | } 208 | 209 | /** 210 | * 211 | * Is this an index page? 212 | * 213 | * @return bool 214 | * 215 | */ 216 | public function isIndex() 217 | { 218 | return true; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/Content/Page.php: -------------------------------------------------------------------------------- 1 | origin = $origin; 121 | $this->name = $name; 122 | $this->parent = $parent; 123 | $this->count = $count; 124 | } 125 | 126 | /** 127 | * 128 | * Returns the name for this page. 129 | * 130 | * @return string 131 | * 132 | */ 133 | public function getName() 134 | { 135 | return $this->name; 136 | } 137 | 138 | /** 139 | * 140 | * Returns the origin path of this Page. 141 | * 142 | * @return string 143 | * 144 | */ 145 | public function getOrigin() 146 | { 147 | return $this->origin; 148 | } 149 | 150 | /** 151 | * 152 | * Sets the title of this Page. 153 | * 154 | * @param string $title The title of this page. 155 | * 156 | */ 157 | public function setTitle($title) 158 | { 159 | $this->title = $title; 160 | } 161 | 162 | /** 163 | * 164 | * Returns the title of this Page. 165 | * 166 | * @return string 167 | * 168 | */ 169 | public function getTitle() 170 | { 171 | return $this->title; 172 | } 173 | 174 | /** 175 | * 176 | * Does this Page have a parent? 177 | * 178 | * @return bool 179 | * 180 | */ 181 | public function hasParent() 182 | { 183 | return (bool) $this->parent; 184 | } 185 | 186 | /** 187 | * 188 | * Returns the parent page, if any. 189 | * 190 | * @return IndexPage|null 191 | * 192 | */ 193 | public function getParent() 194 | { 195 | return $this->parent; 196 | } 197 | 198 | /** 199 | * 200 | * Returns this page's position in the TOC at this level. 201 | * 202 | * @return int 203 | * 204 | */ 205 | public function getCount() 206 | { 207 | return $this->count; 208 | } 209 | 210 | /** 211 | * 212 | * Returns the TOC depth level of this Page. 213 | * 214 | * @return int 215 | * 216 | */ 217 | public function getLevel() 218 | { 219 | if ($this->hasParent()) { 220 | return $this->parent->getLevel() + 1; 221 | } 222 | 223 | return 0; 224 | } 225 | 226 | /** 227 | * 228 | * Sets the Page previous to this one. 229 | * 230 | * @param Page $prev The page previous to this one. 231 | * 232 | */ 233 | public function setPrev(Page $prev) 234 | { 235 | $this->prev = $prev; 236 | } 237 | 238 | /** 239 | * 240 | * Is there a Page previous to this one? 241 | * 242 | * @return bool 243 | * 244 | */ 245 | public function hasPrev() 246 | { 247 | return (bool) $this->prev; 248 | } 249 | 250 | /** 251 | * 252 | * Returns the page previous to this one, if any. 253 | * 254 | * @return Page|null 255 | * 256 | */ 257 | public function getPrev() 258 | { 259 | return $this->prev; 260 | } 261 | 262 | /** 263 | * 264 | * Sets the next Page after this one, if any. 265 | * 266 | * @param Page $next The next page. 267 | * 268 | */ 269 | public function setNext(Page $next) 270 | { 271 | $this->next = $next; 272 | } 273 | 274 | /** 275 | * 276 | * Is there a Page after this one? 277 | * 278 | * @return bool 279 | * 280 | */ 281 | public function hasNext() 282 | { 283 | return (bool) $this->next; 284 | } 285 | 286 | /** 287 | * 288 | * Returns the next Page after this one, if any. 289 | * 290 | * @return Page|null 291 | * 292 | */ 293 | public function getNext() 294 | { 295 | return $this->next; 296 | } 297 | 298 | /** 299 | * 300 | * Returns the href attribute for linking to this page. 301 | * 302 | * @return string 303 | * 304 | */ 305 | public function getHref() 306 | { 307 | $base = $this->getParent()->getHref(); 308 | return $base . $this->getName() . '.html'; 309 | } 310 | 311 | /** 312 | * 313 | * Returns the full number for this page. 314 | * 315 | * @return string 316 | * 317 | */ 318 | public function getNumber() 319 | { 320 | $base = $this->getParent()->getNumber(); 321 | $count = $this->getCount(); 322 | return "{$base}{$count}."; 323 | } 324 | 325 | /** 326 | * 327 | * Returns the full number-and-title for this page. 328 | * 329 | * @return string 330 | * 331 | */ 332 | public function getNumberAndTitle() 333 | { 334 | return trim($this->getNumber() . ' ' . $this->getTitle()); 335 | } 336 | 337 | /** 338 | * 339 | * Returns the target path for output from this page. 340 | * 341 | * @return string 342 | * 343 | */ 344 | public function getTarget() 345 | { 346 | $base = rtrim( 347 | dirname($this->getParent()->getTarget()), 348 | DIRECTORY_SEPARATOR 349 | ); 350 | return $base . DIRECTORY_SEPARATOR . $this->getName() . '.html'; 351 | } 352 | 353 | /** 354 | * 355 | * Returns the copyright string for this page. 356 | * 357 | * @return string 358 | * 359 | */ 360 | public function getCopyright() 361 | { 362 | return $this->copyright; 363 | } 364 | 365 | /** 366 | * 367 | * Sets the copyright string for this page. 368 | * 369 | * @param string $copyright The copyright string. 370 | * 371 | * 372 | */ 373 | public function setCopyright($copyright) 374 | { 375 | $this->copyright = $copyright; 376 | } 377 | 378 | /** 379 | * 380 | * Sets the heading objects for this page. 381 | * 382 | * @param array $headings An array of Heading objects. 383 | * 384 | */ 385 | public function setHeadings(array $headings) 386 | { 387 | $this->headings = $headings; 388 | } 389 | 390 | /** 391 | * 392 | * Does this page have any headings? 393 | * 394 | * @return bool 395 | * 396 | */ 397 | public function hasHeadings() 398 | { 399 | return (bool) $this->headings; 400 | } 401 | 402 | /** 403 | * 404 | * Returns the array of Heading objects. 405 | * 406 | * @return array 407 | * 408 | */ 409 | public function getHeadings() 410 | { 411 | return $this->headings; 412 | } 413 | 414 | /** 415 | * 416 | * Is this an index page? 417 | * 418 | * @return bool 419 | * 420 | */ 421 | public function isIndex() 422 | { 423 | return false; 424 | } 425 | 426 | /** 427 | * 428 | * Is this the root page? 429 | * 430 | * @return bool 431 | * 432 | */ 433 | public function isRoot() 434 | { 435 | return false; 436 | } 437 | 438 | /** 439 | * 440 | * Returns the root page object. 441 | * 442 | * @return RootPage 443 | * 444 | */ 445 | public function getRoot() 446 | { 447 | $page = $this; 448 | while (! $page->isRoot()) { 449 | $page = $page->getParent(); 450 | } 451 | return $page; 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /src/Content/PageFactory.php: -------------------------------------------------------------------------------- 1 | config = $config; 32 | $this->setTitle($config->getTitle()); 33 | } 34 | 35 | /** 36 | * 37 | * Returns the href attribute for this Page. 38 | * 39 | * @return string 40 | * 41 | */ 42 | public function getHref() 43 | { 44 | return $this->config->getRootHref(); 45 | } 46 | 47 | /** 48 | * 49 | * Returns the full number for this page. 50 | * 51 | * @return string 52 | * 53 | */ 54 | public function getNumber() 55 | { 56 | return ''; 57 | } 58 | 59 | /** 60 | * 61 | * Returns the target file path for the output from this page. 62 | * 63 | * @return string 64 | * 65 | */ 66 | public function getTarget() 67 | { 68 | return $this->getConfig()->getTarget() . 'index.html'; 69 | } 70 | 71 | /** 72 | * 73 | * Is this a root page? 74 | * 75 | * @return bool 76 | * 77 | */ 78 | public function isRoot() 79 | { 80 | return true; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Content/TocHeading.php: -------------------------------------------------------------------------------- 1 | children; 32 | } 33 | 34 | /** 35 | * @param TocHeadingIterator $children 36 | */ 37 | public function setChildren(TocHeadingIterator $children) 38 | { 39 | $this->children = $children; 40 | } 41 | 42 | /** 43 | * @return bool 44 | */ 45 | public function hasChildren() 46 | { 47 | return $this->getChildren() && count($this->getChildren()) > 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Content/TocHeadingIterator.php: -------------------------------------------------------------------------------- 1 | sourceHeadings = $this->createSourceHeadingList($headings); 42 | $this->rootHeadings = $this->findRoots(); 43 | } 44 | 45 | /** 46 | * @inheritdoc 47 | */ 48 | public function current() 49 | { 50 | $heading = $this->rootHeadings[$this->current]; 51 | return $this->decorateHeading($heading, $this->findChildren($heading)); 52 | } 53 | 54 | /** 55 | * @inheritdoc 56 | */ 57 | public function next() 58 | { 59 | ++$this->current; 60 | } 61 | 62 | /** 63 | * @inheritdoc 64 | */ 65 | public function key() 66 | { 67 | return $this->current; 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | */ 73 | public function valid() 74 | { 75 | return array_key_exists($this->current, $this->rootHeadings); 76 | } 77 | 78 | /** 79 | * @inheritdoc 80 | */ 81 | public function rewind() 82 | { 83 | $this->current = 1; 84 | } 85 | 86 | /** 87 | * @inheritdoc 88 | */ 89 | public function count() 90 | { 91 | return count($this->rootHeadings); 92 | } 93 | 94 | /** 95 | * @param Heading[] $headings 96 | * @return TocHeading[] 97 | */ 98 | protected function createSourceHeadingList(array $headings) 99 | { 100 | $result = array(); 101 | 102 | foreach ($headings as $heading) { 103 | $result[trim($heading->getNumber(), '.')] = $heading; 104 | } 105 | 106 | return $result; 107 | } 108 | 109 | /** 110 | * @return TocHeading[] 111 | */ 112 | protected function findRoots() 113 | { 114 | $headings = $this->sourceHeadings; 115 | 116 | $firstHeading = reset($headings); 117 | $level = $firstHeading->getLevel(); 118 | 119 | $roots = array_filter($headings, function ($key) use ($level) { 120 | if (count(explode('.', $key)) === $level) { 121 | return true; 122 | } 123 | 124 | return false; 125 | }, ARRAY_FILTER_USE_KEY); 126 | 127 | return $this->normalizeRootKeys($roots, $level); 128 | } 129 | 130 | /** 131 | * @param TocHeading[] $roots 132 | * @param string $level 133 | * @return TocHeading[] 134 | */ 135 | protected function normalizeRootKeys(array $roots, $level) 136 | { 137 | $result = array(); 138 | 139 | foreach ($roots as $key => $root) { 140 | $explodedKey = explode('.', $key); 141 | $normalizedKey = $explodedKey[$level - 1]; 142 | 143 | $result[$normalizedKey] = $root; 144 | } 145 | 146 | return $result; 147 | } 148 | 149 | /** 150 | * @param TocHeading $rootHeading 151 | * @return TocHeading[] 152 | */ 153 | protected function findChildren(TocHeading $rootHeading) 154 | { 155 | $headings = $this->sourceHeadings; 156 | 157 | $number = (string)$rootHeading->getNumber(); 158 | 159 | $children = array_filter($headings, function ($key) use ($number) { 160 | if (strpos((string)$key, $number) === 0) { 161 | return true; 162 | } 163 | 164 | return false; 165 | 166 | }, ARRAY_FILTER_USE_KEY); 167 | 168 | return $this->normalizeChildKeys($children, $number); 169 | } 170 | 171 | /** 172 | * @param TocHeading[] $headings 173 | * @param $number 174 | * @return TocHeading[] 175 | */ 176 | protected function normalizeChildKeys(array $headings, $number) 177 | { 178 | $result = array(); 179 | 180 | foreach ($headings as $key => $heading) { 181 | $normalizedKey = substr($key, strlen($number)); 182 | 183 | $result[$this->trimNumber($normalizedKey)] = $heading; 184 | } 185 | 186 | return $result; 187 | } 188 | 189 | /** 190 | * @param TocHeading $heading 191 | * @param TocHeading[] $children 192 | * @return Heading 193 | */ 194 | protected function decorateHeading(TocHeading $heading, $children) 195 | { 196 | if (count($children) === 0) { 197 | return $heading; 198 | } 199 | 200 | $heading->setChildren(new TocHeadingIterator($children)); 201 | return $heading; 202 | } 203 | 204 | /** 205 | * @param string $number 206 | * @return string 207 | */ 208 | protected function trimNumber($number) 209 | { 210 | return trim($number, '.'); 211 | } 212 | 213 | } -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 81 | $this->fsio = $fsio; 82 | $this->commonMarkConverter = $commonMarkConverter; 83 | } 84 | 85 | /** 86 | * 87 | * Invokes the processor. 88 | * 89 | * @param Page $page The Page to process. 90 | * 91 | */ 92 | public function __invoke(Page $page) 93 | { 94 | $this->page = $page; 95 | $text = $this->readOrigin(); 96 | $html = $this->commonMarkConverter->convertToHtml($text); 97 | $html = $this->convertMdHrefsToHtml($html); 98 | $this->saveTarget($html); 99 | } 100 | 101 | /** 102 | * 103 | * Returns the origin file Markdown. 104 | * 105 | * @return string 106 | * 107 | */ 108 | protected function readOrigin() 109 | { 110 | $file = $this->page->getOrigin(); 111 | if (! $file) { 112 | $this->logger->info(" No origin for {$this->page->getTarget()}"); 113 | return ''; 114 | } 115 | 116 | $this->logger->info(" Reading origin {$file}"); 117 | return $this->fsio->get($file); 118 | } 119 | 120 | /** 121 | * 122 | * Converts relative `.md` anchor hrefs to `.html` hrefs. 123 | * 124 | * @param string $html 125 | * 126 | * @return string 127 | * 128 | */ 129 | protected function convertMdHrefsToHtml($html) 130 | { 131 | if (! $html) { 132 | return $html; 133 | } 134 | 135 | $doc = new DomDocument(); 136 | $doc->formatOutput = true; 137 | $doc->loadHtml( 138 | mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), 139 | LIBXML_HTML_NODEFDTD 140 | ); 141 | 142 | $elems = $doc->getElementsByTagName('a'); 143 | foreach ($elems as $elem) { 144 | $href = $elem->getAttribute('href'); 145 | if ( 146 | strpos($href, "://") === false 147 | && substr($href, -3) === '.md' 148 | ) { 149 | $href = substr($href, 0, -3) . '.html'; 150 | } 151 | $elem->setAttribute('href', $href); 152 | } 153 | 154 | $html = trim($doc->saveHtml($doc->documentElement)); 155 | 156 | $html = substr( 157 | $html, 158 | strlen(''), 159 | -1 * strlen('') 160 | ); 161 | 162 | return trim($html) . PHP_EOL; 163 | } 164 | 165 | /** 166 | * 167 | * Saves the converted HTML file. 168 | * 169 | * @param string $html The HTML converted from Markdown. 170 | * 171 | */ 172 | protected function saveTarget($html) 173 | { 174 | $file = $this->page->getTarget(); 175 | $dir = dirname($file); 176 | if (! $this->fsio->isDir($dir)) { 177 | $this->logger->info(" Making directory {$dir}"); 178 | $this->fsio->mkdir($dir); 179 | } 180 | 181 | $this->logger->info(" Saving target {$file}"); 182 | $this->fsio->put($file, $html); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Process/Conversion/ConversionProcessBuilder.php: -------------------------------------------------------------------------------- 1 | newCommonMarkConverter($config) 48 | ); 49 | } 50 | 51 | /** 52 | * 53 | * Returns a new Converter object. 54 | * 55 | * @param RootConfig $config The root-level config object. 56 | * 57 | * @return Converter 58 | * 59 | * @throws \RuntimeException when a requested CommonMark extension class 60 | * does not exist. 61 | * 62 | */ 63 | protected function newCommonMarkConverter(RootConfig $config) 64 | { 65 | $environment = Environment::createCommonMarkEnvironment(); 66 | 67 | foreach ($config->getCommonMarkExtensions() as $extension) { 68 | if (! class_exists($extension)) { 69 | throw new \RuntimeException( 70 | sprintf('CommonMark extension class "%s" does not exists. You must use a FCQN!', $extension) 71 | ); 72 | } 73 | $environment->addExtension(new $extension()); 74 | } 75 | 76 | return new Converter( 77 | new DocParser($environment), 78 | new HtmlRenderer($environment) 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Process/CopyImage/CopyImageProcess.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 101 | $this->fsio = $fsio; 102 | $this->config = $config; 103 | } 104 | 105 | /** 106 | * 107 | * Invokes the processor. 108 | * 109 | * @param Page $page The Page to process. 110 | * 111 | */ 112 | public function __invoke(Page $page) 113 | { 114 | $this->logger->info(" Processing copy images for {$page->getTarget()}"); 115 | 116 | $this->reset($page); 117 | 118 | $this->loadHtml(); 119 | if ($this->html) { 120 | $this->loadDomDocument(); 121 | $this->processImageNodes(); 122 | $this->saveHtml(); 123 | } 124 | } 125 | 126 | /** 127 | * 128 | * Resets the processor for the Page to be processed. 129 | * 130 | * @param Page $page The page to be processed. 131 | * 132 | */ 133 | protected function reset(Page $page) 134 | { 135 | $this->page = $page; 136 | $this->html = null; 137 | $this->doc = null; 138 | } 139 | 140 | /** 141 | * 142 | * Loads the HTML from the rendered page. 143 | * 144 | */ 145 | protected function loadHtml() 146 | { 147 | $this->html = $this->fsio->get($this->page->getTarget()); 148 | } 149 | 150 | /** 151 | * 152 | * Save the modified HTML back to the rendered page. 153 | * 154 | */ 155 | protected function saveHtml() 156 | { 157 | $this->fsio->put($this->page->getTarget(), $this->html); 158 | } 159 | 160 | /** 161 | * 162 | * Creates a DomDocument from the page HTML. 163 | * 164 | */ 165 | protected function loadDomDocument() 166 | { 167 | $this->doc = new DomDocument(); 168 | $this->doc->formatOutput = true; 169 | $this->doc->loadHtml( 170 | mb_convert_encoding($this->html, 'HTML-ENTITIES', 'UTF-8'), 171 | LIBXML_HTML_NODEFDTD 172 | ); 173 | } 174 | 175 | /** 176 | * 177 | * Finds and retains all images in the DomDocument. 178 | * 179 | */ 180 | protected function processImageNodes() 181 | { 182 | $nodes = $this->getImageNodes(); 183 | $this->addImages($nodes); 184 | $this->setHtmlFromDomDocument(); 185 | } 186 | 187 | /** 188 | * 189 | * Gets all the images nodes in the DomDocument. 190 | * 191 | * @return DomNodeList 192 | * 193 | */ 194 | protected function getImageNodes() 195 | { 196 | $xpath = new DomXpath($this->doc); 197 | $query = '//img'; 198 | return $xpath->query($query); 199 | } 200 | 201 | /** 202 | * 203 | * Retains all the image nodes. 204 | * 205 | * @param DomNodeList $nodes The image nodes. 206 | * 207 | */ 208 | protected function addImages(DomNodeList $nodes) 209 | { 210 | foreach ($nodes as $node) { 211 | $this->addImage($node); 212 | } 213 | } 214 | 215 | /** 216 | * 217 | * Adds one image. 218 | * 219 | * @param DomNode $node The image node. 220 | * 221 | */ 222 | protected function addImage(DomNode $node) 223 | { 224 | if ($src = $this->downloadImage($node)) { 225 | $node->attributes->getNamedItem('src')->nodeValue = $src; 226 | } 227 | } 228 | 229 | /** 230 | * 231 | * Copies the image to the rendering location. 232 | * 233 | * @param DomNode $node The image node. 234 | * 235 | * @throws Exception on error. 236 | * 237 | */ 238 | protected function downloadImage(DomNode $node) 239 | { 240 | $image = $node->attributes->getNamedItem('src')->nodeValue; 241 | 242 | // no image or absolute URI 243 | if (! $image || preg_match('#^(http(s)?|//)#', $image)) { 244 | return ''; 245 | } 246 | $imageName = basename($image); 247 | $originFile = dirname($this->page->getOrigin()) . '/' . ltrim($image, '/'); 248 | 249 | $dir = dirname($this->page->getTarget()) . '/img/'; 250 | $file = $dir . $imageName; 251 | 252 | if (!$this->fsio->isDir($dir)) { 253 | $this->fsio->mkdir($dir); 254 | } 255 | 256 | try { 257 | $this->fsio->put($file, $this->fsio->get($originFile)); 258 | } catch (\Exception $e) { 259 | $this->logger->warning(" Image {$originFile} does not exist."); 260 | } 261 | return $this->config->getRootHref() . (str_replace($this->config->getTarget(), '', $dir)) . $imageName; 262 | } 263 | 264 | /** 265 | * 266 | * Retains the modified DomDocument HTML. 267 | * 268 | */ 269 | protected function setHtmlFromDomDocument() 270 | { 271 | // retain the modified html 272 | $this->html = trim($this->doc->saveHtml($this->doc->documentElement)); 273 | 274 | // strip the html and body tags added by DomDocument 275 | $this->html = substr( 276 | $this->html, 277 | strlen(''), 278 | -1 * strlen('') 279 | ); 280 | 281 | // still may be whitespace all about 282 | $this->html = trim($this->html) . PHP_EOL; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/Process/CopyImage/CopyImageProcessBuilder.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 70 | $this->fsio = $fsio; 71 | $this->config = $config; 72 | } 73 | 74 | /** 75 | * 76 | * Invokes the processor. 77 | * 78 | * @param Page $page The Page to process. 79 | * 80 | */ 81 | public function __invoke(Page $page) 82 | { 83 | $page->setCopyright($this->config->getCopyright()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Process/Copyright/CopyrightProcessBuilder.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 132 | $this->fsio = $fsio; 133 | $this->headingFactory = $headingFactory; 134 | $this->numbering = $numbering; 135 | } 136 | 137 | /** 138 | * 139 | * Invokes the processor. 140 | * 141 | * @param Page $page The Page to process. 142 | * 143 | */ 144 | public function __invoke(Page $page) 145 | { 146 | $this->logger->info(" Processing headings for {$page->getTarget()}"); 147 | 148 | $this->reset($page); 149 | 150 | $this->loadHtml(); 151 | if ($this->html) { 152 | $this->loadDomDocument(); 153 | $this->processHeadingNodes(); 154 | $this->saveHtml(); 155 | } 156 | 157 | $page->setHeadings($this->headings); 158 | } 159 | 160 | /** 161 | * 162 | * Resets this processor for a new Page. 163 | * 164 | * @param Page $page The page to process. 165 | * 166 | */ 167 | protected function reset(Page $page) 168 | { 169 | $this->page = $page; 170 | $this->html = null; 171 | $this->doc = null; 172 | $this->counts = array( 173 | 'h2' => 0, 174 | 'h3' => 0, 175 | 'h4' => 0, 176 | 'h5' => 0, 177 | 'h6' => 0, 178 | ); 179 | $this->headings = array(); 180 | 181 | if ($this->page->isIndex()) { 182 | $this->headings[] = $this->headingFactory->newInstance( 183 | $this->page->getNumber(), 184 | $this->page->getTitle(), 185 | $this->page->getHref() 186 | ); 187 | } 188 | } 189 | 190 | /** 191 | * 192 | * Loads the $html property from the rendered page. 193 | * 194 | */ 195 | protected function loadHtml() 196 | { 197 | $this->html = $this->fsio->get($this->page->getTarget()); 198 | } 199 | 200 | /** 201 | * 202 | * Saves the processed HTML back to the rendered page. 203 | * 204 | */ 205 | protected function saveHtml() 206 | { 207 | $this->fsio->put($this->page->getTarget(), $this->html); 208 | } 209 | 210 | /** 211 | * 212 | * Loads the HTML into a DomDocument. 213 | * 214 | */ 215 | protected function loadDomDocument() 216 | { 217 | $this->doc = new DomDocument(); 218 | $this->doc->formatOutput = true; 219 | $this->doc->loadHtml( 220 | mb_convert_encoding($this->html, 'HTML-ENTITIES', 'UTF-8'), 221 | LIBXML_HTML_NODEFDTD 222 | ); 223 | } 224 | 225 | /** 226 | * 227 | * Adds heading objects from Dom nodes. 228 | * 229 | */ 230 | protected function processHeadingNodes() 231 | { 232 | $nodes = $this->getHeadingNodes(); 233 | $this->setPageTitle($nodes); 234 | $this->addHeadings($nodes); 235 | $this->setHtmlFromDomDocument(); 236 | } 237 | 238 | /** 239 | * 240 | * Gets heading nodes from the DomDocument. 241 | * 242 | * @return DomNodeList 243 | * 244 | */ 245 | protected function getHeadingNodes() 246 | { 247 | $xpath = new DomXpath($this->doc); 248 | $query = '/html/body/*[self::h1 or self::h2 or self::h3 or self::h4 ' 249 | . ' or self::h5 or self::h6]'; 250 | return $xpath->query($query); 251 | } 252 | 253 | /** 254 | * 255 | * Sets the page title from the first DomNode. 256 | * 257 | * @param DomNodeList $nodes The heading nodes. 258 | * 259 | */ 260 | protected function setPageTitle(DomNodeList $nodes) 261 | { 262 | $node = $nodes->item(0); 263 | if ($node) { 264 | $this->page->setTitle($node->nodeValue); 265 | } 266 | } 267 | 268 | /** 269 | * 270 | * Adds all DomNodeList nodes as Heading objects. 271 | * 272 | * @param DomNodeList $nodes The heading nodes. 273 | * 274 | */ 275 | protected function addHeadings(DomNodeList $nodes) 276 | { 277 | foreach ($nodes as $node) { 278 | $this->addHeading($node); 279 | } 280 | } 281 | 282 | /** 283 | * 284 | * Adds one DomNode as a Heading object, and sets the heading number and ID 285 | * on the DomNode. 286 | * 287 | * @param DomNode $node The heading node. 288 | * 289 | */ 290 | protected function addHeading(DomNode $node) 291 | { 292 | $heading = $this->newHeading($node); 293 | $this->headings[] = $heading; 294 | 295 | $number = new DomText(); 296 | 297 | switch ($this->numbering) { 298 | case false: 299 | $number->nodeValue = ''; 300 | break; 301 | case 'decimal': 302 | default: 303 | $number->nodeValue = $heading->getNumber() . ' '; 304 | break; 305 | } 306 | 307 | $node->insertBefore($number, $node->firstChild); 308 | 309 | $node->setAttribute('id', $heading->getAnchor()); 310 | } 311 | 312 | /** 313 | * 314 | * Creates a new Heading object from a DomNode. 315 | * 316 | * @param DomNode $node The heading node. 317 | * 318 | * @return Heading 319 | * 320 | */ 321 | protected function newHeading(DomNode $node) 322 | { 323 | // the full heading number 324 | $number = $this->getHeadingNumber($node); 325 | 326 | // strip the leading and the closing 327 | // this assumes the tag has no attributes 328 | $title = substr($node->C14N(), 4, -5); 329 | 330 | // lose the trailing dot for the ID 331 | $id = substr($number, 0, -1); 332 | 333 | return $this->headingFactory->newInstance( 334 | $number, 335 | $title, 336 | $this->page->getHref(), 337 | null, 338 | $id 339 | ); 340 | } 341 | 342 | /** 343 | * 344 | * Gets the heading number from a DomNode. 345 | * 346 | * @param DomNode $node The heading node. 347 | * 348 | * @return string 349 | * 350 | */ 351 | protected function getHeadingNumber(DomNode $node) 352 | { 353 | $this->setCounts($node); 354 | $string = ''; 355 | foreach ($this->counts as $count) { 356 | if (! $count) { 357 | break; 358 | } 359 | $string .= "{$count}."; 360 | } 361 | return $this->page->getNumber() . $string; 362 | } 363 | 364 | /** 365 | * 366 | * Given a DomNode, increment or reset the h2/h3/etc counts. 367 | * 368 | * @param DomNode $node The heading node. 369 | * 370 | */ 371 | protected function setCounts(DomNode $node) 372 | { 373 | foreach ($this->counts as $level => $count) { 374 | if ($level == $node->nodeName) { 375 | $this->counts[$level] ++; 376 | } 377 | if ($level > $node->nodeName) { 378 | $this->counts[$level] = 0; 379 | } 380 | } 381 | } 382 | 383 | /** 384 | * 385 | * Retains modified HTML from the DomDocument manipulations. 386 | * 387 | */ 388 | protected function setHtmlFromDomDocument() 389 | { 390 | // retain the modified html 391 | $this->html = trim($this->doc->saveHtml($this->doc->documentElement)); 392 | 393 | // strip the html and body tags added by DomDocument 394 | $this->html = substr( 395 | $this->html, 396 | strlen(''), 397 | -1 * strlen('') 398 | ); 399 | 400 | // still may be whitespace all about 401 | $this->html = trim($this->html) . PHP_EOL; 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /src/Process/Headings/HeadingsProcessBuilder.php: -------------------------------------------------------------------------------- 1 | newHeadingFactory(), 45 | $config->getNumbering() 46 | ); 47 | } 48 | 49 | /** 50 | * 51 | * Returns a new HeadingFactory object. 52 | * 53 | * @return HeadingFactory 54 | * 55 | */ 56 | protected function newHeadingFactory() 57 | { 58 | return new HeadingFactory(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Process/Index/IndexProcess.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 108 | $this->fsio = $fsio; 109 | $this->config = $config; 110 | } 111 | 112 | /** 113 | * 114 | * Invokes the processor. 115 | * 116 | * @param Page $page The Page to process. 117 | * 118 | */ 119 | public function __invoke(Page $page) 120 | { 121 | $file = $this->config->getTarget() . 'index.json'; 122 | 123 | $this->reset(); 124 | $this->logger->info(" Create search index for {$page->getTarget()}"); 125 | $this->writeIndex($page, $file); 126 | } 127 | 128 | /** 129 | * 130 | * Writes page search data to the JSON search index file. 131 | * 132 | * @param Page $page The page being processed. 133 | * 134 | * @param string $file The search index file. 135 | * 136 | * @throws Exception on error. 137 | * 138 | */ 139 | protected function writeIndex(Page $page, $file) 140 | { 141 | if ($page->isRoot()) { 142 | $this->fsio->put($file, json_encode(array())); 143 | return; 144 | } 145 | 146 | if ($page->isIndex()) { 147 | return; 148 | } 149 | 150 | $html = $this->loadHtml($page); 151 | $this->processHtml($html, $page); 152 | 153 | $this->searchIndex = $this->readJson($file); 154 | $this->buildRelatedContent(); 155 | $this->writeJson($this->searchIndex, $file); 156 | } 157 | 158 | /** 159 | * 160 | * Create the heading and content array with related keys. 161 | * 162 | * @param string $html The HTML from the page. 163 | * 164 | * @param Page $page The page being processed. 165 | * 166 | */ 167 | protected function processHtml($html, Page $page) 168 | { 169 | $i = 0; 170 | $headingTags = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); 171 | $domDocument = $this->createDomDocument($html); 172 | $elements = $this->getHtmlDomBody($domDocument); 173 | 174 | foreach ($elements as $element) { 175 | 176 | $isHeading = in_array($element->nodeName, $headingTags); 177 | 178 | // add heading 179 | if ($isHeading) { 180 | /* @var Heading $heading */ 181 | $heading = $page->getHeadings()[$i]; 182 | $i++; 183 | $this->headings[$i] = array( 184 | 'title' => strip_tags($domDocument->saveHTML($element)), 185 | 'href' => $heading->getHref() 186 | ); 187 | } 188 | 189 | // add content 190 | if (!$isHeading && $i > 0) { 191 | if (empty($this->contents[$i])) { 192 | $this->contents[$i] = ''; 193 | } 194 | $this->contents[$i] .= preg_replace('/\s+/', ' ', strip_tags($domDocument->saveHTML($element))); 195 | } 196 | } 197 | } 198 | 199 | /** 200 | * 201 | * Creates the content index entries related to the correct title. 202 | * 203 | */ 204 | protected function buildRelatedContent() 205 | { 206 | $contents = $this->getContents(); 207 | 208 | if (count($contents) > 0) { 209 | foreach ($contents as $key => $content) { 210 | array_push($this->searchIndex, array( 211 | 'id' => utf8_encode($this->getHeadings()[$key]['href']), 212 | 'title' => utf8_encode($this->getHeadings()[$key]['title']), 213 | 'content' => utf8_encode($content) 214 | )); 215 | } 216 | } 217 | } 218 | 219 | /** 220 | * 221 | * Gets the html dom body children list. 222 | * 223 | * @param \DOMDocument $domDocument The DomDocument for the page. 224 | * 225 | * @return \DOMNodeList 226 | * 227 | */ 228 | protected function getHtmlDomBody(\DOMDocument $domDocument) 229 | { 230 | $xpath = new \DomXpath($domDocument); 231 | $query = '//div[@id="section-main"]//h1/../*|//div[@id="section-main"]//h2/../*|//div[@id="section-main"]//h3/../*|//div[@id="section-main"]//h4/../*|//div[@id="section-main"]//h5/../*|//div[@id="section-main"]//h6/../*'; 232 | return $xpath->query($query); 233 | } 234 | 235 | /** 236 | * 237 | * Creates a DomDocument from the page HTML. 238 | * 239 | * @param string $html The Page HTML. 240 | * 241 | * @return \DOMDocument 242 | * 243 | */ 244 | protected function createDomDocument($html) 245 | { 246 | $domDocument = new \DOMDocument(); 247 | libxml_use_internal_errors(true); 248 | $domDocument->formatOutput = true; 249 | $domDocument->loadHTML(mb_convert_encoding( 250 | $html, 251 | 'HTML-ENTITIES', 252 | 'UTF-8' 253 | ), LIBXML_HTML_NODEFDTD); 254 | libxml_use_internal_errors(false); 255 | 256 | return $domDocument; 257 | } 258 | 259 | /** 260 | * 261 | * Returns HTML from the rendered Page file. 262 | * 263 | * @param Page $page The page being processed. 264 | * 265 | * @return string 266 | * 267 | */ 268 | protected function loadHtml(Page $page) 269 | { 270 | return $this->fsio->get($page->getTarget()); 271 | } 272 | 273 | /** 274 | * 275 | * Read in the search index file and returns JSON data as array. 276 | * 277 | * @param string $file The search index JSON file. 278 | * 279 | * @return array 280 | * 281 | */ 282 | protected function readJson($file) 283 | { 284 | $json = $this->fsio->get($file); 285 | return json_decode($json, true); 286 | } 287 | 288 | /** 289 | * 290 | * Writes the content array to the search index JSON file. 291 | * 292 | * @param array $content The content to put into the JSON file. 293 | * 294 | * @param string $file The file to write to. 295 | * 296 | */ 297 | protected function writeJson(array $content, $file) 298 | { 299 | $json = json_encode($content); 300 | if ($json === false) { 301 | throw new Exception(json_last_error_msg(), json_last_error()); 302 | } 303 | $this->fsio->put($file, $json); 304 | } 305 | 306 | /** 307 | * 308 | * Returns the search index headings. 309 | * 310 | * @return array 311 | * 312 | */ 313 | protected function getHeadings() 314 | { 315 | return $this->headings; 316 | } 317 | 318 | /** 319 | * 320 | * Returns the new search index contents. 321 | * 322 | * @return array 323 | * 324 | */ 325 | protected function getContents() 326 | { 327 | return $this->contents; 328 | } 329 | 330 | /** 331 | * 332 | * Resets the processor for a new page. 333 | * 334 | */ 335 | protected function reset() 336 | { 337 | $this->headings = null; 338 | $this->contents = null; 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/Process/Index/IndexProcessBuilder.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 70 | $this->fsio = $fsio; 71 | $this->view = $view; 72 | } 73 | 74 | /** 75 | * 76 | * Invokes the processor. 77 | * 78 | * @param Page $page The Page to process. 79 | * 80 | */ 81 | public function __invoke(Page $page) 82 | { 83 | $file = $page->getTarget(); 84 | $this->logger->info(" Rendering {$file}"); 85 | $this->view->page = $page; 86 | $this->view->html = '
' 87 | .$this->fsio->get($page->getTarget()) 88 | .'
'; 89 | $result = $this->view->__invoke(); 90 | $this->fsio->put($file, $result); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Process/Rendering/RenderingProcessBuilder.php: -------------------------------------------------------------------------------- 1 | newView($config) 48 | ); 49 | } 50 | 51 | /** 52 | * 53 | * Returns a new View object. 54 | * 55 | * @param RootConfig $config The root-level config object. 56 | * 57 | * @return View 58 | * 59 | */ 60 | protected function newView(RootConfig $config) 61 | { 62 | $helpersFactory = new HelperLocatorFactory(); 63 | $helpers = $helpersFactory->newInstance(); 64 | 65 | $viewFactory = new ViewFactory(); 66 | $view = $viewFactory->newInstance($helpers); 67 | 68 | $this->setTemplate($view, $config); 69 | 70 | return $view; 71 | } 72 | 73 | /** 74 | * 75 | * Sets the main template into a View object. 76 | * 77 | * @param View $view The View object. 78 | * 79 | * @param RootConfig $config The root-level config object. 80 | * 81 | */ 82 | protected function setTemplate(View $view, RootConfig $config) 83 | { 84 | $template = $config->getTemplate(); 85 | $projectRoot = dirname(dirname(dirname(__DIR__))); 86 | 87 | if (!$template) { 88 | $template = $projectRoot . '/templates/main.php'; 89 | } elseif (RootConfig::BOOKDOWN_THEMES_DEFAULT === $template) { 90 | $templateFilePath = '/themes/templates/main.php'; 91 | 92 | $template = realpath(dirname($projectRoot) . $templateFilePath); 93 | if (!file_exists($template)) { 94 | $template = realpath($projectRoot . '/vendor/bookdown' . $templateFilePath); 95 | } 96 | } 97 | 98 | if (! file_exists($template) && ! is_readable($template)) { 99 | throw new Exception("Cannot find template '$template'."); 100 | } 101 | 102 | $registry = $view->getViewRegistry(); 103 | $registry->set('__BOOKDOWN__', $template); 104 | 105 | $view->setView('__BOOKDOWN__'); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Process/Toc/TocProcess.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 76 | } 77 | 78 | /** 79 | * 80 | * Invokes the processor. 81 | * 82 | * @param Page $page The Page to process. 83 | * 84 | */ 85 | public function __invoke(Page $page) 86 | { 87 | if (! $page->isIndex()) { 88 | $this->logger->info(" Skipping TOC entries for non-index {$page->getTarget()}"); 89 | return; 90 | } 91 | 92 | $this->logger->info(" Adding TOC entries for {$page->getTarget()}"); 93 | 94 | $this->tocEntries = []; 95 | $this->tocDepth = $page->getConfig()->getTocDepth(); 96 | $this->maxLevel = $this->tocDepth + $page->getLevel(); 97 | 98 | $this->addTocEntries($page); 99 | $page->setTocEntries($this->tocEntries); 100 | 101 | $this->addNestedTocEntries($page); 102 | } 103 | 104 | /** 105 | * 106 | * Adds TOC entries to the index page. 107 | * 108 | * @param IndexPage $index 109 | * 110 | */ 111 | protected function addTocEntries(IndexPage $index) 112 | { 113 | foreach ($index->getChildren() as $child) { 114 | $headings = $child->getHeadings(); 115 | foreach ($headings as $heading) { 116 | $this->addTocEntry($heading); 117 | } 118 | if ($child->isIndex()) { 119 | $this->addTocEntries($child); 120 | } 121 | } 122 | } 123 | 124 | /** 125 | * 126 | * Adds a single TOC entry, as long as it's before the max level allowed. 127 | * 128 | * @param Heading $heading A Heading for a TOC entry. 129 | * 130 | */ 131 | protected function addTocEntry(Heading $heading) 132 | { 133 | if (! $this->tocDepth || $heading->getLevel() <= $this->maxLevel) { 134 | $this->tocEntries[] = $heading; 135 | } 136 | } 137 | 138 | /** 139 | * @param IndexPage $index 140 | */ 141 | protected function addNestedTocEntries(IndexPage $index) 142 | { 143 | $index->setNestedTocEntries(new TocHeadingIterator($this->createTocHeadingList($this->tocEntries))); 144 | } 145 | 146 | /** 147 | * @param Heading[] $headings 148 | * @return TocHeading[] 149 | */ 150 | protected function createTocHeadingList(array $headings) 151 | { 152 | $tocHeading = array(); 153 | 154 | foreach ($headings as $heading) { 155 | $tocHeading[] = new TocHeading( 156 | $heading->getNumber(), 157 | $heading->getTitle(), 158 | $heading->getHref(), 159 | $heading->getHrefAnchor(), 160 | $heading->getId() 161 | ); 162 | } 163 | 164 | return $tocHeading; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Process/Toc/TocProcessBuilder.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 110 | $this->fsio = $fsio; 111 | $this->configFactory = $configFactory; 112 | $this->pageFactory = $pageFactory; 113 | } 114 | 115 | /** 116 | * 117 | * Sets the root-level config override values. 118 | * 119 | * @param array $rootConfigOverrides The override values. 120 | * 121 | */ 122 | public function setRootConfigOverrides(array $rootConfigOverrides) 123 | { 124 | $this->configFactory->setRootConfigOverrides($rootConfigOverrides); 125 | } 126 | 127 | /** 128 | * 129 | * Executes the collection process. 130 | * 131 | * @param string $configFile The config file for a directory of pages. 132 | * 133 | * @param string $name The name of the current page. 134 | * 135 | * @param Page $parent The parent page, if any. 136 | * 137 | * @param int $count The current sequential page count. 138 | * 139 | * @return RootPage 140 | * 141 | */ 142 | public function __invoke($configFile, $name = '', $parent = null, $count = 0) 143 | { 144 | $this->padlog("Collecting content from {$configFile}"); 145 | $this->level ++; 146 | $index = $this->newIndex($configFile, $name, $parent, $count); 147 | $this->addContent($index); 148 | $this->level --; 149 | return $index; 150 | } 151 | 152 | /** 153 | * 154 | * Adds and returns a new IndexPage. 155 | * 156 | * @param string $configFile The config file for a directory of pages. 157 | * 158 | * @param string $name The name of the current page. 159 | * 160 | * @param Page $parent The parent page, if any. 161 | * 162 | * @param int $count The current sequential page count. 163 | * 164 | * @return RootPage\IndexPage 165 | * 166 | */ 167 | protected function newIndex($configFile, $name, $parent, $count) 168 | { 169 | if (! $parent) { 170 | return $this->addRootPage($configFile); 171 | } 172 | 173 | return $this->addIndexPage($configFile, $name, $parent, $count); 174 | } 175 | 176 | /** 177 | * 178 | * Adds child pages to an IndexPage. 179 | * 180 | * @param IndexPage $index The IndexPage to add to. 181 | * 182 | */ 183 | protected function addContent(IndexPage $index) 184 | { 185 | $count = 1; 186 | foreach ($index->getConfig()->getContent() as $name => $file) { 187 | $child = $this->newChild($file, $name, $index, $count); 188 | $index->addChild($child); 189 | $count ++; 190 | } 191 | } 192 | 193 | /** 194 | * 195 | * Creates and returns a new Page object. 196 | * 197 | * @param string $file The file for the page; if a bookdown.json file, 198 | * recurses into its contents. 199 | * 200 | * @param string $name The name of the current page. 201 | * 202 | * @param IndexPage $index The index page over this one. 203 | * 204 | * @param int $count The current sequential page count. 205 | * 206 | * @return Page 207 | * 208 | */ 209 | protected function newChild($file, $name, IndexPage $index, $count) 210 | { 211 | $bookdown_json = 'bookdown.json'; 212 | $len = -1 * strlen($bookdown_json); 213 | 214 | if (substr($file, $len) == $bookdown_json) { 215 | return $this->__invoke($file, $name, $index, $count); 216 | } 217 | 218 | return $this->addPage($file, $name, $index, $count); 219 | } 220 | 221 | /** 222 | * 223 | * Appends a Page to $pages. 224 | * 225 | * @param string $origin The Markdown file for the page. 226 | * 227 | * @param string $name The name of the current page. 228 | * 229 | * @param IndexPage $parent The parent page over this one. 230 | * 231 | * @param int $count The current sequential page count. 232 | * 233 | * @return Page 234 | * 235 | */ 236 | protected function addPage($origin, $name, IndexPage $parent, $count) 237 | { 238 | $page = $this->pageFactory->newPage($origin, $name, $parent, $count); 239 | $this->padlog("Added page {$page->getOrigin()}"); 240 | return $this->append($page); 241 | } 242 | 243 | /** 244 | * 245 | * Adds the root page to $pages. 246 | * 247 | * @param string $configFile The root-level config file 248 | * 249 | * @return RootPage 250 | * 251 | */ 252 | protected function addRootPage($configFile) 253 | { 254 | $data = $this->fsio->get($configFile); 255 | $config = $this->configFactory->newRootConfig($configFile, $data); 256 | $page = $this->pageFactory->newRootPage($config); 257 | $this->padlog("Added root page from {$configFile}"); 258 | return $this->append($page); 259 | } 260 | 261 | /** 262 | * 263 | * Adds and returns a new IndexPage. 264 | * 265 | * @param string $configFile The config file for a directory of pages. 266 | * 267 | * @param string $name The name of the current page. 268 | * 269 | * @param Page $parent The parent page, if any. 270 | * 271 | * @param int $count The current sequential page count. 272 | * 273 | * @return RootPage\IndexPage 274 | * 275 | */ 276 | protected function addIndexPage($configFile, $name, $parent, $count) 277 | { 278 | $data = $this->fsio->get($configFile); 279 | $config = $this->configFactory->newIndexConfig($configFile, $data); 280 | $page = $this->pageFactory->newIndexPage($config, $name, $parent, $count); 281 | $this->padlog("Added index page from {$configFile}"); 282 | return $this->append($page); 283 | } 284 | 285 | /** 286 | * 287 | * Appends a page to $pages. 288 | * 289 | * @param Page $page The page to append. 290 | * 291 | * @return Page 292 | * 293 | */ 294 | protected function append(Page $page) 295 | { 296 | if ($this->prev) { 297 | $this->prev->setNext($page); 298 | $page->setPrev($this->prev); 299 | } 300 | $this->pages[] = $page; 301 | $this->prev = $page; 302 | return $page; 303 | } 304 | 305 | /** 306 | * 307 | * Logs a message, with padding. 308 | * 309 | * @param string $str The message to log. 310 | * 311 | */ 312 | protected function padlog($str) 313 | { 314 | $pad = str_pad('', $this->level * 2); 315 | $this->logger->info("{$pad}{$str}"); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/Service/Processor.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 55 | $this->processes = $processes; 56 | } 57 | 58 | /** 59 | * 60 | * Applies the process objects to the pages. 61 | * 62 | * @param RootPage $root The root page. 63 | * 64 | */ 65 | public function __invoke(RootPage $root) 66 | { 67 | $this->logger->info("Processing content."); 68 | foreach ($this->processes as $process) { 69 | $this->logger->info(" Applying " . get_class($process)); 70 | $page = $root; 71 | while ($page) { 72 | $process($page); 73 | $page = $page->getNext(); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Service/ProcessorBuilder.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 55 | $this->fsio = $fsio; 56 | } 57 | 58 | /** 59 | * 60 | * Returns a new Processor object. 61 | * 62 | * @param RootConfig $config The root-level config object. 63 | * 64 | * @return Processor 65 | * 66 | */ 67 | public function newProcessor(RootConfig $config) 68 | { 69 | return new Processor( 70 | $this->logger, 71 | array( 72 | $this->newProcess($config, 'Conversion'), 73 | $this->newProcess($config, 'Copyright'), 74 | $this->newProcess($config, 'Headings'), 75 | $this->newProcess($config, 'CopyImage'), 76 | $this->newProcess($config, 'Toc'), 77 | $this->newProcess($config, 'Rendering'), 78 | $this->newProcess($config, 'Index'), 79 | ) 80 | ); 81 | } 82 | 83 | /** 84 | * 85 | * Returns a new Process object. 86 | * 87 | * @param RootConfig $config The root-level config object. 88 | * 89 | * @param string $name The process name. 90 | * 91 | * @return ProcessInterface 92 | * 93 | */ 94 | public function newProcess(RootConfig $config, $name) 95 | { 96 | $method = "get{$name}Process"; 97 | $class = $config->$method(); 98 | 99 | $implemented = is_subclass_of( 100 | $class, 101 | 'Bookdown\Bookdown\Process\ProcessBuilderInterface' 102 | ); 103 | if (! $implemented) { 104 | throw new Exception( 105 | "'{$class}' does not implement ProcessBuilderInterface" 106 | ); 107 | } 108 | 109 | $builder = new $class(); 110 | return $builder->newInstance($config, $this->logger, $this->fsio); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Service/Service.php: -------------------------------------------------------------------------------- 1 | collector = $collector; 64 | $this->processorBuilder = $processorBuilder; 65 | $this->timer = $timer; 66 | } 67 | 68 | /** 69 | * 70 | * Collects and processes the pages. 71 | * 72 | * @param string $rootConfigFile The location of the root config file. 73 | * 74 | * @param array $rootConfigOverrides Override values from the command-line 75 | * options. 76 | * 77 | */ 78 | public function __invoke($rootConfigFile, array $rootConfigOverrides) 79 | { 80 | $this->collector->setRootConfigOverrides($rootConfigOverrides); 81 | $rootPage = $this->collector->__invoke($rootConfigFile); 82 | $processor = $this->processorBuilder->newProcessor($rootPage->getConfig()); 83 | $processor->__invoke($rootPage); 84 | $this->timer->report(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Service/Timer.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 41 | $this->start = microtime(true); 42 | } 43 | 44 | /** 45 | * 46 | * Reports the run time to the logger. 47 | * 48 | */ 49 | public function report() 50 | { 51 | $seconds = microtime(true) - $this->start; 52 | $seconds = trim(sprintf("%10.2f", $seconds)); 53 | $this->logger->info("Completed in {$seconds} seconds."); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Stdlog.php: -------------------------------------------------------------------------------- 1 | stdout = $stdout; 67 | $this->stderr = $stderr; 68 | } 69 | 70 | /** 71 | * 72 | * Logs with an arbitrary level. 73 | * 74 | * @param mixed $level The log level. 75 | * 76 | * @param string $message The log message. 77 | * 78 | * @param array $context Data to interpolate into the message. 79 | * 80 | */ 81 | public function log($level, $message, array $context = []) 82 | { 83 | $replace = []; 84 | foreach ($context as $key => $val) { 85 | $replace['{' . $key . '}'] = $val; 86 | } 87 | $message = strtr($message, $replace) . PHP_EOL; 88 | 89 | $handle = $this->stdout; 90 | if (in_array($level, $this->stderrLevels)) { 91 | $handle = $this->stderr; 92 | } 93 | 94 | fwrite($handle, $message); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /templates/body.php: -------------------------------------------------------------------------------- 1 | 2 | render('core'); ?> 3 | 4 | -------------------------------------------------------------------------------- /templates/core.php: -------------------------------------------------------------------------------- 1 | render('navheader'); 3 | echo $this->page->isIndex() ? $this->render('toc') : ''; 4 | echo $this->html; 5 | echo $this->render('navfooter'); 6 | ?> 7 | -------------------------------------------------------------------------------- /templates/head.php: -------------------------------------------------------------------------------- 1 | 2 | <?php echo $this->page->getTitle(); ?> 3 | 4 | 50 | 51 | -------------------------------------------------------------------------------- /templates/main.php: -------------------------------------------------------------------------------- 1 | getViewRegistry(); 3 | $views->set('head', __DIR__ . '/head.php'); 4 | $views->set('body', __DIR__ . '/body.php'); 5 | $views->set('core', __DIR__ . '/core.php'); 6 | $views->set('navheader', __DIR__ . '/navheader.php'); 7 | $views->set('navfooter', __DIR__ . '/navfooter.php'); 8 | $views->set('toc', __DIR__ . '/toc.php'); 9 | ?> 10 | 11 | render('head'); ?> 12 | render('body'); ?> 13 | 14 | -------------------------------------------------------------------------------- /templates/navfooter.php: -------------------------------------------------------------------------------- 1 | page->getPrev(); 3 | $parent = $this->page->getParent(); 4 | $next = $this->page->getNext(); 5 | ?> 6 | 7 | 40 | -------------------------------------------------------------------------------- /templates/navheader.php: -------------------------------------------------------------------------------- 1 | page->getPrev(); 3 | $parent = $this->page->getParent(); 4 | $next = $this->page->getNext(); 5 | ?> 6 | 7 | 27 | -------------------------------------------------------------------------------- /templates/toc.php: -------------------------------------------------------------------------------- 1 | page->hasTocEntries()) { 2 | return; 3 | } ?> 4 | 5 |

page->getNumberAndTitle(); ?>

6 |
7 | page->getTocEntries(); 9 | $baseLevel = reset($entries)->getLevel(); 10 | $lastLevel = $baseLevel; 11 | $currLevel = $lastLevel; 12 | 13 | foreach ($entries as $entry) { 14 | 15 | while ($entry->getLevel() > $currLevel) { 16 | echo "
" . PHP_EOL; 17 | $currLevel ++; 18 | } 19 | 20 | while ($entry->getLevel() < $currLevel) { 21 | $currLevel --; 22 | echo "
" . PHP_EOL; 23 | } 24 | 25 | echo "
{$entry->getNumber()} " 26 | . $this->anchorRaw($entry->getHref(), $entry->getTitle()) 27 | . "
" . PHP_EOL; 28 | } 29 | 30 | while ($currLevel > $baseLevel) { 31 | $currLevel --; 32 | echo "
" . PHP_EOL; 33 | } 34 | ?> 35 | 36 | -------------------------------------------------------------------------------- /tests/BookFixture.php: -------------------------------------------------------------------------------- 1 | Bokdown.io" 27 | }'; 28 | public $rootConfig; 29 | public $rootPage; 30 | 31 | public $indexConfigFile = '/path/to/chapter/bookdown.json'; 32 | public $indexConfigData = '{ 33 | "title": "Chapter", 34 | "content": [ 35 | {"section": "section.md"} 36 | ] 37 | }'; 38 | public $indexConfig; 39 | public $indexPage; 40 | 41 | public $pageFile = '/path/to/chapter/section.md'; 42 | public $pageData = '# Title 43 | 44 | Text under title. 45 | 46 | ## Subtitle `code` A 47 | 48 | Text under subtitle A. 49 | 50 | ### Sub-subtitle 51 | 52 | Text under sub-subtitle. 53 | 54 | ## Subtitle B 55 | 56 | Text under subtitle B. 57 | 58 | > Blockqoute 59 | {: title="Blockquote title"} 60 | 61 | th | th(center) | th(right) 62 | ---|:----------:|----------: 63 | td | td | td 64 | '; 65 | public $page; 66 | 67 | public function __construct(FakeFsio $fsio) 68 | { 69 | $pageFactory = new PageFactory(); 70 | 71 | $fsio->put($this->rootConfigFile, $this->rootConfigData); 72 | $this->rootConfig = new RootConfig($this->rootConfigFile, $this->rootConfigData); 73 | $this->rootPage = $pageFactory->newRootPage($this->rootConfig); 74 | 75 | $fsio->put($this->indexConfigFile, $this->indexConfigData); 76 | $this->indexConfig = new IndexConfig($this->indexConfigFile, $this->indexConfigData); 77 | $this->indexPage = $pageFactory->newIndexPage( 78 | $this->indexConfig, 79 | 'chapter', 80 | $this->rootPage, 81 | 1 82 | ); 83 | $this->rootPage->setNext($this->indexPage); 84 | $this->rootPage->addChild($this->indexPage); 85 | $this->indexPage->setPrev($this->rootPage); 86 | 87 | $fsio->put($this->pageFile, $this->pageData); 88 | $this->page = $pageFactory->newPage( 89 | $this->pageFile, 90 | 'section', 91 | $this->indexPage, 92 | 1 93 | ); 94 | 95 | $this->indexPage->addChild($this->page); 96 | $this->indexPage->setNext($this->page); 97 | $this->page->setPrev($this->indexPage); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/BookImageFixture.php: -------------------------------------------------------------------------------- 1 | put($this->rootConfigFile, $this->rootConfigData); 65 | $this->rootConfig = new RootConfig($this->rootConfigFile, $this->rootConfigData); 66 | $this->rootPage = $pageFactory->newRootPage($this->rootConfig); 67 | 68 | $fsio->put($this->indexConfigFile, $this->indexConfigData); 69 | $this->indexConfig = new IndexConfig($this->indexConfigFile, $this->indexConfigData); 70 | $this->indexPage = $pageFactory->newIndexPage( 71 | $this->indexConfig, 72 | 'chapter', 73 | $this->rootPage, 74 | 1 75 | ); 76 | $this->rootPage->setNext($this->indexPage); 77 | $this->rootPage->addChild($this->indexPage); 78 | $this->indexPage->setPrev($this->rootPage); 79 | 80 | $fsio->put($this->pageFile, $this->pageData); 81 | $this->page = $pageFactory->newPage( 82 | $this->pageFile, 83 | 'section', 84 | $this->indexPage, 85 | 1 86 | ); 87 | 88 | $this->indexPage->addChild($this->page); 89 | $this->indexPage->setNext($this->page); 90 | $this->page->setPrev($this->indexPage); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/BookNumberingFixture.php: -------------------------------------------------------------------------------- 1 | Bokdown.io", 27 | "numbering": false 28 | }'; 29 | public $rootConfig; 30 | public $rootPage; 31 | 32 | public $indexConfigFile = '/path/to/chapter/bookdown.json'; 33 | public $indexConfigData = '{ 34 | "title": "Chapter", 35 | "content": [ 36 | {"section": "section.md"} 37 | ] 38 | }'; 39 | public $indexConfig; 40 | public $indexPage; 41 | 42 | public $pageFile = '/path/to/chapter/section.md'; 43 | public $pageData = '# Title 44 | 45 | Text under title. 46 | 47 | ## Subtitle `code` A 48 | 49 | Text under subtitle A. 50 | 51 | ### Sub-subtitle 52 | 53 | Text under sub-subtitle. 54 | 55 | ## Subtitle B 56 | 57 | Text under subtitle B. 58 | '; 59 | public $page; 60 | 61 | public function __construct(FakeFsio $fsio) 62 | { 63 | $pageFactory = new PageFactory(); 64 | 65 | $fsio->put($this->rootConfigFile, $this->rootConfigData); 66 | $this->rootConfig = new RootConfig($this->rootConfigFile, $this->rootConfigData); 67 | $this->rootPage = $pageFactory->newRootPage($this->rootConfig); 68 | 69 | $fsio->put($this->indexConfigFile, $this->indexConfigData); 70 | $this->indexConfig = new IndexConfig($this->indexConfigFile, $this->indexConfigData); 71 | $this->indexPage = $pageFactory->newIndexPage( 72 | $this->indexConfig, 73 | 'chapter', 74 | $this->rootPage, 75 | 1 76 | ); 77 | $this->rootPage->setNext($this->indexPage); 78 | $this->rootPage->addChild($this->indexPage); 79 | $this->indexPage->setPrev($this->rootPage); 80 | 81 | $fsio->put($this->pageFile, $this->pageData); 82 | $this->page = $pageFactory->newPage( 83 | $this->pageFile, 84 | 'section', 85 | $this->indexPage, 86 | 1 87 | ); 88 | 89 | $this->indexPage->addChild($this->page); 90 | $this->indexPage->setNext($this->page); 91 | $this->page->setPrev($this->indexPage); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/BookTocFixture.php: -------------------------------------------------------------------------------- 1 | rootConfigData = '{ 54 | "title": "Example Book", 55 | "content": [ 56 | {"chapter": "chapter/bookdown.json"} 57 | ], 58 | "target": "/_site", 59 | "tocDepth": ' . $tocDepth . ' 60 | }'; 61 | 62 | $this->indexConfigData = '{ 63 | "title": "Index Page", 64 | "content": [ 65 | {"section": "section.md"} 66 | ], 67 | "tocDepth": ' . $tocDepth . ' 68 | }'; 69 | 70 | $pageFactory = new PageFactory(); 71 | 72 | $fsio->put($this->rootConfigFile, $this->rootConfigData); 73 | $this->rootConfig = new RootConfig($this->rootConfigFile, $this->rootConfigData); 74 | $this->rootPage = $pageFactory->newRootPage($this->rootConfig); 75 | 76 | $fsio->put($this->indexConfigFile, $this->indexConfigData); 77 | $this->indexConfig = new IndexConfig($this->indexConfigFile, $this->indexConfigData); 78 | $this->indexPage = $pageFactory->newIndexPage( 79 | $this->indexConfig, 80 | 'chapter', 81 | $this->rootPage, 82 | 1 83 | ); 84 | $this->rootPage->setNext($this->indexPage); 85 | $this->rootPage->addChild($this->indexPage); 86 | $this->indexPage->setPrev($this->rootPage); 87 | 88 | $fsio->put($this->pageFile, $this->pageData); 89 | $this->page = $pageFactory->newPage( 90 | $this->pageFile, 91 | 'section', 92 | $this->indexPage, 93 | 1 94 | ); 95 | 96 | $this->indexPage->addChild($this->page); 97 | $this->indexPage->setNext($this->page); 98 | $this->page->setPrev($this->indexPage); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/CommandTest.php: -------------------------------------------------------------------------------- 1 | stdout = fopen('php://memory', 'a+'); 15 | $this->stderr = fopen('php://memory', 'a+'); 16 | 17 | $this->container = new Container( 18 | $this->stdout, 19 | $this->stderr, 20 | 'Bookdown\Bookdown\FakeFsio' 21 | ); 22 | 23 | $this->fsio = $this->container->getFsio(); 24 | $this->fixture = new BookFixture($this->fsio); 25 | 26 | } 27 | 28 | protected function exec(array $argv) 29 | { 30 | $command = $this->container->newCommand(array( 31 | 'argv' => $argv 32 | )); 33 | return $command(); 34 | } 35 | 36 | protected function assertLastStderr($expect) 37 | { 38 | $string = $this->getStderr(); 39 | $lines = explode(PHP_EOL, trim($string)); 40 | $actual = trim(end($lines)); 41 | $this->assertSame($expect, $actual); 42 | } 43 | 44 | protected function getStderr() 45 | { 46 | rewind($this->stderr); 47 | $string = ''; 48 | while ($chars = fread($this->stderr, 8192)) { 49 | $string .= $chars; 50 | } 51 | return $string; 52 | } 53 | 54 | public function testNoConfigFileSpecified() 55 | { 56 | $argv = array(); 57 | $exit = $this->exec($argv); 58 | $this->assertSame(1, $exit); 59 | $this->assertLastStderr('Please enter the path to a bookdown.json file as the first argument.'); 60 | } 61 | 62 | public function testSuccess() 63 | { 64 | $argv = array( 65 | 0 => './vendor/bin/bookdown', 66 | 1 => $this->fixture->rootConfigFile, 67 | ); 68 | $exit = $this->exec($argv); 69 | $this->assertSame(0, $exit); 70 | $this->assertLastStderr(''); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Config/ConfigFactoryTest.php: -------------------------------------------------------------------------------- 1 | factory = new ConfigFactory(); 25 | } 26 | 27 | public function testNewIndexConfig() 28 | { 29 | $config = $this->factory->newIndexConfig($this->file, $this->data); 30 | $this->assertInstanceOf('Bookdown\Bookdown\Config\IndexConfig', $config); 31 | } 32 | 33 | public function testNewRootConfig() 34 | { 35 | $overrides = array(); 36 | $config = $this->factory->newRootConfig($this->file, $this->data, $overrides); 37 | $this->assertInstanceOf('Bookdown\Bookdown\Config\RootConfig', $config); 38 | } 39 | 40 | public function testNewRootConfigOverrides() 41 | { 42 | $overrides = array( 43 | 'template' => "../../" . md5(uniqid()), 44 | 'target' => "../../" . md5(uniqid()), 45 | ); 46 | 47 | $this->factory->setRootConfigOverrides($overrides); 48 | $config = $this->factory->newRootConfig($this->file, $this->data); 49 | $this->assertInstanceOf('Bookdown\Bookdown\Config\RootConfig', $config); 50 | $this->assertSame("{$overrides['template']}", $config->getTemplate()); 51 | $this->assertSame("{$overrides['target']}/", $config->getTarget()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Config/IndexConfigTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException( 94 | 'Bookdown\Bookdown\Exception', 95 | "Malformed JSON in '/path/to/bookdown.json'." 96 | ); 97 | $config = $this->newIndexConfig('/path/to/bookdown.json', $this->malformedJson); 98 | } 99 | 100 | public function testMissingTitle() 101 | { 102 | $this->setExpectedException( 103 | 'Bookdown\Bookdown\Exception', 104 | "No title set in '/path/to/bookdown.json'." 105 | ); 106 | $config = $this->newIndexConfig('/path/to/bookdown.json', $this->jsonMissingTitle); 107 | } 108 | 109 | public function testMissingContent() 110 | { 111 | $this->setExpectedException( 112 | 'Bookdown\Bookdown\Exception', 113 | "No content listed in '/path/to/bookdown.json'." 114 | ); 115 | $config = $this->newIndexConfig('/path/to/bookdown.json', $this->jsonMissingContent); 116 | } 117 | 118 | public function testContentNotArray() 119 | { 120 | $this->setExpectedException( 121 | 'Bookdown\Bookdown\Exception', 122 | "Content must be an array in '/path/to/bookdown.json'." 123 | ); 124 | $config = $this->newIndexConfig('/path/to/bookdown.json', $this->jsonContentNotArray); 125 | } 126 | 127 | public function testContentItemNotStringOrObject() 128 | { 129 | $this->setExpectedException( 130 | 'Bookdown\Bookdown\Exception', 131 | "Content origin must be object or string in '/path/to/bookdown.json'." 132 | ); 133 | $config = $this->newIndexConfig('/path/to/bookdown.json', $this->jsonContentItemNotStringOrObject); 134 | } 135 | 136 | public function testContentIndex() 137 | { 138 | $this->setExpectedException( 139 | 'Bookdown\Bookdown\Exception', 140 | "Disallowed 'index' content name in '/path/to/bookdown.json'." 141 | ); 142 | $config = $this->newIndexConfig('/path/to/bookdown.json', $this->jsonContentIndex); 143 | } 144 | 145 | public function testDuplicateName() 146 | { 147 | $this->setExpectedException( 148 | 'Bookdown\Bookdown\Exception', 149 | "Content name 'master' already set in '/path/to/bookdown.json'." 150 | ); 151 | $config = $this->newIndexConfig('/path/to/bookdown.json', $this->jsonContentSameResolvedName); 152 | } 153 | 154 | 155 | public function testValidLocal() 156 | { 157 | $config = $this->newIndexConfig('/path/to/bookdown.json', $this->jsonValidLocal); 158 | 159 | $this->assertSame('/path/to/bookdown.json', $config->getFile()); 160 | $this->assertSame('/path/to/', $config->getDir()); 161 | $this->assertSame('Example Title', $config->getTitle()); 162 | $expect = array( 163 | 'foo' => '/path/to/foo.md', 164 | 'bar' => '/bar.md', 165 | 'baz' => 'http://example.com/baz.md', 166 | ); 167 | $this->assertSame($expect, $config->getContent()); 168 | } 169 | 170 | public function testValidRemote() 171 | { 172 | $config = $this->newIndexConfig( 173 | 'http://example.net/path/to/bookdown.json', 174 | $this->jsonValidRemote 175 | ); 176 | 177 | $this->assertSame('http://example.net/path/to/bookdown.json', $config->getFile()); 178 | 179 | $expect = array( 180 | 'zim' => 'http://example.net/path/to/zim.md', 181 | 'dib' => 'http://example.net/path/to/dib.md', 182 | 'gir' => 'http://example.com/gir.md', 183 | ); 184 | $this->assertSame($expect, $config->getContent()); 185 | } 186 | 187 | public function testContentConvenience() 188 | { 189 | $config = $this->newIndexConfig( 190 | '/path/to/bookdown.json', 191 | $this->jsonContentConvenience 192 | ); 193 | 194 | $this->assertSame('/path/to/bookdown.json', $config->getFile()); 195 | 196 | $expect = array( 197 | 'foo' => '/path/to/foo.md', 198 | 'bar' => '/path/to/bar/bookdown.json', 199 | 'baz' => 'http://example.com/baz.md', 200 | 'dib' => 'http://example.dom/dib/bookdown.json', 201 | ); 202 | 203 | $this->assertSame($expect, $config->getContent()); 204 | } 205 | 206 | public function testReusedContentName() 207 | { 208 | $this->setExpectedException( 209 | 'Bookdown\Bookdown\Exception', 210 | "Content name 'foo' already set in '/path/to/bookdown.json'." 211 | ); 212 | $config = $this->newIndexConfig( 213 | '/path/to/bookdown.json', 214 | $this->jsonReusedContentName 215 | ); 216 | } 217 | 218 | public function testInvalidRemote() 219 | { 220 | $this->setExpectedException( 221 | 'Bookdown\Bookdown\Exception', 222 | "Cannot handle absolute content path '/bar.md' in remote 'http://example.net/path/to/bookdown.json'." 223 | ); 224 | $config = $this->newIndexConfig( 225 | 'http://example.net/path/to/bookdown.json', 226 | $this->jsonValidLocal 227 | ); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /tests/Config/RootConfigTest.php: -------------------------------------------------------------------------------- 1 | newRootConfig('/path/to/bookdown.json', $this->maxRootJson); 59 | $this->assertBasics($config); 60 | 61 | $this->assertSame('/path/to/my/target/', $config->getTarget()); 62 | $this->assertSame('/path/to/../templates/master.php', $config->getTemplate()); 63 | $this->assertSame('My\\Conversion\\Builder', $config->getConversionProcess()); 64 | $this->assertSame('My\\Headings\\Builder', $config->getHeadingsProcess()); 65 | $this->assertSame('My\\Toc\\Builder', $config->getTocProcess()); 66 | $this->assertSame('My\\Rendering\\Builder', $config->getRenderingProcess()); 67 | $this->assertSame('whatever', $config->get('extra')); 68 | $this->assertSame('none', $config->get('no-such-key', 'none')); 69 | $this->assertSame('http://awesome.io/docs/', $config->getRootHref()); 70 | $this->assertSame('decimal', $config->getNumbering()); 71 | 72 | $extensions = $config->getCommonMarkExtensions(); 73 | 74 | $this->assertContains('Webuni\\CommonMark\\TableExtension\\TableExtension', $extensions); 75 | $this->assertContains( 76 | 'Webuni\\CommonMark\\AttributesExtension\\AttributesExtension', 77 | $extensions 78 | ); 79 | } 80 | 81 | protected function assertBasics($config) 82 | { 83 | $this->assertSame('/path/to/bookdown.json', $config->getFile()); 84 | $this->assertSame('/path/to/', $config->getDir()); 85 | $this->assertSame('Example Title', $config->getTitle()); 86 | $expect = array( 87 | 'foo' => '/path/to/foo.md', 88 | 'bar' => '/bar.md', 89 | 'baz' => 'http://example.com/baz.md', 90 | ); 91 | $this->assertSame($expect, $config->getContent()); 92 | } 93 | 94 | public function testMin() 95 | { 96 | $config = $this->newRootConfig('/path/to/bookdown.json', $this->minRootJson); 97 | $this->assertBasics($config); 98 | 99 | $this->assertSame('/path/to/_site/', $config->getTarget()); 100 | $this->assertSame(null, $config->getTemplate()); 101 | $this->assertSame( 102 | 'Bookdown\Bookdown\Process\Conversion\ConversionProcessBuilder', 103 | $config->getConversionProcess() 104 | ); 105 | $this->assertSame( 106 | 'Bookdown\Bookdown\Process\Headings\HeadingsProcessBuilder', 107 | $config->getHeadingsProcess() 108 | ); 109 | $this->assertSame( 110 | 'Bookdown\Bookdown\Process\Toc\TocProcessBuilder', 111 | $config->getTocProcess() 112 | ); 113 | $this->assertSame( 114 | 'Bookdown\Bookdown\Process\Rendering\RenderingProcessBuilder', 115 | $config->getRenderingProcess() 116 | ); 117 | } 118 | 119 | public function testMissingTitle() 120 | { 121 | $this->setExpectedException( 122 | 'Bookdown\Bookdown\Exception', 123 | "No target set in '/path/to/bookdown.json'." 124 | ); 125 | $config = $this->newRootConfig('/path/to/bookdown.json', $this->missingTargetJson); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/ContainerTest.php: -------------------------------------------------------------------------------- 1 | container = new Container(); 13 | } 14 | 15 | public function testNewCommand() 16 | { 17 | $this->assertInstanceOf( 18 | 'Bookdown\Bookdown\Command', 19 | $this->container->newCommand(array()) 20 | ); 21 | } 22 | 23 | public function testNewCollector() 24 | { 25 | $this->assertInstanceOf( 26 | 'Bookdown\Bookdown\Service\Collector', 27 | $this->container->newCollector() 28 | ); 29 | } 30 | 31 | public function testNewProcessorBuilder() 32 | { 33 | $this->assertInstanceOf( 34 | 'Bookdown\Bookdown\Service\ProcessorBuilder', 35 | $this->container->newProcessorBuilder() 36 | ); 37 | } 38 | 39 | public function testGetCliFactory() 40 | { 41 | $factory = $this->container->getCliFactory(); 42 | $this->assertInstanceOf('Aura\Cli\CliFactory', $factory); 43 | $again = $this->container->getCliFactory(); 44 | $this->assertSame($factory, $again); 45 | } 46 | 47 | public function testGetLogger() 48 | { 49 | $logger = $this->container->getLogger(); 50 | $this->assertInstanceOf('Psr\Log\LoggerInterface', $logger); 51 | $again = $this->container->getLogger(); 52 | $this->assertSame($logger, $again); 53 | } 54 | 55 | public function testGetFsio() 56 | { 57 | $fsio = $this->container->getFsio(); 58 | $this->assertInstanceOf('Bookdown\Bookdown\Fsio', $fsio); 59 | $again = $this->container->getFsio(); 60 | $this->assertSame($fsio, $again); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Content/ContentTest.php: -------------------------------------------------------------------------------- 1 | root = $bookFixture->rootPage; 20 | $this->index = $bookFixture->indexPage; 21 | $this->page = $bookFixture->page; 22 | } 23 | 24 | public function testRootPage() 25 | { 26 | $this->assertInstanceOf('Bookdown\Bookdown\Content\RootPage', $this->root); 27 | 28 | $this->assertSame(null, $this->root->getOrigin()); 29 | $this->assertSame('/_site/index.html', $this->root->getTarget()); 30 | 31 | $this->assertSame('/', $this->root->getHref()); 32 | $this->assertSame('', $this->root->getNumber()); 33 | $this->assertSame('Example Book', $this->root->getTitle()); 34 | $this->assertSame('Example Book', $this->root->getNumberAndTitle()); 35 | 36 | $this->assertTrue($this->root->isRoot()); 37 | $this->assertTrue($this->root->isIndex()); 38 | $this->assertSame($this->root, $this->root->getRoot()); 39 | 40 | $this->assertFalse($this->root->hasParent()); 41 | $this->assertNull($this->root->getParent()); 42 | 43 | $this->assertSame(array($this->index), $this->root->getChildren()); 44 | 45 | $this->assertFalse($this->root->hasPrev()); 46 | $this->assertNull($this->root->getPrev()); 47 | $this->assertTrue($this->root->hasNext()); 48 | $this->assertSame($this->index, $this->root->getNext()); 49 | 50 | $this->assertFalse($this->root->hasHeadings()); 51 | $fakeHeadings = array(1, 2, 3); 52 | $this->root->setHeadings($fakeHeadings); 53 | $this->assertTrue($this->root->hasHeadings()); 54 | $this->assertSame($fakeHeadings, $this->root->getHeadings()); 55 | 56 | $this->assertFalse($this->root->hasTocEntries()); 57 | $fakeTocEntries = array(1, 2, 3); 58 | $this->root->setTocEntries($fakeTocEntries); 59 | $this->assertTrue($this->root->hasTocEntries()); 60 | $this->assertSame($fakeTocEntries, $this->root->getTocEntries()); 61 | } 62 | 63 | public function testIndexPage() 64 | { 65 | $this->assertInstanceOf('Bookdown\Bookdown\Content\IndexPage', $this->index); 66 | 67 | $this->assertSame(null, $this->index->getOrigin()); 68 | $this->assertSame('/_site/chapter/index.html', $this->index->getTarget()); 69 | $this->assertSame('/chapter/', $this->index->getHref()); 70 | 71 | $this->assertSame('1.', $this->index->getNumber()); 72 | $this->assertSame('Chapter', $this->index->getTitle()); 73 | $this->assertSame('1. Chapter', $this->index->getNumberAndTitle()); 74 | 75 | $this->assertFalse($this->index->isRoot()); 76 | $this->assertTrue($this->index->isIndex()); 77 | $this->assertSame($this->root, $this->index->getRoot()); 78 | 79 | $this->assertTrue($this->index->hasParent()); 80 | $this->assertSame($this->root, $this->index->getParent()); 81 | 82 | $this->assertSame(array($this->page), $this->index->getChildren()); 83 | 84 | $this->assertTrue($this->index->hasPrev()); 85 | $this->assertSame($this->root, $this->index->getPrev()); 86 | $this->assertTrue($this->index->hasNext()); 87 | $this->assertSame($this->page, $this->index->getNext()); 88 | 89 | $this->assertFalse($this->index->hasHeadings()); 90 | $fakeHeadings = array(1, 2, 3); 91 | $this->index->setHeadings($fakeHeadings); 92 | $this->assertTrue($this->index->hasHeadings()); 93 | $this->assertSame($fakeHeadings, $this->index->getHeadings()); 94 | 95 | $this->assertFalse($this->root->hasTocEntries()); 96 | $fakeTocEntries = array(1, 2, 3); 97 | $this->root->setTocEntries($fakeTocEntries); 98 | $this->assertTrue($this->root->hasTocEntries()); 99 | $this->assertSame($fakeTocEntries, $this->root->getTocEntries()); 100 | } 101 | 102 | public function testPage() 103 | { 104 | $this->assertInstanceOf('Bookdown\Bookdown\Content\Page', $this->page); 105 | 106 | $this->assertSame('/path/to/chapter/section.md', $this->page->getOrigin()); 107 | $this->assertSame('/_site/chapter/section.html', $this->page->getTarget()); 108 | 109 | $this->assertSame('/chapter/section.html', $this->page->getHref()); 110 | $this->assertSame('1.1.', $this->page->getNumber()); 111 | $this->assertNull($this->page->getTitle()); 112 | $this->assertSame('1.1.', $this->page->getNumberAndTitle()); 113 | $this->page->setTitle('Section'); 114 | $this->assertSame('1.1. Section', $this->page->getNumberAndTitle()); 115 | 116 | $this->assertFalse($this->page->isRoot()); 117 | $this->assertFalse($this->page->isIndex()); 118 | $this->assertSame($this->root, $this->page->getRoot()); 119 | 120 | $this->assertTrue($this->page->hasParent()); 121 | $this->assertSame($this->index, $this->page->getParent()); 122 | 123 | $this->assertTrue($this->page->hasPrev()); 124 | $this->assertSame($this->index, $this->page->getPrev()); 125 | $this->assertFalse($this->page->hasNext()); 126 | $this->assertNull($this->page->getNext()); 127 | 128 | $this->assertFalse($this->page->hasHeadings()); 129 | $fakeHeadings = array(1, 2, 3); 130 | $this->page->setHeadings($fakeHeadings); 131 | $this->assertTrue($this->page->hasHeadings()); 132 | $this->assertSame($fakeHeadings, $this->page->getHeadings()); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/Content/HeadingTest.php: -------------------------------------------------------------------------------- 1 | headingFactory = new HeadingFactory(); 11 | } 12 | 13 | public function testHeadingWithId() 14 | { 15 | $heading = $this->headingFactory->newInstance( 16 | '1.2.3.', 17 | 'Example Heading', 18 | '/foo/bar/baz', 19 | '#1-2-3', 20 | '1.2.3' 21 | ); 22 | 23 | $this->assertSame($heading->getNumber(), '1.2.3.'); 24 | $this->assertSame($heading->getTitle(), 'Example Heading'); 25 | $this->assertSame($heading->getId(), '1.2.3'); 26 | $this->assertSame($heading->getLevel(), 3); 27 | $this->assertSame($heading->getHref(), '/foo/bar/baz#1-2-3'); 28 | $this->assertSame($heading->getHrefAnchor(), '#1-2-3'); 29 | $this->assertSame($heading->getAnchor(), '1-2-3'); 30 | } 31 | 32 | public function testHeadingWithoutId() 33 | { 34 | $heading = $this->headingFactory->newInstance( 35 | '1.2.3.', 36 | 'Example Heading', 37 | '/foo/bar/baz', 38 | '#1-2-3' 39 | ); 40 | 41 | $this->assertSame($heading->getNumber(), '1.2.3.'); 42 | $this->assertSame($heading->getTitle(), 'Example Heading'); 43 | $this->assertSame($heading->getId(), null); 44 | $this->assertSame($heading->getLevel(), 3); 45 | $this->assertSame($heading->getHref(), '/foo/bar/baz'); 46 | $this->assertSame($heading->getHrefAnchor(), '#1-2-3'); 47 | $this->assertSame($heading->getAnchor(), '1-2-3'); 48 | } 49 | 50 | public function testHeadingWithAnchoredHref() 51 | { 52 | $heading = $this->headingFactory->newInstance( 53 | '1.2.3.', 54 | 'Example Heading', 55 | '/foo/bar/baz/test.html#7-8-9', 56 | '#1-2-3' 57 | ); 58 | 59 | $this->assertSame($heading->getNumber(), '1.2.3.'); 60 | $this->assertSame($heading->getTitle(), 'Example Heading'); 61 | $this->assertSame($heading->getId(), null); 62 | $this->assertSame($heading->getLevel(), 3); 63 | $this->assertSame($heading->getHref(), '/foo/bar/baz/test.html#7-8-9'); 64 | $this->assertSame($heading->getHrefAnchor(), '#1-2-3'); 65 | $this->assertSame($heading->getAnchor(), '1-2-3'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/FakeFsio.php: -------------------------------------------------------------------------------- 1 | files[$file]; 12 | } 13 | 14 | public function put($file, $data) 15 | { 16 | $this->files[$file] = $data; 17 | } 18 | 19 | public function isDir($dir) 20 | { 21 | return isset($this->dirs[$dir]); 22 | } 23 | 24 | public function mkdir($dir, $mode = 0777, $deep = true) 25 | { 26 | $this->dirs[$dir] = true; 27 | } 28 | 29 | /** 30 | * @param array $dirs 31 | */ 32 | public function setDirs(array $dirs) 33 | { 34 | $this->dirs = $dirs; 35 | } 36 | 37 | /** 38 | * @param array $files 39 | */ 40 | public function setFiles(array $files) 41 | { 42 | $this->files = $files; 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /tests/FsioTest.php: -------------------------------------------------------------------------------- 1 | fsio = new Fsio(); 12 | $this->base = __DIR__ . DIRECTORY_SEPARATOR . 'tmp'; 13 | } 14 | 15 | public function testIsDir() 16 | { 17 | $this->assertTrue($this->fsio->isDir(__DIR__)); 18 | } 19 | 20 | protected function getPath($path) 21 | { 22 | return $this->base . DIRECTORY_SEPARATOR 23 | . str_replace('/', DIRECTORY_SEPARATOR, $path); 24 | } 25 | 26 | public function testMkdir() 27 | { 28 | $dir = $this->getPath('fakedir'); 29 | if ($this->fsio->isDir($dir)) { 30 | rmdir($dir); 31 | } 32 | 33 | $this->assertFalse($this->fsio->isDir($dir)); 34 | $this->fsio->mkdir($dir); 35 | $this->assertTrue($this->fsio->isDir($dir)); 36 | rmdir($dir); 37 | $this->assertFalse($this->fsio->isDir($dir)); 38 | 39 | $this->setExpectedException( 40 | 'Bookdown\Bookdown\Exception', 41 | 'mkdir(): File exists' 42 | ); 43 | $this->fsio->mkdir(__DIR__); 44 | } 45 | 46 | public function testGet() 47 | { 48 | $text = $this->fsio->get(__FILE__); 49 | $this->assertSame('setExpectedException( 52 | 'Bookdown\Bookdown\Exception', 53 | 'No such file or directory' 54 | ); 55 | $this->fsio->get($this->getPath('no-such-file')); 56 | } 57 | 58 | public function testPut() 59 | { 60 | $file = $this->getPath('fakefile'); 61 | if (file_exists($file)) { 62 | unlink($file); 63 | } 64 | 65 | $expect = 'fake text'; 66 | $this->fsio->put($file, $expect); 67 | $actual = $this->fsio->get($file); 68 | $this->assertSame($expect, $actual); 69 | unlink($file); 70 | 71 | $file = $this->getPath('no-such-directory/fakefile'); 72 | $this->setExpectedException( 73 | 'Bookdown\Bookdown\Exception', 74 | 'No such file or directory' 75 | ); 76 | $this->fsio->put($file, $expect); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Process/ConversionProcessTest.php: -------------------------------------------------------------------------------- 1 | fsio = $container->getFsio(); 21 | 22 | $this->fixture = new BookFixture($this->fsio); 23 | 24 | $builder = $container->newProcessorBuilder(); 25 | $this->process = $builder->newProcess( 26 | $this->fixture->rootConfig, 27 | 'Conversion' 28 | ); 29 | } 30 | 31 | public function testConversion() 32 | { 33 | $this->process->__invoke($this->fixture->page); 34 | $expect = '

Title

35 |

Text under title.

36 |

Subtitle code A

37 |

Text under subtitle A.

38 |

Sub-subtitle

39 |

Text under sub-subtitle.

40 |

Subtitle B

41 |

Text under subtitle B.

42 |
43 |

Blockqoute

44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
thth(center)th(right)
tdtdtd
61 | '; 62 | $actual = $this->fsio->get($this->fixture->page->getTarget()); 63 | $this->assertSame($expect, $actual); 64 | } 65 | 66 | public function testConversionNoOrigin() 67 | { 68 | $this->process->__invoke($this->fixture->rootPage); 69 | $actual = $this->fsio->get($this->fixture->rootPage->getTarget()); 70 | $this->assertSame('', $actual); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Process/CopyImageProcessTest.php: -------------------------------------------------------------------------------- 1 | fsio = $container->getFsio(); 28 | $this->fsio->setFiles( 29 | array( 30 | '/path/to/chapter/img/test4.jpg' => '', 31 | '/path/to/chapter/../img/test5.jpg' => '', 32 | ) 33 | ); 34 | 35 | $this->fixture = new $fixtureClass($this->fsio); 36 | 37 | $builder = $container->newProcessorBuilder(); 38 | 39 | $conversion = $builder->newProcess( 40 | $this->fixture->rootConfig, 41 | 'Conversion' 42 | ); 43 | $conversion->__invoke($this->fixture->rootPage); 44 | $conversion->__invoke($this->fixture->indexPage); 45 | $conversion->__invoke($this->fixture->page); 46 | 47 | $this->process = $builder->newProcess( 48 | $this->fixture->rootConfig, 49 | 'CopyImage' 50 | ); 51 | } 52 | 53 | public function testCopyImage() 54 | { 55 | $this->initializeBook('Bookdown\Bookdown\BookImageFixture'); 56 | $this->process->__invoke($this->fixture->page); 57 | 58 | $actual = $this->fsio->get($this->fixture->page->getTarget()); 59 | 60 | // test absolute URI, nothing changed 61 | $this->assertContains('Build Status', $actual); 62 | $this->assertContains('Build Status', $actual); 63 | $this->assertContains('Build Status', $actual); 64 | 65 | // test replacement 66 | $this->assertContains('Build Status', $actual); 67 | $this->assertContains('Build Status', $actual); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Process/Fake/FakeProcess.php: -------------------------------------------------------------------------------- 1 | info[] = $page->getTarget(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Process/Fake/FakeProcessUnimplementedBuilder.php: -------------------------------------------------------------------------------- 1 | fsio = $container->getFsio(); 24 | $this->builder = $container->newProcessorBuilder(); 25 | } 26 | 27 | protected function initializeProcess() 28 | { 29 | $conversion = $this->builder->newProcess( 30 | $this->fixture->rootConfig, 31 | 'Conversion' 32 | ); 33 | $conversion->__invoke($this->fixture->rootPage); 34 | $conversion->__invoke($this->fixture->indexPage); 35 | $conversion->__invoke($this->fixture->page); 36 | 37 | $this->process = $this->builder->newProcess( 38 | $this->fixture->rootConfig, 39 | 'Headings' 40 | ); 41 | } 42 | 43 | public function testHeadings() 44 | { 45 | $this->fixture = new BookFixture($this->fsio); 46 | $this->initializeProcess(); 47 | 48 | $this->assertNull($this->fixture->page->getTitle()); 49 | $this->process->__invoke($this->fixture->page); 50 | 51 | $expect = array( 52 | array( 53 | 'number' => '1.1.', 54 | 'title' => 'Title', 55 | 'id' => '1.1', 56 | 'href' => '/chapter/section.html', 57 | 'hrefAnchor' => null, 58 | 'level' => 2, 59 | ), 60 | array( 61 | 'number' => '1.1.1.', 62 | 'title' => 'Subtitle code A', 63 | 'id' => '1.1.1', 64 | 'href' => '/chapter/section.html', 65 | 'hrefAnchor' => null, 66 | 'level' => 3, 67 | ), 68 | array( 69 | 'number' => '1.1.1.1.', 70 | 'title' => 'Sub-subtitle', 71 | 'id' => '1.1.1.1', 72 | 'href' => '/chapter/section.html', 73 | 'hrefAnchor' => null, 74 | 'level' => 4, 75 | ), 76 | array( 77 | 'number' => '1.1.2.', 78 | 'title' => 'Subtitle B', 79 | 'id' => '1.1.2', 80 | 'href' => '/chapter/section.html', 81 | 'hrefAnchor' => null, 82 | 'level' => 3, 83 | ), 84 | ); 85 | 86 | $headings = $this->fixture->page->getHeadings(); 87 | $this->assertCount(4, $headings); 88 | foreach ($headings as $key => $actual) { 89 | $this->assertSame($expect[$key], $actual->asArray()); 90 | } 91 | 92 | $this->assertSame('Title', $this->fixture->page->getTitle()); 93 | } 94 | 95 | public function testHeadingsOnIndex() 96 | { 97 | $this->fixture = new BookFixture($this->fsio); 98 | $this->initializeProcess(); 99 | 100 | $this->assertSame('Chapter', $this->fixture->indexPage->getTitle()); 101 | 102 | $this->process->__invoke($this->fixture->indexPage); 103 | $headings = $this->fixture->indexPage->getHeadings(); 104 | $this->assertCount(1, $headings); 105 | $expect = array( 106 | array( 107 | 'number' => '1.', 108 | 'title' => 'Chapter', 109 | 'id' => null, 110 | 'href' => '/chapter/', 111 | 'hrefAnchor' => null, 112 | 'level' => 1, 113 | ) 114 | ); 115 | foreach ($headings as $key => $actual) { 116 | $this->assertSame($expect[$key], $actual->asArray()); 117 | } 118 | 119 | $this->assertSame('Chapter', $this->fixture->indexPage->getTitle()); 120 | } 121 | 122 | public function testHeadingsWithoutNumbering() 123 | { 124 | $this->fixture = new BookNumberingFixture($this->fsio); 125 | $this->initializeProcess(); 126 | 127 | $this->assertNull($this->fixture->page->getTitle()); 128 | $this->process->__invoke($this->fixture->page); 129 | 130 | $reflectionClass = new \ReflectionClass($this->process); 131 | $reflectionProperty = $reflectionClass->getProperty('html'); 132 | $reflectionProperty->setAccessible(true); 133 | 134 | $this->assertSame('Title', $this->fixture->page->getTitle()); 135 | 136 | $expected = <<Title 138 |

Text under title.

139 |

Subtitle code A

140 |

Text under subtitle A.

141 |

Sub-subtitle

142 |

Text under sub-subtitle.

143 |

Subtitle B

144 |

Text under subtitle B.

145 | 146 | EOF; 147 | 148 | $this->assertSame($expected, $reflectionProperty->getValue($this->process)); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/Process/IndexProcessTest.php: -------------------------------------------------------------------------------- 1 | fsio = $container->getFsio(); 36 | $this->fsio->setFiles(array('/_site/index.json' => '')); 37 | 38 | $this->fixture = new BookFixture($this->fsio); 39 | 40 | $builder = $container->newProcessorBuilder(); 41 | 42 | /* @var IndexProcess $conversion */ 43 | $conversion = $builder->newProcess( 44 | $this->fixture->rootConfig, 45 | 'Conversion' 46 | ); 47 | $conversion->__invoke($this->fixture->rootPage); 48 | $conversion->__invoke($this->fixture->indexPage); 49 | $conversion->__invoke($this->fixture->page); 50 | 51 | /* @var HeadingsProcess $headings */ 52 | $headings = $builder->newProcess( 53 | $this->fixture->rootConfig, 54 | 'Headings' 55 | ); 56 | $headings->__invoke($this->fixture->rootPage); 57 | $headings->__invoke($this->fixture->indexPage); 58 | $headings->__invoke($this->fixture->page); 59 | 60 | $toc = $builder->newProcess( 61 | $this->fixture->rootConfig, 62 | 'Toc' 63 | ); 64 | $toc->__invoke($this->fixture->rootPage); 65 | $toc->__invoke($this->fixture->indexPage); 66 | $toc->__invoke($this->fixture->page); 67 | 68 | $toc = $builder->newProcess( 69 | $this->fixture->rootConfig, 70 | 'Copyright' 71 | ); 72 | $toc->__invoke($this->fixture->rootPage); 73 | $toc->__invoke($this->fixture->indexPage); 74 | $toc->__invoke($this->fixture->page); 75 | 76 | $render = $builder->newProcess( 77 | $this->fixture->rootConfig, 78 | 'Rendering' 79 | ); 80 | $render->__invoke($this->fixture->rootPage); 81 | $render->__invoke($this->fixture->indexPage); 82 | $render->__invoke($this->fixture->page); 83 | 84 | $this->process = $builder->newProcess( 85 | $this->fixture->rootConfig, 86 | 'Index' 87 | ); 88 | 89 | } 90 | 91 | public function testIndex() 92 | { 93 | $this->process->__invoke($this->fixture->rootPage); 94 | $this->process->__invoke($this->fixture->indexPage); 95 | $this->process->__invoke($this->fixture->page); 96 | 97 | $expect = '[{"id":"\/chapter\/section.html#1-1","title":"1.1. Title","content":"Text under title."},{"id":"\/chapter\/section.html#1-1-1","title":"1.1.1. Subtitle code A","content":"Text under subtitle A."},{"id":"\/chapter\/section.html#1-1-1-1","title":"1.1.1.1. Sub-subtitle","content":"Text under sub-subtitle."},{"id":"\/chapter\/section.html#1-1-2","title":"1.1.2. Subtitle B","content":"Text under subtitle B. Blockqoute th th(center) th(right) td td td "}]'; 98 | 99 | $actual = $this->fsio->get('/_site/index.json'); 100 | $this->assertSame($expect, $actual); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/Process/RenderingProcessTest.php: -------------------------------------------------------------------------------- 1 | fsio = $container->getFsio(); 21 | 22 | $this->fixture = new BookFixture($this->fsio); 23 | 24 | $builder = $container->newProcessorBuilder(); 25 | 26 | $conversion = $builder->newProcess( 27 | $this->fixture->rootConfig, 28 | 'Conversion' 29 | ); 30 | $conversion->__invoke($this->fixture->rootPage); 31 | $conversion->__invoke($this->fixture->indexPage); 32 | $conversion->__invoke($this->fixture->page); 33 | 34 | $headings = $builder->newProcess( 35 | $this->fixture->rootConfig, 36 | 'Headings' 37 | ); 38 | $headings->__invoke($this->fixture->rootPage); 39 | $headings->__invoke($this->fixture->indexPage); 40 | $headings->__invoke($this->fixture->page); 41 | 42 | $toc = $builder->newProcess( 43 | $this->fixture->rootConfig, 44 | 'Toc' 45 | ); 46 | $toc->__invoke($this->fixture->rootPage); 47 | $toc->__invoke($this->fixture->indexPage); 48 | $toc->__invoke($this->fixture->page); 49 | 50 | $toc = $builder->newProcess( 51 | $this->fixture->rootConfig, 52 | 'Copyright' 53 | ); 54 | $toc->__invoke($this->fixture->rootPage); 55 | $toc->__invoke($this->fixture->indexPage); 56 | $toc->__invoke($this->fixture->page); 57 | 58 | $this->process = $builder->newProcess( 59 | $this->fixture->rootConfig, 60 | 'Rendering' 61 | ); 62 | } 63 | 64 | public function testRendering() 65 | { 66 | $this->process->__invoke($this->fixture->indexPage); 67 | $expect = ' 68 | 69 | Chapter 70 | 71 | 117 | 118 | 119 | 120 | 132 | 133 |

1. Chapter

134 |
135 |
1.1. Title
136 |
137 |
1.1.1. Subtitle code A
138 |
139 |
1.1.1.1. Sub-subtitle
140 |
141 |
1.1.2. Subtitle B
142 |
143 |
144 |
145 | 164 | 165 | 166 | '; 167 | $actual = $this->fsio->get($this->fixture->indexPage->getTarget()); 168 | $this->assertSame($expect, $actual); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/Service/CollectorTest.php: -------------------------------------------------------------------------------- 1 | getFsio()); 24 | $this->collector = $container->newCollector(); 25 | } 26 | 27 | public function testCollector() 28 | { 29 | $this->root = $this->collector->__invoke('/path/to/bookdown.json'); 30 | $this->index = $this->root->getNext(); 31 | $this->page = $this->index->getNext(); 32 | 33 | $this->assertCorrectRoot(); 34 | $this->assertCorrectIndex(); 35 | $this->assertCorrectPage(); 36 | } 37 | 38 | public function assertCorrectRoot() 39 | { 40 | $this->assertSame(null, $this->root->getOrigin()); 41 | $this->assertSame('/_site/index.html', $this->root->getTarget()); 42 | 43 | $this->assertSame('/', $this->root->getHref()); 44 | $this->assertSame('', $this->root->getNumber()); 45 | $this->assertSame('Example Book', $this->root->getTitle()); 46 | $this->assertSame('Example Book', $this->root->getNumberAndTitle()); 47 | 48 | $this->assertTrue($this->root->isRoot()); 49 | $this->assertTrue($this->root->isIndex()); 50 | $this->assertSame($this->root, $this->root->getRoot()); 51 | 52 | $this->assertFalse($this->root->hasParent()); 53 | $this->assertNull($this->root->getParent()); 54 | 55 | $this->assertSame(array($this->index), $this->root->getChildren()); 56 | 57 | $this->assertFalse($this->root->hasPrev()); 58 | $this->assertNull($this->root->getPrev()); 59 | $this->assertTrue($this->root->hasNext()); 60 | $this->assertSame($this->index, $this->root->getNext()); 61 | 62 | $this->assertFalse($this->root->hasHeadings()); 63 | $this->assertFalse($this->root->hasTocEntries()); 64 | } 65 | 66 | public function assertCorrectIndex() 67 | { 68 | $this->assertInstanceOf('Bookdown\Bookdown\Content\IndexPage', $this->index); 69 | 70 | $this->assertSame(null, $this->index->getOrigin()); 71 | $this->assertSame('/_site/chapter/index.html', $this->index->getTarget()); 72 | $this->assertSame('/chapter/', $this->index->getHref()); 73 | 74 | $this->assertSame('1.', $this->index->getNumber()); 75 | $this->assertSame('Chapter', $this->index->getTitle()); 76 | $this->assertSame('1. Chapter', $this->index->getNumberAndTitle()); 77 | 78 | $this->assertFalse($this->index->isRoot()); 79 | $this->assertTrue($this->index->isIndex()); 80 | $this->assertSame($this->root, $this->index->getRoot()); 81 | 82 | $this->assertTrue($this->index->hasParent()); 83 | $this->assertSame($this->root, $this->index->getParent()); 84 | 85 | $this->assertSame(array($this->page), $this->index->getChildren()); 86 | 87 | $this->assertTrue($this->index->hasPrev()); 88 | $this->assertSame($this->root, $this->index->getPrev()); 89 | $this->assertTrue($this->index->hasNext()); 90 | $this->assertSame($this->page, $this->index->getNext()); 91 | 92 | $this->assertFalse($this->index->hasHeadings()); 93 | $this->assertFalse($this->root->hasTocEntries()); 94 | } 95 | 96 | public function assertCorrectPage() 97 | { 98 | $this->assertInstanceOf('Bookdown\Bookdown\Content\Page', $this->page); 99 | 100 | $this->assertSame('/path/to/chapter/section.md', $this->page->getOrigin()); 101 | $this->assertSame('/_site/chapter/section.html', $this->page->getTarget()); 102 | 103 | $this->assertSame('/chapter/section.html', $this->page->getHref()); 104 | $this->assertSame('1.1.', $this->page->getNumber()); 105 | $this->assertNull($this->page->getTitle()); 106 | $this->assertSame('1.1.', $this->page->getNumberAndTitle()); 107 | $this->page->setTitle('Section'); 108 | $this->assertSame('1.1. Section', $this->page->getNumberAndTitle()); 109 | 110 | $this->assertFalse($this->page->isRoot()); 111 | $this->assertFalse($this->page->isIndex()); 112 | $this->assertSame($this->root, $this->page->getRoot()); 113 | 114 | $this->assertTrue($this->page->hasParent()); 115 | $this->assertSame($this->index, $this->page->getParent()); 116 | 117 | $this->assertTrue($this->page->hasPrev()); 118 | $this->assertSame($this->index, $this->page->getPrev()); 119 | $this->assertFalse($this->page->hasNext()); 120 | $this->assertNull($this->page->getNext()); 121 | 122 | $this->assertFalse($this->page->hasHeadings()); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/Service/ProcessorBuilderTest.php: -------------------------------------------------------------------------------- 1 | builder = $container->newProcessorBuilder(); 17 | } 18 | 19 | public function testNewProcessor() 20 | { 21 | $rootConfig = new RootConfig('/path/to/bookdown.json', '{ 22 | "title": "Example Book", 23 | "content": [ 24 | {"foo": "foo.md"} 25 | ], 26 | "target": "_site/" 27 | }'); 28 | 29 | $this->assertInstanceOf( 30 | 'Bookdown\Bookdown\Service\Processor', 31 | $this->builder->newProcessor($rootConfig) 32 | ); 33 | } 34 | 35 | public function testNewProcessUnimplementedContainer() 36 | { 37 | $rootConfig = new RootConfig('/path/to/bookdown.json', '{ 38 | "title": "Example Book", 39 | "content": [ 40 | {"foo": "foo.md"} 41 | ], 42 | "target": "_site/", 43 | "tocProcess": "Bookdown\\\\Bookdown\\\\Process\\\\Fake\\\\FakeProcessUnimplementedBuilder" 44 | }'); 45 | 46 | $this->setExpectedException( 47 | 'Bookdown\Bookdown\Exception', 48 | "Bookdown\Bookdown\Process\Fake\FakeProcessUnimplementedBuilder' does not implement ProcessBuilderInterface" 49 | ); 50 | 51 | $this->builder->newProcess($rootConfig, 'Toc'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Service/ProcessorTest.php: -------------------------------------------------------------------------------- 1 | logger = $container->getLogger(); 24 | $this->fsio = $container->getFsio(); 25 | $this->setUpFsio(); 26 | 27 | $collector = $container->newCollector(); 28 | $this->root = $collector('/path/to/bookdown.json'); 29 | 30 | $this->processes = array( 31 | new FakeProcess(), 32 | new FakeProcess(), 33 | new FakeProcess() 34 | ); 35 | } 36 | 37 | protected function setUpFsio() 38 | { 39 | $this->fsio->put('/path/to/bookdown.json', '{ 40 | "title": "Example Book", 41 | "content": [ 42 | {"chapter-1": "chapter-1/bookdown.json"} 43 | ], 44 | "target": "/_site" 45 | }'); 46 | 47 | $this->fsio->put('/path/to/chapter-1/bookdown.json', '{ 48 | "title": "Chapter 1", 49 | "content": [ 50 | {"section-1": "section-1.md"} 51 | ] 52 | }'); 53 | } 54 | 55 | public function testProcessor() 56 | { 57 | $processor = new Processor( 58 | $this->logger, 59 | $this->processes 60 | ); 61 | 62 | $processor($this->root); 63 | 64 | $expect = array( 65 | 0 => '/_site/index.html', 66 | 1 => '/_site/chapter-1/index.html', 67 | 2 => '/_site/chapter-1/section-1.html', 68 | ); 69 | 70 | foreach ($this->processes as $process) { 71 | $this->assertSame($expect, $process->info); 72 | } 73 | } 74 | } 75 | --------------------------------------------------------------------------------