From d129ba928a46b71533c1bd5ec4f14d1b029554d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ewilan=20Rivi=C3=A8re?= Date: Sun, 26 May 2024 20:41:20 +0200 Subject: [PATCH] v2.5.10 - `MetaTitle::class` : now native slugifier is fixed, float volume works now, volume use `000` padding. - Allow authors with `,`, `;` and `&` in the name for `.opf`, `.pdf`, `.mobi` and audiobooks. --- composer.json | 2 +- src/Formats/Audio/AudiobookModule.php | 62 +-------- src/Formats/Epub/Parser/OpfItem.php | 14 +- src/Formats/Mobi/MobiModule.php | 7 + src/Formats/Pdf/PdfModule.php | 16 +-- src/Models/MetaTitle.php | 126 +++++++++++++++--- src/Utils/EbookUtils.php | 36 +++++ tests/EpubTest.php | 3 +- tests/MetaTitleTest.php | 56 +++++++- tests/OpfTest.php | 7 +- tests/Pest.php | 2 + .../opf-epub2-multiple-authors-merge.opf | 84 ++++++++++++ tests/media/opf-epub2-multiple-authors.opf | 84 ++++++++++++ 13 files changed, 400 insertions(+), 99 deletions(-) create mode 100644 tests/media/opf-epub2-multiple-authors-merge.opf create mode 100644 tests/media/opf-epub2-multiple-authors.opf diff --git a/composer.json b/composer.json index 85746ce..eae6d8c 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "kiwilan/php-ebook", "description": "PHP package to read metadata and extract covers from eBooks, comics and audiobooks.", - "version": "2.5.0", + "version": "2.5.10", "keywords": [ "php", "ebook", diff --git a/src/Formats/Audio/AudiobookModule.php b/src/Formats/Audio/AudiobookModule.php index 881c0a2..c5f21c1 100644 --- a/src/Formats/Audio/AudiobookModule.php +++ b/src/Formats/Audio/AudiobookModule.php @@ -37,7 +37,10 @@ private function create(): self $audio = $this->ebook->getAudio(); $authors = $audio->getArtist() ?? $audio->getAlbumArtist(); - $genres = $this->parseGenres($audio->getGenre()); + + $genres = EbookUtils::parseStringWithSeperator($audio->getGenre()); + $genres = array_map('ucfirst', $genres); + $series = $audio->getTag('series') ?? $audio->getTag('mvnm'); $series_part = $audio->getTag('series-part') ?? $audio->getTag('mvin'); $series_part = $this->parseTag($series_part); @@ -51,12 +54,12 @@ private function create(): self } $this->audio = [ - 'authors' => $this->parseAuthors($authors), + 'authors' => EbookUtils::parseStringWithSeperator($authors), 'title' => $audio->getAlbum() ?? $audio->getTitle(), 'subtitle' => $this->parseTag($audio->getTag('subtitle'), false), 'publisher' => $audio->getTag('encoded_by'), 'publish_year' => $audio->getYear(), - 'narrators' => $this->parseAuthors($narrators), + 'narrators' => EbookUtils::parseStringWithSeperator($narrators), 'description' => $this->parseTag($audio->getDescription(), false), 'lyrics' => $this->parseTag($audio->getLyrics()), 'comment' => $this->parseTag($audio->getComment()), @@ -186,59 +189,6 @@ public function __toString(): string return $this->toJson(); } - /** - * @return string[] - */ - private function parseGenres(?string $genres): array - { - if (! $genres) { - return []; - } - - $items = []; - if (str_contains($genres, ';')) { - $items = explode(';', $genres); - } elseif (str_contains($genres, '/')) { - $items = explode('/', $genres); - } elseif (str_contains($genres, '//')) { - $items = explode('//', $genres); - } elseif (str_contains($genres, ',')) { - $items = explode(',', $genres); - } else { - $items = [$genres]; - } - - $items = array_map('trim', $items); - $items = array_map('ucfirst', $items); - - return $items; - } - - /** - * @return string[] - */ - private function parseAuthors(?string $authors): array - { - if (! $authors) { - return []; - } - - $items = []; - if (str_contains($authors, ',')) { - $items = explode(',', $authors); - } elseif (str_contains($authors, ';')) { - $items = explode(';', $authors); - } elseif (str_contains($authors, '&')) { - $items = explode('&', $authors); - } elseif (str_contains($authors, 'and')) { - $items = explode('and', $authors); - } else { - $items = [$authors]; - } - - return array_map('trim', $items); - } - private function parseTag(?string $tag, bool $flat = true): ?string { if (! $tag) { diff --git a/src/Formats/Epub/Parser/OpfItem.php b/src/Formats/Epub/Parser/OpfItem.php index b60bb81..12cfb77 100644 --- a/src/Formats/Epub/Parser/OpfItem.php +++ b/src/Formats/Epub/Parser/OpfItem.php @@ -8,6 +8,7 @@ use Kiwilan\Ebook\Models\BookContributor; use Kiwilan\Ebook\Models\BookIdentifier; use Kiwilan\Ebook\Models\BookMeta; +use Kiwilan\Ebook\Utils\EbookUtils; use Kiwilan\XmlReader\XmlReader; /** @@ -64,7 +65,7 @@ protected function __construct( ) { } - public static function make(string $content, string $filename): self + public static function make(string $content, ?string $filename = null): self { $xml = XmlReader::make($content); $self = new self($xml); @@ -364,6 +365,8 @@ private function setDcSubjects(): array $items = $core; } + $items = EbookUtils::parseStringWithSeperator($items); + return $items; } @@ -411,6 +414,10 @@ private function setDcCreators(): array continue; } $attributes = XmlReader::parseAttributes($item); + // remove `\n` and `\r` from the name + $name = preg_replace('/\s+/', ' ', $name); + $name = trim($name); + $items[$name] = new BookAuthor( name: $name, role: $attributes['role'] ?? null, @@ -549,10 +556,7 @@ private function multipleItems(mixed $items): array $attr = XmlReader::parseAttributes($items); // Check if bad multiple creators `Jean M. Auel, Philippe Rouard` exists - if (is_string($content) && str_contains($content, ',')) { - $content = explode(',', $content); - $content = array_map('trim', $content); - } + $content = EbookUtils::parseStringWithSeperator($content); $temp = []; // If bad multiple creators exists diff --git a/src/Formats/Mobi/MobiModule.php b/src/Formats/Mobi/MobiModule.php index 425da2e..c1968aa 100644 --- a/src/Formats/Mobi/MobiModule.php +++ b/src/Formats/Mobi/MobiModule.php @@ -10,6 +10,7 @@ use Kiwilan\Ebook\Formats\Mobi\Parser\MobiReader; use Kiwilan\Ebook\Models\BookAuthor; use Kiwilan\Ebook\Models\BookIdentifier; +use Kiwilan\Ebook\Utils\EbookUtils; /** * @docs https://stackoverflow.com/questions/11817047/php-library-to-parse-mobi @@ -33,7 +34,13 @@ public function toEbook(): Ebook return $this->ebook; } + $authors = []; foreach ($this->parser->get(MobiReader::AUTHOR_100, true) as $author) { + $authors[] = $author; + } + + $authors = EbookUtils::parseStringWithSeperator($authors); + foreach ($authors as $author) { $this->ebook->setAuthor(new BookAuthor($author)); } diff --git a/src/Formats/Pdf/PdfModule.php b/src/Formats/Pdf/PdfModule.php index adae17d..70401c9 100644 --- a/src/Formats/Pdf/PdfModule.php +++ b/src/Formats/Pdf/PdfModule.php @@ -7,6 +7,7 @@ use Kiwilan\Ebook\EbookCover; use Kiwilan\Ebook\Formats\EbookModule; use Kiwilan\Ebook\Models\BookAuthor; +use Kiwilan\Ebook\Utils\EbookUtils; class PdfModule extends EbookModule { @@ -27,16 +28,7 @@ public function toEbook(): Ebook $author = $this->meta?->getAuthor(); if ($author !== null) { - $authors = []; - if (str_contains($author, ',')) { - $authors = explode(',', $author); - } elseif (str_contains($author, '&')) { - $authors = explode(',', $author); - } elseif (str_contains($author, 'and')) { - $authors = explode(',', $author); - } else { - $authors[] = $author; - } + $authors = EbookUtils::parseStringWithSeperator($author); $creators = []; foreach ($authors as $author) { @@ -49,9 +41,9 @@ public function toEbook(): Ebook } $this->ebook->setDescription($this->meta?->getSubject()); $this->ebook->setPublisher($this->meta?->getCreator()); - $this->ebook->setTags($this->meta?->getKeywords()); + $keywords = EbookUtils::parseStringWithSeperator($this->meta?->getKeywords()); + $this->ebook->setTags($keywords); $this->ebook->setPublishDate($this->meta?->getCreationDate()); - $this->ebook->setHasParser(true); return $this->ebook; diff --git a/src/Models/MetaTitle.php b/src/Models/MetaTitle.php index df105f1..43c4a01 100644 --- a/src/Models/MetaTitle.php +++ b/src/Models/MetaTitle.php @@ -40,7 +40,7 @@ class MetaTitle ], 'fr' => [ 'les ', - 'l\' ', + 'l\'', 'le ', 'la ', 'du ', @@ -296,13 +296,17 @@ protected function __construct( protected ?string $slugSimple = null, protected ?string $seriesSlug = null, protected ?string $seriesSlugSimple = null, + + protected bool $useIntl = true, ) { } /** * Create a new MetaTitle instance from an Ebook. + * + * @param bool $useIntl Use intl extension for slugify. */ - public static function fromEbook(Ebook $ebook): ?self + public static function fromEbook(Ebook $ebook, bool $useIntl = true): ?self { if (! $ebook->getTitle()) { return null; @@ -317,6 +321,7 @@ public static function fromEbook(Ebook $ebook): ?self year: $ebook->getPublishDate()?->format('Y'), extension: $ebook->getExtension(), ); + $self->useIntl = $useIntl; $self->parse(); return $self; @@ -324,6 +329,8 @@ public static function fromEbook(Ebook $ebook): ?self /** * Create a new MetaTitle instance from data. + * + * @param bool $useIntl Use intl extension for slugify. */ public static function fromData( string $title, @@ -333,6 +340,7 @@ public static function fromData( ?string $author = null, string|int|null $year = null, ?string $extension = null, + bool $useIntl = true, ): self { $self = new self( title: $title, @@ -343,6 +351,7 @@ public static function fromData( year: (string) $year, extension: $extension, ); + $self->useIntl = $useIntl; $self->parse(); return $self; @@ -353,18 +362,24 @@ private function parse(): static $title = $this->generateSlug($this->title); $language = $this->language ? $this->generateSlug($this->language) : null; $series = $this->series ? $this->generateSlug($this->series) : null; - $volume = $this->volume ? str_pad((string) $this->volume, 2, '0', STR_PAD_LEFT) : null; + $volume = $this->parseVolume($this->volume); $author = $this->author ? $this->generateSlug($this->author) : null; $year = $this->year ? $this->generateSlug($this->year) : null; - $extension = strtolower($this->extension); - - $titleDeterminer = $this->removeDeterminers($this->title, $this->language); - $seriesDeterminer = $this->removeDeterminers($this->series, $this->language); + $extension = $this->extension ? strtolower($this->extension) : null; if (! $title) { return $this; } + $title = $this->removeDots($title); + $language = $this->removeDots($language); + $series = $this->removeDots($series); + $author = $this->removeDots($author); + $year = $this->removeDots($year); + + $titleDeterminer = $this->removeDeterminers($this->title, $this->language); + $seriesDeterminer = $this->removeDeterminers($this->series, $this->language); + if ($this->series) { $this->slug = $this->generateSlug([ $seriesDeterminer, @@ -502,6 +517,40 @@ public function getUniqueFilename(): string return $this->slug; } + private function parseVolume(?string $volume): ?string + { + if ($volume === null) { + return null; + } + + if ($volume == '0') { + return '000'; + } + + $decimals = null; + + if (str_contains($volume, '.')) { + $explode = explode('.', $volume); + $volume = $explode[0]; + $decimals = $explode[1]; + } + + if (str_contains($volume, ',')) { + $explode = explode(',', $volume); + $volume = $explode[0]; + $decimals = $explode[1]; + } + + // add `0` before volume number to get `000` format + $volume = str_pad($volume, 3, '0', STR_PAD_LEFT); + + if ($decimals) { + $volume .= '.'.$decimals; + } + + return $volume; + } + private function removeDeterminers(?string $string, ?string $language): ?string { if (! $string) { @@ -516,6 +565,10 @@ private function removeDeterminers(?string $string, ?string $language): ?string $articlesLang = $articles[$language]; } + $uppercaseArticles = array_map('ucfirst', $articlesLang); + $lowercaseArticles = array_map('lcfirst', $articlesLang); + $articlesLang = array_merge($uppercaseArticles, $lowercaseArticles); + foreach ($articlesLang as $key => $value) { $string = preg_replace('/^'.preg_quote($value, '/').'/i', '', $string); } @@ -574,32 +627,41 @@ private function slugifier(?string $title, string $separator = '-', array $dicti return null; } - if (! extension_loaded('intl')) { - return $this->slugifierNative($title, $separator); + if (extension_loaded('intl') && $this->useIntl) { + return $this->slugifierIntl($title, $separator, $dictionary); + } + + return $this->slugifierNative($title, $separator); + } + + private function slugifierIntl(?string $text, string $divider = '-', array $dictionary = ['@' => 'at']): ?string + { + if (! $text) { + return null; } $transliterator = Transliterator::createFromRules(':: Any-Latin; :: Latin-ASCII; :: NFD; :: [:Nonspacing Mark:] Remove; :: Lower(); :: NFC;', Transliterator::FORWARD); - $title = $transliterator->transliterate($title); + $text = $transliterator->transliterate($text); // Convert all dashes/underscores into separator - $flip = $separator === '-' ? '_' : '-'; + $flip = $divider === '-' ? '_' : '-'; - $title = preg_replace('!['.preg_quote($flip).']+!u', $separator, $title); + $text = preg_replace('!['.preg_quote($flip).']+!u', $divider, $text); // Replace dictionary words foreach ($dictionary as $key => $value) { - $dictionary[$key] = $separator.$value.$separator; + $dictionary[$key] = $divider.$value.$divider; } - $title = str_replace(array_keys($dictionary), array_values($dictionary), $title); + $text = str_replace(array_keys($dictionary), array_values($dictionary), $text); // Remove all characters that are not the separator, letters, numbers, or whitespace - $title = preg_replace('![^'.preg_quote($separator).'\pL\pN\s]+!u', '', strtolower($title)); + $text = preg_replace('![^'.preg_quote($divider).'\pL\pN\s\.]+!u', '', strtolower($text)); // Replace all separator characters and whitespace by a single separator - $title = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $title); + $text = preg_replace('!['.preg_quote($divider).'\s]+!u', $divider, $text); - return trim($title, $separator); + return trim($text, $divider); } private function slugifierNative(?string $text, string $divider = '-'): ?string @@ -608,14 +670,17 @@ private function slugifierNative(?string $text, string $divider = '-'): ?string return null; } + // remove `'` and `"` characters + $text = str_replace(["'"], '', $text); + // replace non letter or digits by divider - $text = preg_replace('~[^\pL\d]+~u', $divider, $text); + $text = preg_replace('~[^\pL\d.]+~u', $divider, $text); // transliterate - $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text); + $text = $this->removeAccents($text); // remove unwanted characters - $text = preg_replace('~[^-\w]+~', '', $text); + $text = preg_replace('~[^-\w.]+~', '', $text); // trim $text = trim($text, $divider); @@ -632,4 +697,25 @@ private function slugifierNative(?string $text, string $divider = '-'): ?string return $text; } + + private function removeDots(?string $string): ?string + { + if (! $string) { + return null; + } + + return str_replace('.', ' ', $string); + } + + public function removeAccents(?string $string): ?string + { + if (! $string) { + return null; + } + + $string = htmlentities($string, ENT_COMPAT, 'UTF-8'); + $string = preg_replace('/&([a-zA-Z])(uml|acute|grave|circ|tilde|ring);/', '$1', $string); + + return html_entity_decode($string); + } } diff --git a/src/Utils/EbookUtils.php b/src/Utils/EbookUtils.php index 9651b43..c8fc572 100644 --- a/src/Utils/EbookUtils.php +++ b/src/Utils/EbookUtils.php @@ -4,6 +4,42 @@ class EbookUtils { + /** + * @return string[]|null|string|int + */ + public static function parseStringWithSeperator(mixed $content): mixed + { + if (! $content) { + return null; + } + + if (! is_string($content)) { + return $content; + } + + if (str_contains($content, ',')) { + $content = explode(',', $content); + } elseif (str_contains($content, ';')) { + $content = explode(';', $content); + } elseif (str_contains($content, '&')) { + $content = explode('&', $content); + } elseif (str_contains($content, 'and')) { + $content = explode('and', $content); + } elseif (str_contains($content, '/')) { + $content = explode('/', $content); + } elseif (str_contains($content, '//')) { + $content = explode('//', $content); + } else { + $content = [$content]; + } + + if (is_array($content)) { + $content = array_map('trim', $content); + } + + return $content; + } + public static function parseNumber(mixed $number): int|float|null { if (EbookUtils::isFloat($number)) { diff --git a/tests/EpubTest.php b/tests/EpubTest.php index 3df9ce1..4e5f121 100644 --- a/tests/EpubTest.php +++ b/tests/EpubTest.php @@ -71,7 +71,7 @@ $ebook = Ebook::read(EPUB); $meta = $ebook->getMetaTitle(); - expect($meta->getSlug())->toBe('earths-children-en-01-clan-of-the-cave-bear-jean-m-auel-1980-epub'); + expect($meta->getSlug())->toBe('earths-children-en-001-clan-of-the-cave-bear-jean-m-auel-1980-epub'); expect($meta->getSeriesSlug())->toBe('earths-children-en-jean-m-auel-epub'); expect($meta->toArray())->toBeArray(); @@ -194,6 +194,7 @@ $ebook = Ebook::read($path); expect($ebook->getVolume())->toBe(1.5); + expect($ebook->getMetaTitle()?->getSlug())->toBe('enfants-de-la-terre-fr-001.5-clan-de-lours-des-cavernes-jean-m-auel-1980-epub'); })->with([EPUB_VOLFLOAT]); it('can parse epub with bad summary', function (string $path) { diff --git a/tests/MetaTitleTest.php b/tests/MetaTitleTest.php index 09b39c8..5cbeff3 100644 --- a/tests/MetaTitleTest.php +++ b/tests/MetaTitleTest.php @@ -14,7 +14,7 @@ $ebook->setAuthorMain(new BookAuthor('Pierre Bottero')); $meta = MetaTitle::fromEbook($ebook); - expect($meta->getSlug())->toBe('a-comme-association-fr-01-pale-lumiere-des-tenebres-pierre-bottero-1980-epub'); + expect($meta->getSlug())->toBe('a-comme-association-fr-001-pale-lumiere-des-tenebres-pierre-bottero-1980-epub'); expect($meta->getSlugSimple())->toBe('la-pale-lumiere-des-tenebres'); expect($meta->getSeriesSlug())->toBe('a-comme-association-fr-pierre-bottero-epub'); expect($meta->getSeriesSlugSimple())->toBe('a-comme-association'); @@ -26,7 +26,7 @@ $ebook->setAuthorMain(new BookAuthor('J. R. R. Tolkien')); $meta = MetaTitle::fromEbook($ebook); - expect($meta->getSlug())->toBe('lord-of-the-rings-en-01-fellowship-of-the-ring-j-r-r-tolkien-1980-epub'); + expect($meta->getSlug())->toBe('lord-of-the-rings-en-001-fellowship-of-the-ring-j-r-r-tolkien-1980-epub'); expect($meta->getSlugSimple())->toBe('the-fellowship-of-the-ring'); expect($meta->getSeriesSlug())->toBe('lord-of-the-rings-en-j-r-r-tolkien-epub'); expect($meta->getSeriesSlugSimple())->toBe('the-lord-of-the-rings'); @@ -53,8 +53,58 @@ extension: 'epub', ); - expect($meta->getSlug())->toBe('lord-of-the-rings-en-01-fellowship-of-the-ring-j-r-r-tolkien-1980-epub'); + expect($meta->getSlug())->toBe('lord-of-the-rings-en-001-fellowship-of-the-ring-j-r-r-tolkien-1980-epub'); expect($meta->getSlugSimple())->toBe('the-fellowship-of-the-ring'); expect($meta->getSeriesSlug())->toBe('lord-of-the-rings-en-j-r-r-tolkien-epub'); expect($meta->getSeriesSlugSimple())->toBe('the-lord-of-the-rings'); }); + +it('can slug without intl', function () { + $meta = MetaTitle::fromData( + title: 'La pâle lumière des ténèbres', + language: 'fr', + series: 'A comme Association', + volume: 1.5, + author: 'Pierre Bottero', + extension: 'epub', + useIntl: false, + ); + + expect($meta->getSlug())->toBe('a-comme-association-fr-001.5-pale-lumiere-des-tenebres-pierre-bottero-epub'); + expect($meta->getSlugSimple())->toBe('la-pale-lumiere-des-tenebres'); + expect($meta->getSeriesSlug())->toBe('a-comme-association-fr-pierre-bottero-epub'); + expect($meta->getSeriesSlugSimple())->toBe('a-comme-association'); +}); + +it('can use alt volume', function () { + $ebook = Ebook::read(EPUB_VOLFLOAT); + $meta = $ebook->getMetaTitle(); + + expect($meta->getSlug())->toBe('enfants-de-la-terre-fr-001.5-clan-de-lours-des-cavernes-jean-m-auel-1980-epub'); +}); + +it('can use determiner title', function () { + $ebook = Ebook::read(EPUB); + $ebook->setSeries("L'Assassin Royal"); + $ebook->setLanguage('fr'); + $ebook->setAuthorMain(new BookAuthor('Robin Hobb')); + + $ebook->setTitle("L'apprenti assassin"); + $ebook->setVolume(1); + $meta = MetaTitle::fromEbook($ebook); + + expect($meta->getSlug())->toBe('assassin-royal-fr-001-apprenti-assassin-robin-hobb-1980-epub'); + + $ebook->setTitle('Le Prince Bâtard'); + $ebook->setVolume(0); + $meta = MetaTitle::fromEbook($ebook); + + expect($meta->getSlug())->toBe('assassin-royal-fr-000-prince-batard-robin-hobb-1980-epub'); + ray($ebook->toArray()); + + $ebook->setTitle("L'apprenti assassin"); + $ebook->setVolume(50); + $meta = MetaTitle::fromEbook($ebook); + + expect($meta->getSlug())->toBe('assassin-royal-fr-050-apprenti-assassin-robin-hobb-1980-epub'); +}); diff --git a/tests/OpfTest.php b/tests/OpfTest.php index 2b7b0cf..42afa1b 100644 --- a/tests/OpfTest.php +++ b/tests/OpfTest.php @@ -137,6 +137,11 @@ })->with([EPUB_OPF_EMPTY_CREATOR]); it('can use float volume', function () { - $opf = OpfItem::make(file_get_contents(EPUB_OPF_EPUB2_VOLUME_FLOAT), EPUB_OPF_EPUB2_VOLUME_FLOAT); + $opf = OpfItem::make(file_get_contents(EPUB_OPF_EPUB2_VOLUME_FLOAT)); expect($opf->getMetaItem('calibre:series_index')->getContents())->toBe('1.5'); }); + +it('can use multiple authors', function (string $path) { + $opf = OpfItem::make(file_get_contents($path)); + expect($opf->getDcCreators())->toHaveCount(2); +})->with([EPUB_OPF_MULTIPLE_AUTHORS, EPUB_OPF_MULTIPLE_AUTHORS_MERGE]); diff --git a/tests/Pest.php b/tests/Pest.php index a07d1fe..b80f57f 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -34,6 +34,8 @@ define('EPUB_OPF_NOT_FORMATTED', __DIR__.'/media/opf-not-formatted.opf'); define('EPUB_OPF_EMPTY_CREATOR', __DIR__.'/media/opf-epub2-empty-creator.opf'); define('EPUB_OPF_LA5EVAGUE', __DIR__.'/media/opf-content-la-5e-vague.opf'); +define('EPUB_OPF_MULTIPLE_AUTHORS', __DIR__.'/media/opf-epub2-multiple-authors.opf'); +define('EPUB_OPF_MULTIPLE_AUTHORS_MERGE', __DIR__.'/media/opf-epub2-multiple-authors-merge.opf'); define('EPUB', __DIR__.'/media/test-epub.epub'); define('EPUB_ONE_TAG', __DIR__.'/media/epub-one-tag.epub'); diff --git a/tests/media/opf-epub2-multiple-authors-merge.opf b/tests/media/opf-epub2-multiple-authors-merge.opf new file mode 100644 index 0000000..84d895d --- /dev/null +++ b/tests/media/opf-epub2-multiple-authors-merge.opf @@ -0,0 +1,84 @@ + + + + Le clan de l'ours des cavernes + Jean M. Auel, Philippe + Rouard + calibre (6.12.0) [https://calibre-ebook.com] + <div> + <p>Quelque part en Europe, 35 000 ans avant notre ère. Petite fille Cro-Magnon de cinq + ans, Ayla est séparée de ses parents à la suite d'un violent tremblement de terre. Elle est + recueillie par le clan de l'ours des cavernes, une tribu Neandertal qui l'adopte, non sans + réticence, ayant reconnu en elle la représentante d'une autre espèce, plus évoluée. + <br><br>Iza, la guérisseuse, Brun, le chef et Creb, le magicien lui enseignent les + règles de la vie communautaire, leurs rites, leurs peurs, leurs audaces. Mais Ayla, la + fillette blonde aux yeux bleus les surprend par sa puissance de raisonnement qui lui permet de + s'adapter, de réagir rapidement et de ne pas être totalement dépendante de son environnement. + Une différence qui ne tarde pas à faire d'elle une menace pour tout le clan, et à attiser la + convoitise de Brud, le fils du chef...</p></div> + Presses de la cité + a2cf2f25-4de2-4f77-82cc-0198352b0851 + 1980-01-13T21:00:00+00:00 + Roman Historique + Les Enfants de la Terre + Fiction + fr + a2cf2f25-4de2-4f77-82cc-0198352b0851 + 63CTHAAACAAJ + 9782266122122 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/media/opf-epub2-multiple-authors.opf b/tests/media/opf-epub2-multiple-authors.opf new file mode 100644 index 0000000..7862e78 --- /dev/null +++ b/tests/media/opf-epub2-multiple-authors.opf @@ -0,0 +1,84 @@ + + + + Le clan de l'ours des cavernes + Jean M. Auel + Philippe Rouard + calibre (6.12.0) [https://calibre-ebook.com] + <div> + <p>Quelque part en Europe, 35 000 ans avant notre ère. Petite fille Cro-Magnon de cinq + ans, Ayla est séparée de ses parents à la suite d'un violent tremblement de terre. Elle est + recueillie par le clan de l'ours des cavernes, une tribu Neandertal qui l'adopte, non sans + réticence, ayant reconnu en elle la représentante d'une autre espèce, plus évoluée. + <br><br>Iza, la guérisseuse, Brun, le chef et Creb, le magicien lui enseignent les + règles de la vie communautaire, leurs rites, leurs peurs, leurs audaces. Mais Ayla, la + fillette blonde aux yeux bleus les surprend par sa puissance de raisonnement qui lui permet de + s'adapter, de réagir rapidement et de ne pas être totalement dépendante de son environnement. + Une différence qui ne tarde pas à faire d'elle une menace pour tout le clan, et à attiser la + convoitise de Brud, le fils du chef...</p></div> + Presses de la cité + a2cf2f25-4de2-4f77-82cc-0198352b0851 + 1980-01-13T21:00:00+00:00 + Roman Historique + Les Enfants de la Terre + Fiction + fr + a2cf2f25-4de2-4f77-82cc-0198352b0851 + 63CTHAAACAAJ + 9782266122122 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file