├── .gitignore
├── templates
├── body.php
├── core.php
├── main.php
├── navheader.php
├── toc.php
├── head.php
└── navfooter.php
├── tests
├── Process
│ ├── Fake
│ │ ├── FakeProcessUnimplementedBuilder.php
│ │ └── FakeProcess.php
│ ├── ConversionProcessTest.php
│ ├── CopyImageProcessTest.php
│ ├── IndexProcessTest.php
│ ├── HeadingsProcessTest.php
│ └── RenderingProcessTest.php
├── FakeFsio.php
├── Service
│ ├── ProcessorBuilderTest.php
│ ├── ProcessorTest.php
│ └── CollectorTest.php
├── Config
│ ├── ConfigFactoryTest.php
│ ├── RootConfigTest.php
│ └── IndexConfigTest.php
├── ContainerTest.php
├── CommandTest.php
├── FsioTest.php
├── Content
│ ├── HeadingTest.php
│ └── ContentTest.php
├── BookImageFixture.php
├── BookNumberingFixture.php
├── BookTocFixture.php
└── BookFixture.php
├── .scrutinizer.yml
├── phpunit.php
├── src
├── Exception.php
├── Process
│ ├── ProcessInterface.php
│ ├── ProcessBuilderInterface.php
│ ├── Toc
│ │ ├── TocProcessBuilder.php
│ │ └── TocProcess.php
│ ├── Index
│ │ ├── IndexProcessBuilder.php
│ │ └── IndexProcess.php
│ ├── Copyright
│ │ ├── CopyrightProcessBuilder.php
│ │ └── CopyrightProcess.php
│ ├── CopyImage
│ │ ├── CopyImageProcessBuilder.php
│ │ └── CopyImageProcess.php
│ ├── Headings
│ │ ├── HeadingsProcessBuilder.php
│ │ └── HeadingsProcess.php
│ ├── Rendering
│ │ ├── RenderingProcess.php
│ │ └── RenderingProcessBuilder.php
│ └── Conversion
│ │ ├── ConversionProcessBuilder.php
│ │ └── ConversionProcess.php
├── Content
│ ├── HeadingFactory.php
│ ├── TocHeading.php
│ ├── RootPage.php
│ ├── PageFactory.php
│ ├── Heading.php
│ ├── IndexPage.php
│ ├── TocHeadingIterator.php
│ └── Page.php
├── Service
│ ├── Timer.php
│ ├── Processor.php
│ ├── Service.php
│ ├── ProcessorBuilder.php
│ └── Collector.php
├── Config
│ ├── ConfigFactory.php
│ └── IndexConfig.php
├── Stdlog.php
├── Fsio.php
├── Command.php
└── Container.php
├── phpunit.xml.dist
├── .travis.yml
├── CONTRIBUTING.md
├── bin
└── bookdown
├── LICENSE.md
├── composer.json
├── CHANGELOG.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | composer.lock
3 | vendor/
4 | _site/
5 | tests/tmp
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/Process/Fake/FakeProcessUnimplementedBuilder.php:
--------------------------------------------------------------------------------
1 | info[] = $page->getTarget();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Exception.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ./tests
5 |
6 |
7 |
8 |
9 | ./src
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Process/ProcessInterface.php:
--------------------------------------------------------------------------------
1 | newCommand($GLOBALS);
28 | $code = $command();
29 | exit($code);
30 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/src/Process/ProcessBuilderInterface.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/Process/Toc/TocProcessBuilder.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/Process/Index/IndexProcessBuilder.php:
--------------------------------------------------------------------------------
1 |
2 | page->getTitle(); ?>
3 |
4 |
50 |
51 |
--------------------------------------------------------------------------------
/templates/navfooter.php:
--------------------------------------------------------------------------------
1 | page->getPrev();
3 | $parent = $this->page->getParent();
4 | $next = $this->page->getNext();
5 | ?>
6 |
7 |
40 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/Content/RootPage.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/src/Process/Copyright/CopyrightProcess.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/Content/PageFactory.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 |
--------------------------------------------------------------------------------
/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 | | th |
49 | th(center) |
50 | th(right) |
51 |
52 |
53 |
54 |
55 | | td |
56 | td |
57 | td |
58 |
59 |
60 |
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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Process/Rendering/RenderingProcess.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 |
--------------------------------------------------------------------------------
/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/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('
', $actual);
62 | $this->assertContains('
', $actual);
63 | $this->assertContains('
', $actual);
64 |
65 | // test replacement
66 | $this->assertContains('
', $actual);
67 | $this->assertContains('
', $actual);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Fsio.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bookdown
2 |
3 | [](https://scrutinizer-ci.com/g/bookdown/Bookdown.Bookdown/?branch=master)
4 | [](https://scrutinizer-ci.com/g/bookdown/Bookdown.Bookdown/?branch=master)
5 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/Conversion/ConversionProcess.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/Content/IndexPage.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/Process/HeadingsProcessTest.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/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Service/Collector.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/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/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/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/Process/Headings/HeadingsProcess.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 |
--------------------------------------------------------------------------------