diff --git a/.gitattributes b/.gitattributes index adf6e01..1fe9b0d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,4 +5,3 @@ bin/ export-ignore .gitignore export-ignore composer.lock export-ignore phpunit.xml export-ignore -hamepub export-ignore diff --git a/README.md b/README.md index 3ec2d0f..21d709d 100755 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ foreach( $contents as $key => $html ){ $factory->opf->setLang( 'en_US' ); $factory->opf->setTitle( 'My First eBook', 'main-title' ); $factory->opf->putXML(); -$factory->container->pubXML(); +$factory->container->putXML(); // Save it! $factory->compile('path/to/epub'); ``` @@ -71,6 +71,10 @@ $factory->compile('path/to/epub'); This library is under alpha and highly experimental. Do not trust this until Beta! +## Acknowledgement + +The sample picture is credited by [Public Domain Pictures](https://www.pexels.com/ja-jp/photo/87742/) and [Nadi Lindsay](https://www.pexels.com/ja-jp/photo/3078831/). + ## License As wrote in LICENSE file, this library is released under [MIT](https://opensource.org/licenses/MIT). diff --git a/bin/hamepub b/bin/hamepub index d4ec737..c848b36 100755 --- a/bin/hamepub +++ b/bin/hamepub @@ -29,6 +29,15 @@ $app->registerCommand('generate', function( CommandCall $input ) use ($app, $roo // This is not file. $app->getPrinter()->error('Setting file not found.', false); } + // Generate temp directory. + $tmp_dir = $input->hasParam( 'tmp' ) ? $input->getParam( 'tmp' ) : tempnam( sys_get_temp_dir(), 'hamepub-' ); + try { + $packager = \Hametuha\HamePub\Packager::get(); + $path = $packager->parse( $file, $tmp_dir ); + $app->getPrinter()->success("Generated ePub: {$path}", false); + } catch ( \Exception $e ) { + $app->getPrinter()->error($e->getMessage(), false); + } }); diff --git a/setting.json b/setting.json new file mode 100644 index 0000000..07235aa --- /dev/null +++ b/setting.json @@ -0,0 +1,28 @@ +{ + "root": "./tests/dist/", + "id": "my-sample-ebook", + "isbn": "1234567890123", + "title": "My Sample Book", + "author": [ + { + "name": "Kazuo Ishiguro", + "id": "author-1", + "role": "aut" + }, + { + "name": "Haruki Murakami", + "id": "author-2", + "role": "aut" + }, + { + "name": "Mike Jacob", + "id": "translator-1", + "role": "trl", + "type": "contributor" + } + ], + "target": "./tests/tmp", + "published": "2023-01-01T23:00:00Z", + "direction": "ltr", + "cover": "./tests/dist/img/cover.jpg" +} diff --git a/src/Hametuha/HamePub/Oebps/Content.php b/src/Hametuha/HamePub/Oebps/Content.php index a996c61..8b61fe3 100755 --- a/src/Hametuha/HamePub/Oebps/Content.php +++ b/src/Hametuha/HamePub/Oebps/Content.php @@ -70,14 +70,39 @@ public function setTitle($string, $id, $type = 'main', $sequence = 1) $meta['property'] = 'display-seq'; } + /** + * Add author to as meta value. + * + * @param string $value Name of author. + * @param string $id ID of author. + * @param string $tag Default is 'creator' or 'contributor' + * @param string $role Role tag. See {https://www.loc.gov/marc/relators/relaterm.html} + * + * @return void + */ + public function addAuthor( $value, $id, $tag = 'creator', $role = '' ) { + $creator = $this->dom->metadata->addChild( $tag, $this->h( $value ), Schemas::DC ); + $creator['id'] = $id; + if ( $role ) { + $meta = $this->dom->metadata->addChild( 'meta', $role ); + $meta['refines'] = '#' . $id; + $meta['property'] = 'role'; + $meta['scheme'] = 'marc:relators'; + $meta['id'] = 'role-of-' . $id; + } + } + /** * Add modified date * - * @param int $timestamp UTC timestamp + * @param int|string $timestamp If int, treated as UTC timestamp. */ public function setModifiedDate($timestamp) { - $this->addMeta('meta', date('Y-m-d\TH:i:s\Z', $timestamp), [ + if ( is_int( $timestamp ) ) { + $timestamp = date('Y-m-d\TH:i:s\Z', $timestamp); + } + $this->addMeta('meta', $timestamp, [ 'property' => 'dcterms:modified', ]); } @@ -104,7 +129,7 @@ public function addMeta($tag, $value, array $attributes = []) } /** - * Add item to + * Add item to Content OPF. * * @param string $relative_path * @param string $id If empty, path will convert to id diff --git a/src/Hametuha/HamePub/Oebps/Toc.php b/src/Hametuha/HamePub/Oebps/Toc.php index 762d0aa..754120b 100644 --- a/src/Hametuha/HamePub/Oebps/Toc.php +++ b/src/Hametuha/HamePub/Oebps/Toc.php @@ -85,7 +85,7 @@ public function getHTML($header = '', $footer = '') HTML; - $html .= $this->getNavHTML(); + $html .= $this->getNavHTML( $title ); $html .= << diff --git a/src/Hametuha/HamePub/Packager.php b/src/Hametuha/HamePub/Packager.php index fd54a7e..6e8f73f 100644 --- a/src/Hametuha/HamePub/Packager.php +++ b/src/Hametuha/HamePub/Packager.php @@ -2,11 +2,154 @@ namespace Hametuha\HamePub; +use Hametuha\HamePub\Parser\SettingParser; use Hametuha\HamePub\Pattern\Singleton; +use PHPUnit\Framework\Error\Error; /** * Package ePub from directory. */ -class Packager extends Singleton -{ +class Packager extends Singleton { + + use SettingParser; + + /** + * @var array Setting. + */ + protected $setting = []; + + /** + * @var string[] HTML strings. + */ + protected $htmls = []; + + /** + * @var string Newest modified HTML. + */ + protected $newest = 0; + + /** + * Parse and save setting. + * + * @param string $file Path to setting file. + * @param string $tmp_dir Temporary directory. + * + * @return string ePub file path. + * @throws \Exception + */ + public function parse( $file = 'setting.json', $tmp_dir = '' ) { + $this->setting = $this->getSettingFromFile( $file ); + // Set temporary directory. + if ( empty( $tmp_dir ) ) { + $tmp_dir = tempnam( sys_get_temp_dir(), 'hamepub-' ); + } + // Load HTMLs. + $this->loadHtml(); + if ( empty( $this->htmls ) ) { + throw new \Exception( 'No HTML files found.' ); + } + // Start parsing. + $factory = Factory::init( $this->setting[ 'id' ], $tmp_dir ); + // Set metadata. + $factory->opf->setLang( $this->setting[ 'lang' ] ); + $factory->opf->setTitle( $this->setting[ 'title' ], 'main-title' ); + $factory->opf->setModifiedDate( $this->setting['published'] ); + $factory->opf->direction = $this->setting['direction']; + // Set authors. + if ( is_array( $this->setting['author'] ) ) { + foreach ( $this->setting['author'] as $index => $author ) { + $factory->opf->addAuthor( + $author['name'], + ( $author['id'] ?? sprintf( 'creator-%d', $index + 1 ) ), + ( ( isset( $author['type'] ) && $author['type'] === 'contributor' ) ? 'contributor' : 'creator'), + ( $author['role'] ?? '' ) + ); + } + } else { + $factory->opf->addAuthor( $this->setting['author'], 'creator' ); + } + foreach ( $this->htmls as $key => $html ) { + // Register toc + $toc = $factory->toc->addChild( $key, $key . '.xhtml' ); + // Grab all headers and add them to toc. + $dom = $factory->parser->html5->loadHTML( $html ); + // Grab header and add ID attributes. + $factory->parser->grabHeaders( $toc, $dom, true, $this->setting[ 'header' ][ 'max_level' ], $this->setting[ 'header' ][ 'max_level' ] ); + // Convert from dom object to string. + $html = $factory->parser->convertToString( $dom ); + // Recreate DOM. + $dom = $factory->registerHTML( $key, $html, $this->getLinear( $key ) ); + // Grab all images + foreach ( $factory->parser->extractAssets( $dom, 'img', 'src', $this->setting[ 'url_base' ], $this->setting[ 'root' ] ) as $path ) { + $factory->opf->addItem( $path, '' ); + } + // Grab all CSS + foreach ( $factory->parser->extractAssets( $dom, 'link', 'href', $this->setting[ 'url_base' ], $this->setting[ 'root' ] ) as $path ) { + $factory->opf->addItem( $path, '' ); + } + // Register to OPF + $factory->opf->addItem( "Text/{$key}.xhtml", "{$key}.xhtml" ); + // Save HTML + $factory->parser->saveDom( $dom, "{$key}.xhtml" ); + } + // If TOC is set, save it. + if ( !empty($this->setting['toc']) ) { + $factory->toc->label = $this->setting['toc']; + $toc_html = $factory->toc->getHTML(); + $factory->opf->addItem( 'Text/toc.xhtml', 'toc.xhtml', ['nav'] ); + $factory->parser->saveDom( $factory->registerHTML( 'toc', $toc_html, 'no' ), 'toc.xhtml' ); + } + // Set OPF. + if ( ! empty( $this->setting[ 'isbn' ] ) ) { + $factory->opf->setIdentifier( $this->setting[ 'isbn' ] ); + } + // If cover is set, add it. + if ( $this->setting['cover'] ) { + $factory->addCover( $this->setting['cover'] ); + } + $factory->opf->putXML(); + $factory->container->putXML(); + // Save it! + $target = tempnam( $this->setting['target'], $this->setting['id'] . '-' ) . '.epub'; + if ( ! is_writable( dirname( $target ) ) ) { + throw new \Exception( 'Target directory is not writable: ' . $target ); + } + $factory->compile( $target ); + return $target; + } + + /** + * Get linear property. + * + * @param string $key HTML name. + * @return string + */ + protected function getLinear( $key ) { + return in_array( $key, $this->setting['hidden'], true ) ? 'no' : 'yes'; + } + + /** + * Load HTML and save it in $htmls. + * @return void + */ + public function loadHtml() { + $files = glob( $this->setting[ 'root' ] . '/*.html' ); + foreach ( $files as $file ) { + $this->htmls[ basename( $file, '.html' ) ] = file_get_contents( $file ); + // Save modified time. + $time = filemtime( $file ); + if ( $time > $this->newest ) { + $this->newest = $time; + } + } + } + + /** + * Dump setting for debugging. + * + * @return void + */ + public function dumpSetting() { + var_dump( $this->setting, $this->htmls ); + } } diff --git a/src/Hametuha/HamePub/Parser/SettingParser.php b/src/Hametuha/HamePub/Parser/SettingParser.php new file mode 100644 index 0000000..daa6ccf --- /dev/null +++ b/src/Hametuha/HamePub/Parser/SettingParser.php @@ -0,0 +1,67 @@ +defaultSetting(), $setting ); + // Validate if isbn is not set. + if ( empty( $setting['id']) ) { + throw new \Exception( 'id fields must not be empty: ' . $setting['id'] ); + } + // Validate if published is not set. + if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u', $setting['published' ] ) ) { + throw new \Exception( 'published field is malformed. Should be GMT in ISO8601 e.g. "2000-01-01T00:00:00Z": ' . $setting['published'] ); + } + // Validate if author is set. + if ( empty( $setting['author'] ) ) { + throw new \Exception( 'At least 1 author should be set.' ); + } + return $setting; + } + + /** + * Default setting. + * + * @return array + */ + public function defaultSetting() { + return [ + 'lang' => 'en', + 'id' => '', + 'isbn' => '', + 'title' => '', + 'author' => '', + 'published' => '', + 'root' => './dist/', + 'header' => [ + 'max_level' => 3, + 'depth' => 2, + ], + 'toc' => 'Table of Contents', + 'url_base' => '#\./#u', + 'target' => './tmp', + 'direction' => 'default', + 'hidden' => [ 'toc' ], + 'cover' => '', + ]; + } +} diff --git a/src/Hametuha/HamePub/Pattern/Singleton.php b/src/Hametuha/HamePub/Pattern/Singleton.php index 9d450bd..d2c16e5 100755 --- a/src/Hametuha/HamePub/Pattern/Singleton.php +++ b/src/Hametuha/HamePub/Pattern/Singleton.php @@ -7,7 +7,7 @@ * * @package Hametuha\HamePub\Pattern */ -class Singleton +abstract class Singleton { /** * @var static[] @@ -17,25 +17,32 @@ class Singleton /** * Constructor * - * @param array $settings */ - protected function __construct(array $settings = []) + protected function __construct() { - // Do nothing + $this->init(); } /** - * Get instance + * Executed in constructor. * - * @param array $settings + * @return void + */ + protected function init() + { + // Do something. + } + + /** + * Get instance * * @return static */ - public static function get(array $settings = []) + public static function get() { $class_name = get_called_class(); if (!isset(self::$instances[$class_name])) { - self::$instances[$class_name] = new $class_name($settings); + self::$instances[$class_name] = new $class_name(); } return self::$instances[$class_name]; } diff --git a/tests/dist/chapter-1.html b/tests/dist/chapter-1.html new file mode 100644 index 0000000..af1ed1a --- /dev/null +++ b/tests/dist/chapter-1.html @@ -0,0 +1,129 @@ + + + + + Chapter 1 | Lorem Ipsum + + + + +
+ +

How Lipsum Starts

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed accumsan finibus diam et molestie. Mauris gravida + mi sed risus fermentum suscipit. Duis sodales turpis id neque finibus, non consectetur augue euismod. Praesent + ante metus, mattis eget commodo vel, sollicitudin sed urna. Morbi fringilla posuere velit non imperdiet. Donec + ac ex fringilla, congue arcu vehicula, egestas arcu. Quisque vitae lacus mauris. Nunc auctor nisi quis nulla + mollis condimentum. Fusce scelerisque suscipit suscipit. Duis commodo fringilla justo a varius. Aliquam bibendum + interdum mi, id mattis augue rutrum ac. Mauris volutpat semper nulla non scelerisque. Nunc purus eros, mollis ut + velit et, vehicula egestas mi. Fusce vestibulum interdum efficitur. +

+

+ Duis non ante in libero facilisis semper. Cras turpis nibh, consectetur ullamcorper semper ut, ultrices gravida + odio. Proin non pulvinar elit. Nunc mattis, nunc eget lobortis pulvinar, neque nisl eleifend ex, in imperdiet + velit quam et dui. Nam molestie ex ac lectus gravida tristique. Mauris in dolor bibendum, posuere velit non, + egestas quam. Aliquam at metus sagittis, pharetra neque ut, vulputate tellus. Maecenas vitae mauris sed nunc + efficitur ullamcorper efficitur et lectus. Maecenas sed tempor dolor. Nulla dignissim eros ipsum, quis efficitur + nisi maximus et. +

+

+ Integer nec mi consectetur, maximus mauris sed, mattis mauris. Vestibulum ipsum arcu, blandit vel consectetur + nec, tincidunt a quam. Duis tincidunt aliquet efficitur. Proin maximus, justo vel semper malesuada, sem nibh + ultrices purus, eget aliquet leo lacus a tortor. Etiam id nunc mollis, blandit est quis, faucibus odio. + Pellentesque ipsum lectus, euismod eget accumsan vel, blandit id dolor. Curabitur vel augue suscipit lacus + ultrices varius ullamcorper at odio. Duis in vehicula odio, vitae posuere quam. Pellentesque scelerisque justo + tristique est vestibulum venenatis. Maecenas ornare ut nulla nec malesuada. +

+

Heading 2

+

+ Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam nunc tellus, + feugiat at risus vel, eleifend lacinia sem. Ut lacinia mattis dui sed vehicula. Mauris sed dui bibendum, mattis + augue vitae, egestas nunc. Suspendisse potenti. Nulla nec mattis eros, at bibendum turpis. Donec volutpat dolor + nec commodo imperdiet. Aenean leo odio, tincidunt id rutrum nec, pretium id dolor. +

+

+ Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nunc mollis tellus purus, + vitae porta ex lacinia vitae. Nunc facilisis, eros ac rhoncus volutpat, tortor ante feugiat leo, non ullamcorper + dolor neque sit amet justo. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus + mus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Maecenas + ullamcorper consequat enim ac vulputate. Cras dapibus est eu nibh ultrices, vitae condimentum augue lacinia. Sed + venenatis est augue, in finibus velit vestibulum nec. Phasellus ornare mi id orci pharetra, ac tempus nibh + pellentesque. Morbi iaculis eu mauris quis accumsan. Phasellus sollicitudin molestie consequat. Phasellus + sodales varius hendrerit. Phasellus sit amet mi in urna consequat mattis non non massa. +

+

Heading 3

+

+ Nam dictum quis dolor vitae aliquet. Fusce porttitor velit ex, in ultrices lectus vehicula id. Morbi eu sem + odio. Duis commodo ullamcorper molestie. Cras bibendum semper nibh, dictum tempus dui varius non. Donec in neque + neque. Integer at quam non dolor commodo bibendum id vel enim. Donec tristique est a leo porta, eu fermentum + lorem tempus. Fusce id lacinia justo. +

+

+ Cras tempor vulputate efficitur. Vestibulum tempus odio nisl, vitae viverra felis placerat vitae. Nam luctus + feugiat mauris ut tristique. Nam et posuere lectus. Ut nisl orci, euismod sit amet elementum vel, venenatis a + dui. Aliquam iaculis eleifend viverra. Mauris in elementum nibh. +

+

+ Donec eu nisl sed ante finibus tristique. Maecenas ac leo ante. Sed pretium mi sit amet dui viverra scelerisque + ac ut lorem. Quisque imperdiet tellus ex, vel pellentesque leo semper eget. Ut lectus nisl, feugiat eu suscipit + vitae, semper vitae enim. Praesent lobortis consequat sem, a faucibus nibh lacinia sed. Proin auctor eros in + nisl vestibulum eleifend. Etiam ultricies, mauris quis varius placerat, nisl lacus tincidunt lorem, quis gravida + justo leo sit amet sem. Aliquam in feugiat justo, eget euismod ligula. Nulla et maximus nulla, in scelerisque + lacus. Sed eu ipsum vel arcu pretium consequat. Ut maximus faucibus ipsum et semper. Cras porta efficitur ex ut + hendrerit. Sed condimentum et risus in tempor. Fusce vehicula viverra magna. +

+

+ In a mauris viverra, malesuada orci id, pulvinar arcu. Nullam nibh ex, pulvinar eu commodo eget, consectetur + quis elit. Etiam pulvinar, quam et hendrerit molestie, sem diam tempus elit, in rutrum ante nunc et ante. Nunc + tincidunt volutpat pellentesque. Cras sapien ipsum, bibendum at pharetra vitae, scelerisque nec nibh. Fusce + sagittis dignissim nisl non malesuada. Cras vehicula nibh libero, at varius sem fermentum a. +

+

Heading 3

+

+ Integer libero sem, rhoncus a posuere eget, mattis a tellus. Duis scelerisque ornare lectus, eu sodales odio + laoreet sed. Nunc egestas feugiat urna iaculis tempor. Cras vehicula libero et lacus consequat facilisis. Sed + non tempor sapien, ac rutrum neque. Etiam tristique quam eu felis rhoncus, at faucibus diam rhoncus. Etiam + rhoncus, purus eget mollis imperdiet, tortor neque interdum ex, quis fringilla nibh nunc in lectus. Curabitur + dignissim convallis ipsum sed pellentesque. Duis facilisis leo vestibulum neque blandit, id tincidunt nisl + venenatis. Etiam vitae pellentesque nunc, id efficitur nunc. Nam eget libero sit amet ante accumsan cursus vel + scelerisque libero. Morbi pharetra metus ut iaculis vestibulum. +

+

Heading 3-1

+

+ Ut quis erat sed eros vehicula posuere at sed sem. Nulla at neque nibh. Vivamus et mauris id massa euismod + congue. Nulla mollis sem non velit ultrices, quis tempus ligula dictum. Aenean pharetra eget leo eget congue. + Etiam tempor condimentum nisl id rhoncus. Praesent auctor a odio ut posuere. Etiam eget venenatis felis. In hac + habitasse platea dictumst. Sed mattis varius ex eget bibendum. Interdum et malesuada fames ac ante ipsum primis + in faucibus. Phasellus libero ipsum, scelerisque ut lobortis at, tincidunt vel erat. Fusce ultrices accumsan + lacinia. +

+

Heading 3-2

+

+ Quisque eu venenatis neque. Nullam viverra dui nulla, sed euismod risus volutpat ac. Duis tincidunt luctus urna, + sit amet pulvinar velit elementum ut. Duis condimentum mi a odio feugiat vestibulum. Proin vehicula ut magna id + congue. Suspendisse eu pellentesque nisi. Suspendisse ac nisi nec turpis sodales mollis. Nam gravida nunc id + erat molestie, eget volutpat justo condimentum. +

+

Heading 3-3

+

+ Maecenas mattis ac justo id interdum. Mauris vulputate nisl quis mi commodo convallis. Aliquam sed laoreet + metus, sit amet ultrices eros. Morbi molestie ligula suscipit eros faucibus, at sodales sem sagittis. In id + turpis semper, finibus quam ut, cursus odio. Etiam diam nunc, volutpat non arcu ac, pretium convallis arcu. Sed + condimentum, urna vitae interdum interdum, sem quam varius nisl, nec molestie massa turpis a nisi. Vestibulum + scelerisque vestibulum leo vel tempus. Phasellus finibus auctor mauris eget ornare. Sed scelerisque iaculis erat + vitae vulputate. +

+

Heading 3-4

+

+ Nullam leo dui, rhoncus at dui a, facilisis ultricies nisi. Duis dapibus nibh sed nibh fringilla aliquam eu nec + est. Sed bibendum ante in risus viverra, eu pretium sapien fermentum. Cras lacus mauris, congue a sodales vel, + pharetra consectetur lacus. Vivamus convallis accumsan dolor, vitae dictum lectus lobortis ac. Nulla lacus + tortor, molestie nec dapibus et, vulputate in ipsum. Maecenas semper orci aliquam ipsum iaculis, id faucibus + urna feugiat. Praesent ac sem et lectus pharetra mattis a vel massa. Nullam rhoncus dui non sapien vestibulum, + sed tincidunt eros auctor. Proin volutpat gravida dapibus. +

+
+ + + diff --git a/tests/dist/chapter-2.html b/tests/dist/chapter-2.html new file mode 100644 index 0000000..770ef4f --- /dev/null +++ b/tests/dist/chapter-2.html @@ -0,0 +1,56 @@ + + + + + Chapter 2 | Lorem Ipsum + + + + +
+ +

How Lipsum Ends

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed accumsan finibus diam et molestie. Mauris gravida + mi sed risus fermentum suscipit. Duis sodales turpis id neque finibus, non consectetur augue euismod. Praesent + ante metus, mattis eget commodo vel, sollicitudin sed urna. Morbi fringilla posuere velit non imperdiet. Donec + ac ex fringilla, congue arcu vehicula, egestas arcu. Quisque vitae lacus mauris. Nunc auctor nisi quis nulla + mollis condimentum. Fusce scelerisque suscipit suscipit. Duis commodo fringilla justo a varius. Aliquam bibendum + interdum mi, id mattis augue rutrum ac. Mauris volutpat semper nulla non scelerisque. Nunc purus eros, mollis ut + velit et, vehicula egestas mi. Fusce vestibulum interdum efficitur. +

+
+ +
This is a picture of the castle.
+
+

+ Duis non ante in libero facilisis semper. Cras turpis nibh, consectetur ullamcorper semper ut, ultrices gravida + odio. Proin non pulvinar elit. Nunc mattis, nunc eget lobortis pulvinar, neque nisl eleifend ex, in imperdiet + velit quam et dui. Nam molestie ex ac lectus gravida tristique. Mauris in dolor bibendum, posuere velit non, + egestas quam. Aliquam at metus sagittis, pharetra neque ut, vulputate tellus. Maecenas vitae mauris sed nunc + efficitur ullamcorper efficitur et lectus. Maecenas sed tempor dolor. Nulla dignissim eros ipsum, quis efficitur + nisi maximus et. +

+
+

+ Integer nec mi consectetur, maximus mauris sed, mattis mauris. Vestibulum ipsum arcu, blandit vel + consectetur + nec, tincidunt a quam. Duis tincidunt aliquet efficitur. Proin maximus, justo vel semper malesuada, sem nibh + ultrices purus, eget aliquet leo lacus a tortor. Etiam id nunc mollis, blandit est quis, faucibus odio. + Pellentesque ipsum lectus, euismod eget accumsan vel, blandit id dolor. Curabitur vel augue suscipit lacus + ultrices varius ullamcorper at odio. Duis in vehicula odio, vitae posuere quam. Pellentesque scelerisque + justo + tristique est vestibulum venenatis. Maecenas ornare ut nulla nec malesuada. +

+
+

The End of Lipsum

+

+ Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam nunc tellus, + feugiat at risus vel, eleifend lacinia sem. Ut lacinia mattis dui sed vehicula. Mauris sed dui bibendum, mattis + augue vitae, egestas nunc. Suspendisse potenti. Nulla nec mattis eros, at bibendum turpis. Donec volutpat dolor + nec commodo imperdiet. Aenean leo odio, tincidunt id rutrum nec, pretium id dolor. +

+
+ + + diff --git a/tests/dist/css/style.css b/tests/dist/css/style.css new file mode 100644 index 0000000..0f92b20 --- /dev/null +++ b/tests/dist/css/style.css @@ -0,0 +1,13 @@ +body { + color: #444; + font-weight: 400; +} + +img { + max-width: 100%; + height: auto; +} + +figcaption { + text-align: center; +} diff --git a/tests/dist/img/cover.jpg b/tests/dist/img/cover.jpg new file mode 100644 index 0000000..bb3b055 Binary files /dev/null and b/tests/dist/img/cover.jpg differ diff --git a/tests/dist/img/sample.jpg b/tests/dist/img/sample.jpg new file mode 100644 index 0000000..b03037b Binary files /dev/null and b/tests/dist/img/sample.jpg differ