Skip to content

Commit 23348c1

Browse files
committed
feature: add txt support
#5
1 parent 78f921d commit 23348c1

File tree

16 files changed

+4885
-30
lines changed

16 files changed

+4885
-30
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,6 @@ jobs:
1010
fail-fast: false
1111
matrix:
1212
include:
13-
- operating-system: 'ubuntu-latest'
14-
php-version: '8.2'
15-
job-description: 'Ubuntu; PHP 8.2; latest-deps'
16-
17-
- operating-system: 'ubuntu-latest'
18-
php-version: '8.2'
19-
composer-flags: '--prefer-lowest'
20-
job-description: 'Ubuntu; PHP 8.2; lowest-deps'
21-
22-
- operating-system: 'ubuntu-latest'
23-
php-version: '8.3'
24-
job-description: 'Ubuntu; PHP 8.3; latest-deps'
25-
26-
- operating-system: 'ubuntu-latest'
27-
php-version: '8.3'
28-
composer-flags: '--prefer-lowest'
29-
job-description: 'Ubuntu; PHP 8.3; lowest-deps'
30-
3113
- operating-system: 'ubuntu-latest'
3214
php-version: '8.4'
3315
job-description: 'Ubuntu; PHP 8.4; latest-deps'

.php-cs-fixer.dist.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
'@Symfony' => true,
1010
'@Symfony:risky' => true,
1111
'@PHP82Migration:risky' => true,
12-
'@PHP82Migration' => true,
12+
'@PHP84Migration' => true,
1313
'@PHPUnit100Migration:risky' => true,
1414

1515
'combine_consecutive_issets' => true,

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
- EPUB
1010
- MOBI
1111
- FB2, FB2-ZIP
12+
- TXT, TXT-ZIP
1213

1314

1415
### Requirements:
15-
- PHP >= 8.2
16+
- PHP >= 8.4
1617
- ext-zip
1718
- ext-dom
1819
- ext-xmlreader
20+
- ext-mbstring
1921

2022

2123
### Installation:

composer.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "gemorroj/ebook-reader",
33
"description": "E-books reader",
4-
"keywords": ["ebook","mobi","epub","fb2"],
4+
"keywords": ["ebook","mobi","epub","fb2","txt"],
55
"type": "library",
66
"license": "LGPL-3.0-or-later",
77
"authors": [
@@ -10,14 +10,15 @@
1010
}
1111
],
1212
"require": {
13-
"php": ">=8.2",
13+
"php": ">=8.4",
1414
"ext-zip": "*",
1515
"ext-dom": "*",
1616
"ext-xmlreader": "*",
17-
"ext-libxml": "*"
17+
"ext-libxml": "*",
18+
"ext-mbstring": "*"
1819
},
1920
"require-dev": {
20-
"phpunit/phpunit": "^11.5.15",
21+
"phpunit/phpunit": "^12.2.1",
2122
"friendsofphp/php-cs-fixer": "^3.75",
2223
"phpstan/phpstan": "^2.1"
2324
},

src/Data/TxtData.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace EbookReader\Data;
6+
7+
use EbookReader\EbookDataInterface;
8+
use EbookReader\Resource\Style;
9+
10+
final readonly class TxtData implements EbookDataInterface
11+
{
12+
/**
13+
* @param Style[] $styles
14+
*/
15+
public function __construct(
16+
private string $text,
17+
private ?string $title,
18+
private array $styles = [],
19+
) {
20+
}
21+
22+
public function getText(): string
23+
{
24+
return $this->text;
25+
}
26+
27+
public function getTitle(): ?string
28+
{
29+
return $this->title;
30+
}
31+
32+
/**
33+
* @return Style[]
34+
*/
35+
public function getStyles(): array
36+
{
37+
return $this->styles;
38+
}
39+
}

src/Driver/Epub3Driver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ protected function makeDescription(\DOMElement $metadataNode): ?string
255255
$descriptionNode = $metadataNode->getElementsByTagName('description')->item(0);
256256

257257
if ($descriptionNode) {
258-
return \trim($descriptionNode->nodeValue);
258+
return \mb_trim($descriptionNode->nodeValue);
259259
}
260260

261261
return null;

src/Driver/Fb2Driver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ protected function makeDescription(\DOMElement $titleInfoNode): ?string
241241
}
242242
}
243243

244-
return \trim(\implode('', $text));
244+
return \mb_trim(\implode('', $text));
245245
}
246246

247247
return null;

src/Driver/MobiDriver.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public function isValid(): bool
2626
$exthData = $this->parseExth($f);
2727
} catch (\Exception $e) {
2828
return false;
29+
} finally {
30+
unset($f); // close file
2931
}
3032

3133
return true;
@@ -256,7 +258,7 @@ protected function parseExth(\SplFileObject $f): array
256258
*/
257259
protected function parsePalmDb(\SplFileObject $f): array
258260
{
259-
$name = \trim($f->fread(32));
261+
$name = \mb_trim($f->fread(32));
260262
$attributes = \hexdec(\bin2hex($f->fread(2)));
261263
$version = \hexdec(\bin2hex($f->fread(2)));
262264
$creationDate = \hexdec(\bin2hex($f->fread(4)));

src/Driver/TxtDriver.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace EbookReader\Driver;
6+
7+
use EbookReader\Data\TxtData;
8+
use EbookReader\Exception\ParserException;
9+
use EbookReader\Meta\TxtMeta;
10+
11+
final class TxtDriver extends AbstractDriver
12+
{
13+
private ?string $internalFile = null;
14+
15+
protected function getInternalFile(): string
16+
{
17+
if (!$this->internalFile) {
18+
$zip = new \ZipArchive();
19+
$res = $zip->open($this->getFile(), \ZipArchive::RDONLY);
20+
if (true === $res) {
21+
$txtFile = null;
22+
// get first .txt file
23+
for ($i = 0; $i < $zip->numFiles; ++$i) {
24+
$fileName = $zip->getNameIndex($i);
25+
if (false === $fileName) {
26+
continue;
27+
}
28+
if ('txt' === \pathinfo($fileName, \PATHINFO_EXTENSION)) {
29+
$txtFile = $fileName;
30+
break;
31+
}
32+
}
33+
$zip->close();
34+
if (null === $txtFile) {
35+
throw new ParserException();
36+
}
37+
38+
$this->internalFile = 'zip://'.$this->getFile().'#'.$txtFile;
39+
} else {
40+
$this->internalFile = 'file://'.$this->getFile();
41+
}
42+
}
43+
44+
return $this->internalFile;
45+
}
46+
47+
public function isValid(): bool
48+
{
49+
try {
50+
$f = new \SplFileObject($this->getInternalFile(), 'r');
51+
} catch (\Exception $e) {
52+
return false;
53+
} finally {
54+
unset($f); // close file
55+
}
56+
57+
return true;
58+
}
59+
60+
/**
61+
* @return TxtData[]
62+
*/
63+
public function getData(): array
64+
{
65+
try {
66+
$f = new \SplFileObject($this->getInternalFile(), 'r');
67+
} catch (\Exception $e) {
68+
throw new ParserException(previous: $e);
69+
}
70+
71+
try {
72+
$text = '';
73+
while (!$f->eof()) {
74+
$text .= $f->fread(4096);
75+
}
76+
} finally {
77+
unset($f); // close file
78+
}
79+
80+
$text = \mb_trim($text);
81+
$pos = \strpos($text, "\n");
82+
if (false !== $pos) {
83+
$title = \substr($text, 0, $pos);
84+
$title = \mb_trim($title);
85+
} else {
86+
$title = null;
87+
}
88+
89+
return [
90+
new TxtData($text, $title, []),
91+
];
92+
}
93+
94+
public function getCover(): ?string
95+
{
96+
// todo
97+
throw new \RuntimeException('Not implemented');
98+
}
99+
100+
public function getMeta(): TxtMeta
101+
{
102+
try {
103+
$f = new \SplFileObject($this->getInternalFile(), 'r');
104+
} catch (\Exception $e) {
105+
throw new ParserException(previous: $e);
106+
}
107+
108+
try {
109+
$text = '';
110+
while (!$f->eof()) {
111+
$text = \mb_trim($f->fgets());
112+
if ('' !== $text) {
113+
break;
114+
}
115+
}
116+
} finally {
117+
unset($f); // close file
118+
}
119+
120+
return new TxtMeta($text);
121+
}
122+
}

src/Meta/TxtMeta.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace EbookReader\Meta;
6+
7+
use EbookReader\EbookMetaInterface;
8+
9+
final readonly class TxtMeta implements EbookMetaInterface
10+
{
11+
public function __construct(
12+
private string $title,
13+
private ?string $author = null,
14+
private ?string $publisher = null,
15+
private ?string $isbn = null,
16+
private ?string $description = null,
17+
private ?string $language = null,
18+
private ?string $license = null,
19+
private ?int $publishYear = null,
20+
private ?int $publishMonth = null,
21+
private ?int $publishDay = null,
22+
) {
23+
}
24+
25+
public function getTitle(): string
26+
{
27+
return $this->title;
28+
}
29+
30+
public function getAuthor(): ?string
31+
{
32+
return $this->author;
33+
}
34+
35+
public function getPublisher(): ?string
36+
{
37+
return $this->publisher;
38+
}
39+
40+
public function getIsbn(): ?string
41+
{
42+
return $this->isbn;
43+
}
44+
45+
public function getDescription(): ?string
46+
{
47+
return $this->description;
48+
}
49+
50+
public function getLanguage(): ?string
51+
{
52+
return $this->language;
53+
}
54+
55+
public function getLicense(): ?string
56+
{
57+
return $this->license;
58+
}
59+
60+
public function getPublishYear(): ?int
61+
{
62+
return $this->publishYear;
63+
}
64+
65+
public function getPublishMonth(): ?int
66+
{
67+
return $this->publishMonth;
68+
}
69+
70+
public function getPublishDay(): ?int
71+
{
72+
return $this->publishDay;
73+
}
74+
}

0 commit comments

Comments
 (0)