/** * @dataProvider getConfigFileName */ public function testEasybookConfiguration($configFileName) { $this->app = $this->getApplication($configFileName); $config = $this->app['configurator']->loadBookFileConfiguration(null); $easybookConfig = isset($config['easybook']) ? $config['easybook']['parameters'] : array(); $expectedConfiguration = Toolkit::array_deep_merge_and_replace($this->getEasybookDefaultParameters(), $easybookConfig); $this->assertEasybookConfiguration($expectedConfiguration); }
public function testUuidMethodGeneratesRandomIds() { $uuids = array(); while (count($uuids) < 1000) { $uuids[] = Toolkit::uuid(); } $this->assertEquals(count($uuids), count(array_unique($uuids))); }
public function testBookPublish() { $console = new ConsoleApplication($this->app); // find the test books $books = $this->app['finder']->directories()->name('book*')->depth(0)->in(__DIR__ . '/fixtures'); foreach ($books as $book) { $this->markTestSkipped('Temporarily marked as skipeed until we update these tests to be less fragile with whitespaces.'); $slug = $book->getFileName(); if ('book5' == $slug && (version_compare(phpversion(), '5.4.0', '<') || !extension_loaded('intl'))) { $this->markTestSkipped('This test requires PHP 5.4.0+ with the intl extension enabled (the book contains a lot of non-latin characters that need the native PHP transliterator)'); } // mirror test book contents in temp dir $this->filesystem->mirror(__DIR__ . '/fixtures/' . $slug . '/input', $this->tmpDir . '/' . $slug); // look for and publish all the book editions $bookConfig = Yaml::parse($this->tmpDir . '/' . $slug . '/config.yml'); $editionNames = array_keys($bookConfig['book']['editions']); foreach ($editionNames as $editionName) { // publish each book edition $input = new ArrayInput(array('command' => 'publish', 'slug' => $slug, 'edition' => $editionName, '--dir' => $this->tmpDir)); $console->find('publish')->run($input, new NullOutput()); // assert that generated files are exactly the same as expected $generatedFiles = $this->app['finder']->files()->notName('.gitignore')->in($this->tmpDir . '/' . $slug . '/Output/' . $editionName); foreach ($generatedFiles as $file) { if ('epub' == $file->getExtension()) { // unzip both files to compare its contents $workDir = $this->tmpDir . '/' . $slug . '/unzip/' . $editionName; $generated = $workDir . '/generated'; $expected = $workDir . '/expected'; Toolkit::unzip($file->getRealPath(), $generated); Toolkit::unzip(__DIR__ . '/fixtures/' . $slug . '/expected/' . $editionName . '/' . $file->getRelativePathname(), $expected); // assert that generated files are exactly the same as expected $genFiles = $this->app['finder']->files()->notName('.gitignore')->in($generated); foreach ($genFiles as $genFile) { $this->assertFileEquals($expected . '/' . $genFile->getRelativePathname(), $genFile->getPathname(), sprintf("ERROR on {$book}:\n '%s' file (into ZIP file '%s') not properly generated", $genFile->getRelativePathname(), $file->getPathName())); } // assert that all required files are generated $this->checkForMissingFiles($expected, $generated); } else { $this->assertFileEquals(__DIR__ . '/fixtures/' . $slug . '/expected/' . $editionName . '/' . $file->getRelativePathname(), $file->getPathname(), sprintf("'%s' file not properly generated", $file->getPathname())); } } // assert that all required files are generated $this->checkForMissingFiles(__DIR__ . '/fixtures/' . $slug . '/expected/' . $editionName, $this->tmpDir . '/' . $slug . '/Output/' . $editionName); // assert than book publication took less than 5 seconds $this->assertLessThan(5, $this->app['app.timer.finish'] - $this->app['app.timer.start'], sprintf("Publication of '%s' edition for '%s' book took more than 5 seconds", $editionName, $slug)); // reset app state before the next publishing $this->app = new Application(); $console = new ConsoleApplication($this->app); } } }
public function register(Container $app) { $app['twig.options'] = array('autoescape' => false, 'charset' => $app['app.charset'], 'debug' => $app['app.debug'], 'strict_variables' => $app['app.debug']); $app['twig.loader'] = function () use($app) { $theme = ucfirst($app->edition('theme')); $format = Toolkit::camelize($app->edition('format'), true); $loader = new \Twig_Loader_Filesystem($app['app.dir.themes']); // Base theme (common styles per edition type) // <easybook>/app/Resources/Themes/Base/<edition-type>/Templates/<template-name>.twig $baseThemeDir = sprintf('%s/Base/%s/Templates', $app['app.dir.themes'], $format); $loader->addPath($baseThemeDir); $loader->addPath($baseThemeDir, 'theme'); $loader->addPath($baseThemeDir, 'theme_base'); // Book theme (configured per edition in 'config.yml') // <easybook>/app/Resources/Themes/<theme>/<edition-type>/Templates/<template-name>.twig $bookThemeDir = sprintf('%s/%s/%s/Templates', $app['app.dir.themes'], $theme, $format); $loader->prependPath($bookThemeDir); $loader->prependPath($bookThemeDir, 'theme'); $userTemplatePaths = array($app['publishing.dir.templates'], sprintf('%s/%s', $app['publishing.dir.templates'], strtolower($format)), sprintf('%s/%s', $app['publishing.dir.templates'], $app['publishing.edition'])); foreach ($userTemplatePaths as $path) { if (file_exists($path)) { $loader->prependPath($path); } } $defaultContentPaths = array(sprintf('%s/Base/%s/Contents', $app['app.dir.themes'], $format), sprintf('%s/%s/%s/Contents', $app['app.dir.themes'], $theme, $format)); foreach ($defaultContentPaths as $path) { if (file_exists($path)) { $loader->prependPath($path, 'content'); } } return $loader; }; $app['twig'] = function () use($app) { $twig = new \Twig_Environment($app['twig.loader'], $app['twig.options']); $twig->addExtension(new TwigCssExtension()); $twig->addGlobal('app', $app); if (null !== ($bookConfig = $app['publishing.book.config'])) { $twig->addGlobal('book', $bookConfig['book']); $publishingEdition = $app['publishing.edition']; $editions = $app->book('editions'); $twig->addGlobal('edition', $editions[$publishingEdition]); } return $twig; }; }
public function build() { echo sprintf("\nBuilding easybook %s package\n%s\n", $this->version, str_repeat('=', 80)); // add book script $this->addFile(new \SplFileInfo($this->rootDir . '/book')); // add autoloaders $this->addFile(new \SplFileInfo($this->rootDir . '/vendor/autoload.php')); $this->addFile(new \SplFileInfo($this->rootDir . '/vendor/composer/ClassLoader.php')); $this->addFile(new \SplFileInfo($this->rootDir . '/vendor/composer/autoload_classmap.php')); $this->addFile(new \SplFileInfo($this->rootDir . '/vendor/composer/autoload_namespaces.php')); // add resources $finder = new Finder(); $finder->files()->ignoreVCS(true)->notName('.DS_Store')->in($this->rootDir . '/app/Resources'); foreach ($finder as $file) { $this->addFile($file); } // add sample books $finder = new Finder(); $finder->files()->ignoreVCS(true)->notName('.DS_Store')->exclude('Output')->exclude('Resources')->in(array($this->rootDir . '/doc/easybook-doc-en', $this->rootDir . '/doc/easybook-doc-es')); foreach ($finder as $file) { $this->addFile($file); } // add core classes $finder = new Finder(); $finder->files()->ignoreVCS(true)->name('*.php')->notName('.DS_Store')->exclude('Tests')->notName('Builder.php')->in($this->rootDir . '/src'); foreach ($finder as $file) { $this->addFile($file); } // add vendors $finder = new Finder(); $finder->files()->ignoreVCS(true)->notName('.DS_Store')->notName('README*')->notName('CHANGELOG')->notName('AUTHORS')->notName('create_pear_package.php')->notName('composer.json')->notName('installed.json')->notName('package.xml.tpl')->notName('phpunit.xml.dist')->exclude(array('docs', 'tests', 'twig/bin', 'twig/doc', 'twig/ext', 'twig/test'))->in($this->rootDir . '/vendor'); foreach ($finder as $file) { $this->addFile($file); } // add license and Readme $this->addFile(new \SplFileInfo($this->rootDir . '/LICENSE.md')); $this->addFile(new \SplFileInfo($this->rootDir . '/README.md')); // compress all files into a single ZIP file Toolkit::zip($this->packageDir, './' . $this->zipFile); // delete temp directory $this->filesystem->remove($this->packageDir); echo sprintf("\n %d files added\n\n %s (%.2f MB) package built successfully\n\n", $this->fileCount, $this->zipFile, filesize($this->zipFile) / (1024 * 1024)); }
public function build($zipFile = null) { $this->zipFile = $zipFile ?: sprintf('%s/easybook-%s.zip', $this->rootDir, $this->version); if (file_exists($this->zipFile)) { unlink($this->zipFile); } // add package files $this->addBookScript(); $this->addAutoloaders(); $this->addResources(); $this->addSampleBooks(); $this->addCommandHelp(); $this->addCoreClasses(); $this->addVendors(); $this->addLicenseAndReadme(); // compress all files into a single ZIP file Toolkit::zip($this->packageDir, $this->zipFile); // delete temp directory $this->filesystem->remove($this->packageDir); echo sprintf("\n %d files added\n\n %s (%.2f MB) package built successfully\n\n", $this->fileCount, $this->zipFile, filesize($this->zipFile) / (1024 * 1024)); }
public function assembleBook() { // set the edition id needed for ebook generation $this->app->edition('id', $this->app['publishing.id']); // variables needed to hold the list of images and fonts of the book $bookImages = array(); $bookFonts = array(); // prepare the temp directory used to build the book $bookTempDir = $this->app['app.dir.cache'] . '/' . $this->app['publishing.book.slug'] . '-' . $this->app['publishing.edition']; $this->app->get('filesystem')->mkdir(array($bookTempDir, $bookTempDir . '/book', $bookTempDir . '/book/META-INF', $bookTempDir . '/book/OEBPS', $bookTempDir . '/book/OEBPS/css', $bookTempDir . '/book/OEBPS/images', $bookTempDir . '/book/OEBPS/fonts')); // generate easybook CSS file if ($this->app->edition('include_styles')) { $this->app->renderThemeTemplate('style.css.twig', array('resources_dir' => '..'), $bookTempDir . '/book/OEBPS/css/easybook.css'); // copy book fonts and prepare font data for ebook manifest $this->app->get('filesystem')->copy($this->app['app.dir.resources'] . '/Fonts/Inconsolata/Inconsolata.ttf', $bookTempDir . '/book/OEBPS/fonts/Inconsolata.ttf'); $bookFonts[] = array('id' => 'font-1', 'filePath' => 'fonts/Inconsolata.ttf', 'mediaType' => 'application/octet-stream'); } // generate custom CSS file $customCss = $this->app->getCustomTemplate('style.css'); if (file_exists($customCss)) { $this->app->get('filesystem')->copy($customCss, $bookTempDir . '/book/OEBPS/css/styles.css', true); } // each book element will generate an HTML page // use automatic slugs (chapter-1, chapter-2, ...) instead of // semantic slugs (lorem-ipsum, dolor-sit-amet, ...) $this->app->set('publishing.slugs', array()); $items = array(); foreach ($this->app['publishing.items'] as $item) { $pageName = array_key_exists('number', $item['config']) ? $item['config']['element'] . ' ' . $item['config']['number'] : $item['config']['element']; $slug = $this->app->get('slugger')->slugify(trim($pageName)); $item['slug'] = $slug; // TODO: document this new item property $item['fileName'] = $slug . '.html'; $items[] = $item; } // update `publishing items` with the new slug value $this->app->set('publishing.items', $items); // generate one HTML page for every book item $items = array(); foreach ($this->app['publishing.items'] as $item) { $this->app->renderThemeTemplate('chunk.twig', array('item' => $item, 'has_custom_css' => file_exists($customCss)), $bookTempDir . '/book/OEBPS/' . $item['fileName']); } // copy book images and prepare image data for ebook manifest if (file_exists($imagesDir = $this->app['publishing.dir.contents'] . '/images')) { $images = $this->app->get('finder')->files()->in($imagesDir); $i = 1; foreach ($images as $image) { $this->app->get('filesystem')->copy($image->getPathName(), $bookTempDir . '/book/OEBPS/images/' . $image->getFileName()); $bookImages[] = array('id' => 'figure-' . $i++, 'filePath' => 'images/' . $image->getFileName(), 'mediaType' => 'image/' . pathinfo($image->getFilename(), PATHINFO_EXTENSION)); } } // look for cover images $cover = null; if (null != ($image = $this->app->getCustomCoverImage())) { list($width, $height, $type) = getimagesize($image); $cover = array('height' => $height, 'width' => $width, 'filePath' => 'images/' . basename($image), 'mediaType' => image_type_to_mime_type($type)); // copy the cover image $this->app->get('filesystem')->copy($image, $bookTempDir . '/book/OEBPS/images/' . basename($image)); } // generate book cover $this->app->renderThemeTemplate('cover.twig', array('cover' => $cover), $bookTempDir . '/book/OEBPS/titlepage.html'); // generate OPF file $this->app->renderThemeTemplate('content.opf.twig', array('cover' => $cover, 'has_custom_css' => file_exists($customCss), 'fonts' => $bookFonts, 'images' => $bookImages), $bookTempDir . '/book/OEBPS/content.opf'); // generate NCX file $this->app->renderThemeTemplate('toc.ncx.twig', array(), $bookTempDir . '/book/OEBPS/toc.ncx'); // generate container.xml and mimetype files $this->app->renderThemeTemplate('container.xml.twig', array(), $bookTempDir . '/book/META-INF/container.xml'); $this->app->renderThemeTemplate('mimetype.twig', array(), $bookTempDir . '/book/mimetype'); // compress book contents as ZIP file and rename to .epub // TODO: the name of the book file (book.epub) must be configurable Toolkit::zip($bookTempDir . '/book', $bookTempDir . '/book.zip'); $this->app->get('filesystem')->copy($bookTempDir . '/book.zip', $this->app['publishing.dir.output'] . '/book.epub', true); // remove temp directory used to build the book $this->app->get('filesystem')->remove($bookTempDir); }
public function __construct() { parent::__construct(); $app = $this; // -- global generic parameters --------------------------------------- $this['app.debug'] = false; $this['app.charset'] = 'UTF-8'; $this['app.name'] = 'easybook'; $this['app.signature'] = <<<SIGNATURE | | ,---.,---.,---., .|---.,---.,---.|__/ |---',---|`---.| || || || || \\ `---'`---^`---'`---|`---'`---'`---'` ` `---' SIGNATURE; // -- global directories location ------------------------------------- $this['app.dir.base'] = realpath(__DIR__ . '/../../../'); $this['app.dir.cache'] = $this['app.dir.base'] . '/app/Cache'; $this['app.dir.doc'] = $this['app.dir.base'] . '/doc'; $this['app.dir.resources'] = $this['app.dir.base'] . '/app/Resources'; $this['app.dir.plugins'] = $this['app.dir.base'] . '/src/Easybook/Plugins'; $this['app.dir.translations'] = $this['app.dir.resources'] . '/Translations'; $this['app.dir.skeletons'] = $this['app.dir.resources'] . '/Skeletons'; $this['app.dir.themes'] = $this['app.dir.resources'] . '/Themes'; // -- console --------------------------------------------------------- $this['console.input'] = null; $this['console.output'] = null; $this['console.dialog'] = null; // -- timer ----------------------------------------------------------- $this['app.timer.start'] = 0.0; $this['app.timer.finish'] = 0.0; // -- publishing process variables ------------------------------------ // holds the app theme dir for the current edition $this['publishing.dir.app_theme'] = ''; $this['publishing.dir.book'] = ''; $this['publishing.dir.contents'] = ''; $this['publishing.dir.resources'] = ''; $this['publishing.dir.plugins'] = ''; $this['publishing.dir.templates'] = ''; $this['publishing.dir.output'] = ''; $this['publishing.edition'] = ''; $this['publishing.items'] = array(); // the specific item currently being parsed/modified/decorated/... $this['publishing.active_item'] = array(); $this['publishing.active_item.toc'] = array(); $this['publishing.book.config'] = array('book' => array()); $this['publishing.book.slug'] = ''; $this['publishing.book.items'] = array(); // the real TOC used to generate the book (needed for html_chunked editions) $this['publishing.book.toc'] = array(); // holds all the internal links (used in html_chunked and epub editions) $this['publishing.links'] = array(); $this['publishing.list.images'] = array(); $this['publishing.list.tables'] = array(); $this['publishing.edition.id'] = function ($app) { if (null !== ($isbn = $app->edition('isbn'))) { return array('scheme' => 'isbn', 'value' => $isbn); } // for ISBN-less books, generate a unique RFC 4211 UUID v4 ID return array('scheme' => 'URN', 'value' => Toolkit::uuid()); }; // maintained for backwards compatibility $this['publishing.id'] = function () { trigger_error('The "publishing.id" option is deprecated since version 5.0 and will be removed in the future. Use "publishing.edition.id" instead.', E_USER_DEPRECATED); }; // -- event dispatcher ------------------------------------------------ $this['dispatcher'] = function () { return new EventDispatcher(); }; // -- finder ---------------------------------------------------------- $this['finder'] = $this->factory(function () { return new Finder(); }); // -- filesystem ------------------------------------------------------ $this['filesystem'] = $this->factory(function () { return new Filesystem(); }); // -- configurator ---------------------------------------------------- $this['configurator'] = function ($app) { return new BookConfigurator($app); }; // -- validator ------------------------------------------------------- $this['validator'] = function ($app) { return new Validator($app); }; $this->register(new PublisherServiceProvider()); $this->register(new ParserServiceProvider()); $this->register(new TwigServiceProvider()); $this->register(new PrinceXMLServiceProvider()); $this->register(new KindleGenServiceProvider()); $this->register(new SluggerServiceProvider()); $this->register(new CodeHighlighterServiceProvider()); // -- labels --------------------------------------------------------- $this['labels'] = function () use($app) { $labels = Yaml::parse($app['app.dir.translations'] . '/labels.' . $app->book('language') . '.yml'); // books can define their own labels files if (null !== ($customLabelsFile = $app->getCustomLabelsFile())) { $customLabels = Yaml::parse($customLabelsFile); return Toolkit::array_deep_merge_and_replace($labels, $customLabels); } return $labels; }; // -- titles ---------------------------------------------------------- $this['titles'] = function () use($app) { $titles = Yaml::parse($app['app.dir.translations'] . '/titles.' . $app->book('language') . '.yml'); // books can define their own titles files if (null !== ($customTitlesFile = $app->getCustomTitlesFile())) { $customTitles = Yaml::parse($customTitlesFile); return Toolkit::array_deep_merge_and_replace($titles, $customTitles); } return $titles; }; }
public function loadEditionConfig() { $book = $this->get('book'); $edition = $this->get('publishing.edition'); $userConfig = $book['editions'][$edition]; $defaultConfig = $this['app.edition.defaults']; // if edition extends another edition, merge their configurations if (null != ($parent = $this->edition('extends'))) { if (!array_key_exists($parent, $book['editions'])) { throw new \UnexpectedValueException(sprintf(" ERROR: '%s' edition extends nonexistent '%s' edition" . "\n\n" . "Check in '%s' file \n" . "that the value of 'extends' option in '%s' edition is a valid \n" . "edition of the book", $edition, $parent, realpath($this['publishing.dir.book'] . '/config.yml'), $edition)); } $parentConfig = $book['editions'][$parent]; $userConfig = Toolkit::array_deep_merge($parentConfig, $userConfig); } $config = array_merge($defaultConfig, $userConfig); $book['editions'][$edition] = $config; $this->set('book', $book); }
/** * It generates the compressed file required for the ePub book. * To compress the contents, it uses the 'Zip' PHP extension. * * @param string $directory Book contents directory * @param string $zip_file The path of the generated ZIP file */ private function zipBookContentsWithPhpExtension($directory, $zip_file) { Toolkit::zip($directory, $zip_file); }
private function zipBookContents($directory, $zip_file) { if (extension_loaded('zip')) { return Toolkit::zip($directory, $zip_file); } // After several hours trying to create ZIP files with lots of PHP // tools and libraries (Archive_Zip, Pclzip, zetacomponents/archive, ...) // I can't produce a proper ZIP file for ebook readers. // Therefore, if ZIP extension isn't enabled, the ePub ZIP file is // generated by executing 'zip' command // check if 'zip' command exists $process = new Process('zip'); $process->run(); if (!$process->isSuccessful()) { throw new \RuntimeException("[ERROR] You must enable the ZIP extension in PHP \n" . " or your system should be able to execute 'zip' console command."); } // To generate the ePub file, you must execute the following commands: // $ cd /path/to/ebook/contents // $ zip -X0 book.zip mimetype // $ zip -rX9 book.zip * -x mimetype $command = sprintf('cd %s && zip -X0 %s mimetype && zip -rX9 %s * -x mimetype', $directory, $zip_file, $zip_file); $process = new Process($command); $process->run(); if (!$process->isSuccessful()) { throw new \RuntimeException("[ERROR] 'zip' command execution wasn't successful.\n\n" . "Executed command:\n" . " {$command}\n\n" . "Result:\n" . $process->getErrorOutput()); } }