txt: more robust TXT parsing (#1706)
This commit is contained in:
@@ -72,6 +72,7 @@ export const useBooksSync = ({ onSyncStart, onSyncEnd }: UseBooksSyncProps) => {
|
||||
if (matchingBook) {
|
||||
if (!matchingBook.deletedAt && matchingBook.uploadedAt && !oldBook.coverDownloadedAt) {
|
||||
await appService?.downloadBook(oldBook, true);
|
||||
oldBook.coverImageUrl = await appService?.generateCoverImageUrl(oldBook);
|
||||
}
|
||||
const mergedBook =
|
||||
matchingBook.updatedAt > oldBook.updatedAt
|
||||
@@ -92,7 +93,6 @@ export const useBooksSync = ({ onSyncStart, onSyncEnd }: UseBooksSyncProps) => {
|
||||
console.error('Failed to download book:', newBook);
|
||||
} finally {
|
||||
newBook.coverImageUrl = await appService?.generateCoverImageUrl(newBook);
|
||||
newBook.coverImageUrl += '?timestamp=' + Date.now();
|
||||
updatedLibrary.push(newBook);
|
||||
setLibrary(updatedLibrary);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ const indexedDBFileSystem: FileSystem = {
|
||||
const content = await this.readFile(path, base, 'binary');
|
||||
return URL.createObjectURL(new Blob([content]));
|
||||
} catch {
|
||||
console.warn(`Failed to get Blob URL for path: ${path} in base: ${base}`);
|
||||
return path;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ interface Txt2EpubOptions {
|
||||
|
||||
interface ExtractChapterOptions {
|
||||
linesBetweenSegments: number;
|
||||
paragraphsPerChapter?: number;
|
||||
fallbackParagraphsPerChapter: number;
|
||||
}
|
||||
|
||||
interface ConversionResult {
|
||||
@@ -68,9 +68,10 @@ export class TxtToEpubConverter {
|
||||
const metadata = { bookTitle, author, language, identifier };
|
||||
|
||||
let chapters: Chapter[] = [];
|
||||
for (let i = 4; i >= 3; i--) {
|
||||
for (let i = 8; i >= 6; i--) {
|
||||
chapters = this.extractChapters(txtContent, metadata, {
|
||||
linesBetweenSegments: i,
|
||||
fallbackParagraphsPerChapter: 100,
|
||||
});
|
||||
|
||||
if (chapters.length === 0) {
|
||||
@@ -79,12 +80,6 @@ export class TxtToEpubConverter {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (chapters.length === 1) {
|
||||
chapters = this.extractChapters(txtContent, metadata, {
|
||||
linesBetweenSegments: 4,
|
||||
paragraphsPerChapter: 100,
|
||||
});
|
||||
}
|
||||
|
||||
const blob = await this.createEpub(chapters, metadata);
|
||||
return {
|
||||
@@ -101,24 +96,38 @@ export class TxtToEpubConverter {
|
||||
option: ExtractChapterOptions,
|
||||
): Chapter[] {
|
||||
const { language } = metadata;
|
||||
const { linesBetweenSegments, paragraphsPerChapter } = option;
|
||||
const { linesBetweenSegments, fallbackParagraphsPerChapter } = option;
|
||||
const segmentRegex = new RegExp(`(?:\\r?\\n){${linesBetweenSegments},}|-{8,}\r?\n`);
|
||||
let chapterRegex: RegExp;
|
||||
const chapterRegexps: RegExp[] = [];
|
||||
if (language === 'zh') {
|
||||
chapterRegex = new RegExp(
|
||||
String.raw`(?:^|\n|\s)` +
|
||||
'(' +
|
||||
[
|
||||
String.raw`第[零〇一二三四五六七八九十0-9][零〇一二三四五六七八九十百千万0-9]*(?:[章卷节回讲篇封])(?:[::、 \(\)0-9]*[^\n-]{0,24})(?!\S)`,
|
||||
String.raw`(?:^|\n|\s|《[^》]+》)[一二三四五六七八九十][零〇一二三四五六七八九十百千万]*(?:[:: ][^\n-]{0,24})(?!\S)`,
|
||||
String.raw`(?:楔子|前言|引言|序言|序章|总论|概论)(?:[:: ][^\n-]{0,24})?(?!\S)`,
|
||||
].join('|') +
|
||||
')',
|
||||
'gu',
|
||||
chapterRegexps.push(
|
||||
new RegExp(
|
||||
String.raw`(?:^|\n)\s*` +
|
||||
'(' +
|
||||
[
|
||||
String.raw`第[零〇一二三四五六七八九十0-9][零〇一二三四五六七八九十百千万0-9]*(?:[章卷节回讲篇封])(?:[::、 \(\)0-9]*[^\n-]{0,24})(?!\S)`,
|
||||
String.raw`(?:楔子|前言|简介|引言|序言|序章|总论|概论)(?:[:: ][^\n-]{0,24})?(?!\S)`,
|
||||
].join('|') +
|
||||
')',
|
||||
'gu',
|
||||
),
|
||||
);
|
||||
chapterRegexps.push(
|
||||
new RegExp(
|
||||
String.raw`(?:^|\n)\s*` +
|
||||
'(' +
|
||||
[
|
||||
String.raw`[一二三四五六七八九十][零〇一二三四五六七八九十百千万]?[::、 ][^\n-]{0,24}(?=\n|$)`,
|
||||
String.raw`[0-9]+[^\n]{0,16}(?=\n|$)`,
|
||||
].join('|') +
|
||||
')',
|
||||
'gu',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
chapterRegex =
|
||||
/(?:^|\n|\s)(?:(Chapter|Part)\s+(\d+|[IVXLCDM]+)(?:[:.\-–—]?\s+[^\n]*)?|(?:Prologue|Epilogue|Introduction|Foreword)(?:[:.\-–—]?\s+[^\n]*)?)(?=\s|$)/gi;
|
||||
chapterRegexps.push(
|
||||
/(?:^|\n|\s)(?:(Chapter|Part)\s+(\d+|[IVXLCDM]+)(?:[:.\-–—]?\s+[^\n]*)?|(?:Prologue|Epilogue|Introduction|Foreword)(?:[:.\-–—]?\s+[^\n]*)?)(?=\s|$)/gi,
|
||||
);
|
||||
}
|
||||
|
||||
const formatSegment = (segment: string): string => {
|
||||
@@ -149,17 +158,35 @@ export class TxtToEpubConverter {
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const isGoodMatches = (matches: string[], maxLength: number = 100000): boolean => {
|
||||
const meaningfulParts = matches.filter((part) => part.trim().length > 0);
|
||||
if (meaningfulParts.length <= 1) return false;
|
||||
|
||||
const hasLongParts = meaningfulParts.some((part) => part.length > maxLength);
|
||||
return !hasLongParts;
|
||||
};
|
||||
|
||||
const chapters: Chapter[] = [];
|
||||
const segments = txtContent.split(segmentRegex);
|
||||
for (const segment of segments) {
|
||||
const trimmedSegment = segment.replace(/<!--.*?-->/g, '').trim();
|
||||
if (!trimmedSegment) continue;
|
||||
|
||||
if (paragraphsPerChapter && paragraphsPerChapter > 0) {
|
||||
const segmentChapters = [];
|
||||
let matches: string[] = [];
|
||||
for (const chapterRegex of chapterRegexps) {
|
||||
const tryMatches = trimmedSegment.split(chapterRegex);
|
||||
if (isGoodMatches(tryMatches)) {
|
||||
matches = joinAroundUndefined(tryMatches);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.length === 0 && fallbackParagraphsPerChapter > 0) {
|
||||
const paragraphs = trimmedSegment.split(/\n+/);
|
||||
const totalParagraphs = paragraphs.length;
|
||||
for (let i = 0; i < totalParagraphs; i += paragraphsPerChapter) {
|
||||
const chunks = paragraphs.slice(i, i + paragraphsPerChapter);
|
||||
for (let i = 0; i < totalParagraphs; i += fallbackParagraphsPerChapter) {
|
||||
const chunks = paragraphs.slice(i, i + fallbackParagraphsPerChapter);
|
||||
const formattedSegment = formatSegment(chunks.join('\n'));
|
||||
const title = `${chapters.length + 1}`;
|
||||
const content = `<h2>${title}</h2><p>${formattedSegment}</p>`;
|
||||
@@ -168,8 +195,6 @@ export class TxtToEpubConverter {
|
||||
continue;
|
||||
}
|
||||
|
||||
const segmentChapters = [];
|
||||
const matches = joinAroundUndefined(trimmedSegment.split(chapterRegex));
|
||||
for (let j = 1; j < matches.length; j += 2) {
|
||||
const title = matches[j]?.trim() || '';
|
||||
const content = matches[j + 1]?.trim() || '';
|
||||
|
||||
Reference in New Issue
Block a user