GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Epub2Publisher::loadContents()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 12
cts 12
cp 1
rs 9.6333
c 0
b 0
f 0
cc 3
nc 3
nop 0
crap 3
1
<?php
2
/*
3
 * This file is part of the trefoil application.
4
 *
5
 * (c) Miguel Angel Gabriel <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Trefoil\Publishers;
12
13
use Easybook\Events\BaseEvent;
14
use Easybook\Events\EasybookEvents as Events;
15
use Symfony\Component\Finder\Finder;
16
use Symfony\Component\Finder\SplFileInfo;
17
use Symfony\Component\Process\Process;
18
use Trefoil\Util\Toolkit;
19
20
/**
21
 * It publishes the book as an EPUB file. All the internal links are transformed
22
 * into clickable cross-section book links.
23
 *
24
 * -- This is Trefoil own implementation of this publisher --
25
 * It is based on the original Easybook Epub2Publisher with some added functionality
26
 * and fixes.
27
 */
28
class Epub2Publisher extends HtmlPublisher
29
{
30
    // 'toc' content type usually makes no sense in epub books (see below)
31
    // 'cover' is a very special content for epub books
32
    protected $excludedElements = array('cover', 'toc');
33
34 34
    public function loadContents()
35
    {
36
        // strip excluded elements before loading book contents
37 34
        $contents = array();
38 34
        foreach ($this->app->book('contents') as $content) {
39 34
            if (!in_array($content['element'], $this->excludedElements)) {
40 34
                $contents[] = $content;
41 34
            }
42 34
        }
43 34
        $this->app->book('contents', $contents);
44
45 34
        parent::loadContents();
46
47
        /* assign the normalized page names here to make them available
48
         * to the plugins.
49
        */
50 34
        $bookItems = $this->normalizePageNames($this->app['publishing.items']);
51 34
        $this->app['publishing.items'] = $bookItems;
52 34
    }
53
54
    /**
55
     * Overrides the base publisher method to avoid the decoration of the book items.
56
     * Instead of using the regular Twig templates based on the item type (e.g. chapter),
57
     * ePub books items are decorated with some special Twig templates.
58
     */
59 34
    public function decorateContents()
60
    {
61 34
        $decoratedItems = array();
62
63 34
        foreach ($this->app['publishing.items'] as $item) {
64 34
            $this->app['publishing.active_item'] = $item;
65
66
            // filter the original item content before decorating it
67 34
            $event = new BaseEvent($this->app);
68 34
            $this->app->dispatch(Events::PRE_DECORATE, $event);
69
70
            // try first to render the specific template for each content
71
            // type, if it exists (e.g. toc.twig, chapter.twig, etc.) and
72
            // use chunk.twig as the fallback template
73
            $templateVariables = array(
74 34
                'item'           => $item,
75 34
                'has_custom_css' => (null !== $this->getCustomCssFile()),
76 34
            );
77 34
            try {
78 34
                $templateName = $item['config']['element'] . '.twig';
79 34
                $item['content'] = $this->app->render($templateName, $templateVariables);
80 34
            } catch (\Twig_Error_Loader $e) {
81 34
                $item['content'] = $this->app->render('chunk.twig', $templateVariables);
82
            }
83 34
            $this->app['publishing.active_item'] = $item;
84
85 34
            $event = new BaseEvent($this->app);
86 34
            $this->app->dispatch(Events::POST_DECORATE, $event);
87
88
            // get again 'item' object because POST_DECORATE event can modify it
89 34
            $decoratedItems[] = $this->app['publishing.active_item'];
90 34
        }
91
92 34
        $this->app['publishing.items'] = $decoratedItems;
93 34
    }
94
95 34
    public function assembleBook()
96
    {
97 34
        $bookTmpDir = $this->prepareBookTemporaryDirectory();
98
99
        // generate easybook CSS file
100 34
        if ($this->app->edition('include_styles')) {
101 34
            $this->app->render(
102 34
                      '@theme/style.css.twig',
103 34
                      array('resources_dir' => '..'),
104
                      $bookTmpDir . '/book/OEBPS/css/easybook.css'
105 34
            );
106 34
        }
107
108
        // generate custom CSS file
109 34
        $customCss = $this->getCustomCssFile();
110 34
        $customCssName = pathinfo($customCss, PATHINFO_BASENAME);
111 34
        if ('style.css' == $customCssName) {
112 19
            $this->app['filesystem']->copy(
113 8
                                    $customCss,
114 19
                                    $bookTmpDir . '/book/OEBPS/css/styles.css',
115
                                    true
116 8
            );
117 8
        } else {
118
            // new in Trefoil:
119
            // generate custom CSS file from template
120 26
            if ('style.css.twig' == $customCssName) {
121
                $this->app->render(
122
                          'style.css.twig',
123
                          array(),
124
                          $bookTmpDir . '/book/OEBPS/css/styles.css'
125
                );
126
            }
127
        }
128 34
        $hasCustomCss = ($customCss !== null);
129
130 34
        $bookItems = $this->app['publishing.items'];
131
132
        // generate one HTML page for every book item
133 34
        foreach ($bookItems as $item) {
134 34
            $renderedTemplatePath = $bookTmpDir . '/book/OEBPS/' . $item['page_name'] . '.html';
135
136
            // book items have already been rendered, so we just need
137
            // to copy them to the temp dir
138 34
            file_put_contents($renderedTemplatePath, $item['content']);
139 34
        }
140
141 34
        $bookImages = $this->prepareBookImages($bookTmpDir . '/book/OEBPS/images');
142 34
        $bookCover = $this->prepareBookCoverImage($bookTmpDir . '/book/OEBPS/images');
143 34
        $bookFonts = $this->prepareBookFonts($bookTmpDir . '/book/OEBPS/fonts');
144
145
        // ensure an empty fonts dir is not left begind (epubcheck error)
146 34
        $fontFiles = Finder::create()->files()->in($bookTmpDir . '/book/OEBPS/fonts');
147 34
        if (0 == $fontFiles->count()) {
148 34
            $this->app['filesystem']->remove($bookTmpDir . '/book/OEBPS/fonts');
149 34
        }
150
151
        // generate the book cover page
152 34
        $this->app->render(
153 34
                  'cover.twig',
154 34
                  array('customCoverImage' => $bookCover),
155
                  $bookTmpDir . '/book/OEBPS/titlepage.html'
156 34
        );
157
158
        // generate the OPF file (the ebook manifest)
159 34
        $this->app->render(
160 34
                  'content.opf.twig',
161
                  array(
162 34
                      'cover'          => $bookCover,
163 34
                      'has_custom_css' => $hasCustomCss,
164 34
                      'fonts'          => $bookFonts,
165 34
                      'images'         => $bookImages,
166
                      'items'          => $bookItems
167 34
                  ),
168
                  $bookTmpDir . '/book/OEBPS/content.opf'
169 34
        );
170
171
        // generate the NCX file (the table of contents)
172 34
        $this->app->render(
173 34
                  'toc.ncx.twig',
174 34
                  array('items' => $bookItems),
175
                  $bookTmpDir . '/book/OEBPS/toc.ncx'
176 34
        );
177
178
        // generate container.xml and mimetype files
179 34
        $this->app->render(
180 34
                  'container.xml.twig',
181 34
                  array(),
182
                  $bookTmpDir . '/book/META-INF/container.xml'
183 34
        );
184 34
        $this->app->render(
185 34
                  'mimetype.twig',
186 34
                  array(),
187
                  $bookTmpDir . '/book/mimetype'
188 34
        );
189
190 34
        $this->fixInternalLinks($bookTmpDir . '/book/OEBPS');
191
192
        // compress book contents as ZIP file and rename to .epub
193 34
        $this->zipBookContents($bookTmpDir . '/book', $bookTmpDir . '/book.zip');
194 34
        $this->app['filesystem']->copy(
195 34
                                $bookTmpDir . '/book.zip',
196 34
                                $this->app['publishing.dir.output'] . '/book.epub',
197
                                true
198 34
        );
199
200
        // remove temp directory used to build the book
201 34
        $this->app['filesystem']->remove($bookTmpDir);
202 34
    }
203
204
    /**
205
     * Prepares the temporary directory where the book contents are generated
206
     * before packing them into the resulting EPUB file. It also creates the
207
     * full directory structure required for EPUB books.
208
     *
209
     * @return string The absolute path of the directory created.
210
     */
211 34
    private function prepareBookTemporaryDirectory()
212
    {
213 34
        $bookDir = $this->app['app.dir.cache'] . '/'
214 34
            . uniqid($this->app['publishing.book.slug']);
215
216 34
        $this->app['filesystem']->mkdir(
217
                                array(
218 34
                                    $bookDir,
219 34
                                    $bookDir . '/book',
220 34
                                    $bookDir . '/book/META-INF',
221 34
                                    $bookDir . '/book/OEBPS',
222 34
                                    $bookDir . '/book/OEBPS/css',
223 34
                                    $bookDir . '/book/OEBPS/images',
224 34
                                    $bookDir . '/book/OEBPS/fonts',
225
                                )
226 34
        );
227
228 34
        return $bookDir;
229
    }
230
231
    /**
232
     * It prepares the book cover image (if the book defines one).
233
     *
234
     * @param string $targetDir The directory where the cover image is copied.
235
     *
236
     * @return array|null Book cover image data or null if the book doesn't
237
     *                    include a cover image.
238
     */
239 34 View Code Duplication
    private function prepareBookCoverImage($targetDir)
240
    {
241 34
        $cover = null;
242
243 34
        if (null !== $image = $this->app->getCustomCoverImage()) {
244
            list($width, $height, $type) = getimagesize($image);
245
246
            $cover = array(
247
                'height'    => $height,
248
                'width'     => $width,
249
                'filePath'  => 'images/' . basename($image),
250
                'mediaType' => image_type_to_mime_type($type)
251
            );
252
253
            $this->app['filesystem']->copy($image, $targetDir . '/' . basename($image));
254
        }
255
256 34
        return $cover;
257
    }
258
259
    /**
260
     * It prepares the book fonts by copying them into the appropriate
261
     * temporary directory. It also prepares an array with all the font
262
     * data needed later to generate the full ebook contents manifest.
263
     *
264
     * For now, epub books only include the Inconsolata font to display
265
     * their code listings.
266
     *
267
     * @param string $targetDir The directory where the fonts are copied.
268
     *
269
     * @throws \RuntimeException
270
     * @return array             Font data needed to create the book manifest.
271
     */
272 34
    private function prepareBookFonts($targetDir)
273
    {
274
        // new in trefoil
275 34
        if (!$this->app->edition('include_fonts')) {
276 34
            return array();
277
        }
278
279 View Code Duplication
        if (!file_exists($targetDir)) {
280
            throw new \RuntimeException(sprintf(
281
                                            " ERROR: Books fonts couldn't be copied because \n"
282
                                            . " the given '%s' \n"
283
                                            . " directory doesn't exist.",
284
                                            $targetDir
285
                                        ));
286
        }
287
288
        $sourceDirs = array();
289
        // the standard easybook fonts dir
290
        //     <easybook>/app/Resources/Fonts/
291
        $sourceDirs[] = $this->app['app.dir.resources'] . '/Fonts';
292
293
        // new in trefoil
294
        // fonts inside the Resources directory of the <format> directory of the current theme
295
        // (which can be set via command line argument):
296
        //     <current-theme-dir>/<current-theme>/<format>/Resources/images/
297
        // where <current_theme_dir> can be either
298
        //        <trefoil-dir>/app/Resources/Themes/
299
        //         or
300
        //        <the path set with the "--dir" publish command line argument>
301
        // 'Common' format takes precedence
302
        $sourceDirs[] = Toolkit::getCurrentResourcesDir($this->app, 'Common') . '/Fonts';
303
        $sourceDirs[] = Toolkit::getCurrentResourcesDir($this->app) . '/Fonts';
304
305
        // the fonts inside the book
306
        //     <book-dir>/Fonts/
307
        $sourceDirs[] = $this->app['publishing.dir.resources'] . '/Fonts';
308
309
        // new in trefoil
310
        $allowedFonts = $this->app->edition('fonts');
311
312
        $fontsData = array();
313
        $i = 1;
314
        foreach ($sourceDirs as $fontDir) {
315
            if (file_exists($fontDir)) {
316
317
                $fonts = Finder::create()
318
                               ->files()
319
                               ->name('*.ttf')
320
                               ->name('*.otf')
321
                               ->sortByName()
322
                               ->in($fontDir);
323
324
                /** @var $font SplFileInfo */
325
                foreach ($fonts as $font) {
326
                    /*@var $font SplFileInfo */
327
328
                    $fontName = $font->getBasename('.' . $font->getExtension());
329
330
                    if (is_array($allowedFonts)) {
331
                        if (!in_array($fontName, $allowedFonts)) {
332
                            continue;
333
                        }
334
                    }
335
336
                    $this->app['filesystem']->copy(
337
                                            $font->getPathName(),
338
                                            $targetDir . '/' . $font->getFileName()
339
                    );
340
341
                    $fontsData[] = array(
342
                        'id'        => 'font-' . $i++,
343
                        'filePath'  => 'fonts/' . $font->getFileName(),
344
                        'mediaType' => finfo_file(finfo_open(FILEINFO_MIME_TYPE), $font->getPathName())
345
                    );
346
                }
347
            }
348
        }
349
350
        return $fontsData;
351
    }
352
353
    /**
354
     * The generated HTML pages aren't named after the items' original slugs
355
     * (e.g. introduction-to-lorem-ipsum.html) but using their content types
356
     * and numbers (e.g. chapter-1.html).
357
     *
358
     * This method creates a new property for each item called 'page_name' which
359
     * stores the normalized page name that should have this chunk.
360
     *
361
     * @param array $items The original book items.
362
     *
363
     * @return array The book items with their new 'page_name' property.
364
     */
365 34
    private function normalizePageNames($items)
366
    {
367 34
        $itemsWithNormalizedPageNames = array();
368
369 34
        foreach ($items as $item) {
370
371 34
            $itemPageName = array_key_exists('number', $item['config'])
372 34
                ? $item['config']['element'] . ' ' . $item['config']['number']
373 34
                : $item['config']['element'];
374
375 34
            $item['page_name'] = $this->app->slugifyUniquely($itemPageName);
376
377 34
            $itemsWithNormalizedPageNames[] = $item;
378 34
        }
379
380 34
        return $itemsWithNormalizedPageNames;
381
    }
382
383
    /*
384
     * It creates the ZIP file of the .epub book contents.
385
     *
386
     * The PHP ZIP extension is not suitable for generating EPUB files.
387
     *
388
     * This method will generate he ZIP file using the OS 'zip' command.
389
     *
390
     * @param string $directory Book contents directory
391
     * @param string $zip_file  The path of the generated ZIP file
392
     */
393 34
    private function zipBookContents($directory, $zip_file)
394
    {
395
        // After several hours trying to create ZIP files with lots of PHP
396
        // tools and libraries (Archive_Zip, Pclzip, zetacomponents/archive, ...)
397
        // I can't produce a proper ZIP file for ebook readers.
398
        // Therefore, if ZIP extension isn't enabled, the ePub ZIP file is
399
        // generated by executing 'zip' command
400
401
        // check if 'zip' command exists
402 34
        $process = new Process('zip');
403 34
        $process->run();
404
405 34
        if (!$process->isSuccessful()) {
406
            throw new \RuntimeException(
407
                "[ERROR] You must enable the ZIP extension in PHP \n"
408
                . " or your system should be able to execute 'zip' console command."
409
            );
410
        }
411
412
        // To generate the ePub file, you must execute the following commands:
413
        //   $ cd /path/to/ebook/contents
414
        //   $ zip -X0 book.zip mimetype
415
        //   $ zip -rX9 book.zip * -x mimetype
416 34
        $command = sprintf(
417 34
            'cd %s && zip -X0 %s mimetype && zip -rX9 %s * -x mimetype',
418 34
            $directory,
419 34
            $zip_file,
420
            $zip_file
421 34
        );
422
423 34
        $process = new Process($command);
424 34
        $process->run();
425
426 34
        if (!$process->isSuccessful()) {
427
            throw new \RuntimeException(
428
                "[ERROR] 'zip' command execution wasn't successful.\n\n"
429
                . "Executed command:\n"
430
                . " $command\n\n"
431
                . "Result:\n"
432
                . $process->getErrorOutput()
433
            );
434
        }
435 34
    }
436
437
    /**
438
     * If fixes the internal links of the book (the links that point to chapters
439
     * and sections of the book).
440
     *
441
     * The author of the book always uses relative links, such as:
442
     *   see <a href="#new-content-types">this section</a> for more information
443
     *
444
     * In order to work, the relative URIs must be replaced by absolute URIs:
445
     *   see <a href="chapter3/page-slug.html#new-content-types">this section</a>
446
     *
447
     * Unlike books published as websites, the absolute URIs of the ePub books
448
     * cannot start with './' or '../' In other words, ./chapter.html and
449
     * ./chapter.html#section-slug are wrong and chapter.html or
450
     * chapter.html#section-slug are right.
451
     *
452
     * @param string $chunksDir The directory where the book's HTML page/chunks
453
     *                          are stored
454
     */
455 34
    private function fixInternalLinks($chunksDir)
456
    {
457 34
        $generatedChunks = Finder::create()->files()->name('*.html')->in($chunksDir);
458
459
        // maps the original internal links (e.g. #new-content-types)
460
        // with the correct absolute URL needed for a website
461
        // (e.g. chapter-3/advanced-features.html#new-content-types
462 34
        $internalLinkMapper = array();
463
464
        //look for all the IDs of html tags in the rendered book
465 34
        foreach ($generatedChunks as $chunk) {
466
            /** @var $chunk SplFileInfo */
467 34
            $htmlContent = file_get_contents($chunk->getPathname());
468
469 34
            $matches = array();
470
471 34
            $numAnchors = preg_match_all(
472 34
                '/<.*id="(?<id>.*)"/U',
473 34
                $htmlContent,
474 34
                $matches,
475
                PREG_SET_ORDER
476 34
            );
477
478 34
            if ($numAnchors > 0) {
479 34
                foreach ($matches as $match) {
480 34
                    $relativeUri = '#' . $match['id'];
481 34
                    $absoluteUri = $chunk->getRelativePathname() . $relativeUri;
482
483 34
                    $internalLinkMapper[$relativeUri] = $absoluteUri;
484 34
                }
485 34
            }
486 34
        }
487
488
        // replace the internal relative URIs for the mapped absolute URIs
489 34
        foreach ($generatedChunks as $chunk) {
490
            /** @var $chunk SplFileInfo */
491 34
            $htmlContent = file_get_contents($chunk->getPathname());
492
493 34
            $htmlContent = preg_replace_callback(
494 34
                '/<a href="(?<uri>#.*)"(?<attr>.*)>(?<content>.*)<\/a>/Us',
495 34
                function ($matches) use ($internalLinkMapper) {
496 12
                    if (array_key_exists($matches['uri'], $internalLinkMapper)) {
497 12
                        $newUri = $internalLinkMapper[$matches['uri']];
498 12
                    } else {
499 1
                        $newUri = $matches['uri'];
500
                    }
501
502
                    // add "internal" to link class
503 12
                    $attributes = Toolkit::parseHTMLAttributes($matches['attr']);
504 12
                    $attributes['class'] = isset($attributes['class']) ? $attributes['class'] . ' ' : '';
505 34
                    $attributes['class'] .= 'internal';
506
507
                    // render the new link tag
508 12
                    $attributes['href'] = $newUri;
509 12
                    $tagHtml = Toolkit::renderHTMLTag('a', $matches['content'], $attributes);
510
511 12
                    return $tagHtml;
512
513 34
                },
514
                $htmlContent
515 34
            );
516
517 34
            file_put_contents($chunk->getPathname(), $htmlContent);
518 34
        }
519 34
    }
520
}
521