@@ -5,28 +5,28 @@ document.addEventListener('DOMContentLoaded', () => {
55
66 const ERRORS = {
77 en : {
8- tooLarge : ( size ) => `File too large. Please use an EPUB under ${ size } MB. ` ,
9- tooManyFiles : "EPUB has too many content files to process safely. " ,
10- noContent : "No readable HTML/XHTML content found in EPUB. " ,
11- missingOpf : "Invalid EPUB: OPF file declared in container.xml not found. " ,
12- invalidEpub : "Invalid EPUB/ZIP file. " ,
13- invalidOpf : "Invalid EPUB: OPF file is missing required sections. "
8+ tooLarge : ( size ) => `File too large. Please use an EPUB under ${ size } MB` ,
9+ tooManyFiles : "EPUB has too many content files to process safely" ,
10+ noContent : "No readable HTML/XHTML content found in EPUB" ,
11+ missingOpf : "Invalid EPUB: OPF file declared in container.xml not found" ,
12+ invalidEpub : "Invalid EPUB/ZIP file" ,
13+ invalidOpf : "Invalid EPUB: OPF file is missing required sections"
1414 } ,
1515 ja : {
16- tooLarge : ( size ) => `ファイルサイズが大きすぎます。${ size } MB未満のEPUBを使用してください。 ` ,
17- tooManyFiles : "EPUBに含まれるコンテンツファイルが多すぎるため、安全に処理できません。 " ,
18- noContent : "EPUB内に読み取り可能なHTML/XHTMLコンテンツが見つかりません。 " ,
19- missingOpf : "無効なEPUBです: container.xmlで指定されたOPFファイルが見つかりません。 " ,
20- invalidEpub : "無効なEPUB/ZIPファイルです。 " ,
21- invalidOpf : "無効なEPUBです: OPFファイルに必要なセクションが欠落しています。 "
16+ tooLarge : ( size ) => `ファイルサイズが大きすぎます。${ size } MB未満のEPUBを使用してください` ,
17+ tooManyFiles : "EPUBに含まれるコンテンツファイルが多すぎるため、安全に処理できません" ,
18+ noContent : "EPUB内に読み取り可能なHTML/XHTMLコンテンツが見つかりません" ,
19+ missingOpf : "無効なEPUBです: container.xmlで指定されたOPFファイルが見つかりません" ,
20+ invalidEpub : "無効なEPUB/ZIPファイルです" ,
21+ invalidOpf : "無効なEPUBです: OPFファイルに必要なセクションが欠落しています"
2222 } ,
2323 zh : {
24- tooLarge : ( size ) => `檔案過大,請使用小於 ${ size } MB 的 EPUB。 ` ,
25- tooManyFiles : "此 EPUB 內容檔案過多,無法安全處理。 " ,
26- noContent : "EPUB 中沒有可讀的 HTML/XHTML 內容。 " ,
27- missingOpf : "無效的 EPUB: container.xml 指定的 OPF 檔案不存在。 " ,
28- invalidEpub : "無效的 EPUB/ZIP 檔案。 " ,
29- invalidOpf : "無效的 EPUB: OPF 檔案缺少必要的區段。 "
24+ tooLarge : ( size ) => `檔案過大,請使用小於 ${ size } MB 的 EPUB` ,
25+ tooManyFiles : "此 EPUB 內容檔案過多,無法安全處理" ,
26+ noContent : "EPUB 中沒有可讀的 HTML/XHTML 內容" ,
27+ missingOpf : "無效的 EPUB: container.xml 指定的 OPF 檔案不存在" ,
28+ invalidEpub : "無效的 EPUB/ZIP 檔案" ,
29+ invalidOpf : "無效的 EPUB: OPF 檔案缺少必要的區段"
3030 }
3131 } ;
3232
@@ -80,8 +80,8 @@ document.addEventListener('DOMContentLoaded', () => {
8080 packaging : "Packaging ZIP..." ,
8181 processingFile : ( current , total ) => `File ${ current } /${ total } :` ,
8282 errorPrefix : "Error: " ,
83- onlyEpub : "Only .epub files are supported. " ,
84- genericError : "An unexpected error occurred. " ,
83+ onlyEpub : "Only .epub files are supported" ,
84+ genericError : "An unexpected error occurred" ,
8585 convertAnother : "Drag other .epub files to convert" ,
8686 selectFile : "select file(s)" ,
8787 downloadTxt : "Download TXT" ,
@@ -98,8 +98,8 @@ document.addEventListener('DOMContentLoaded', () => {
9898 packaging : "ZIPを作成中..." ,
9999 processingFile : ( current , total ) => `ファイル ${ current } /${ total } :` ,
100100 errorPrefix : "エラー: " ,
101- onlyEpub : ".epubファイルのみ対応しています。 " ,
102- genericError : "予期しないエラーが発生しました。 " ,
101+ onlyEpub : ".epubファイルのみ対応しています" ,
102+ genericError : "予期しないエラーが発生しました" ,
103103 convertAnother : "他の .epub ファイルをドラッグして変換" ,
104104 selectFile : "ファイルを選択" ,
105105 downloadTxt : "TXTをダウンロード" ,
@@ -116,8 +116,8 @@ document.addEventListener('DOMContentLoaded', () => {
116116 packaging : "正在打包 ZIP..." ,
117117 processingFile : ( current , total ) => `檔案 ${ current } /${ total } :` ,
118118 errorPrefix : "錯誤: " ,
119- onlyEpub : "請選擇 .epub 檔案。 " ,
120- genericError : "發生未預期的錯誤。 " ,
119+ onlyEpub : "僅支援 .epub 檔案" ,
120+ genericError : "發生未預期的錯誤" ,
121121 convertAnother : "拖放其他 .epub 檔案以轉換" ,
122122 selectFile : "選擇檔案" ,
123123 downloadTxt : "下載 TXT" ,
@@ -279,7 +279,13 @@ document.addEventListener('DOMContentLoaded', () => {
279279 // Handle missing files in spine gracefully
280280 let content ;
281281 try {
282- content = await zip . file ( path ) . async ( "string" ) ;
282+ const entry = zip . file ( path ) ;
283+ if ( ! entry ) {
284+ console . warn ( "Could not read file:" , path ) ;
285+ continue ;
286+ }
287+ const bytes = await entry . async ( "uint8array" ) ;
288+ content = decodeBytesToString ( bytes ) ;
283289 } catch ( e ) {
284290 console . warn ( "Could not read file:" , path ) ;
285291 continue ;
@@ -342,6 +348,44 @@ document.addEventListener('DOMContentLoaded', () => {
342348 return stack . join ( '/' ) ;
343349 }
344350
351+ function decodeBytesToString ( bytes ) {
352+ const encoding = sniffEncoding ( bytes ) || 'utf-8' ;
353+ try {
354+ return new TextDecoder ( encoding ) . decode ( bytes ) ;
355+ } catch ( e ) {
356+ return new TextDecoder ( 'utf-8' ) . decode ( bytes ) ;
357+ }
358+ }
359+
360+ function sniffEncoding ( bytes ) {
361+ if ( ! bytes || ! bytes . length ) return null ;
362+ const headerBytes = bytes . subarray ( 0 , 2048 ) ;
363+ let headerText = '' ;
364+ try {
365+ headerText = new TextDecoder ( 'utf-8' ) . decode ( headerBytes ) ;
366+ } catch ( e ) {
367+ return null ;
368+ }
369+
370+ const xmlMatch = headerText . match ( / < \? x m l [ ^ > ] * e n c o d i n g = [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / i) ;
371+ if ( xmlMatch ) return normalizeEncodingName ( xmlMatch [ 1 ] ) ;
372+
373+ const metaCharsetMatch = headerText . match ( / < m e t a [ ^ > ] * c h a r s e t = [ " ' ] ? \s * ( [ ^ " ' \s / > ] + ) / i) ;
374+ if ( metaCharsetMatch ) return normalizeEncodingName ( metaCharsetMatch [ 1 ] ) ;
375+
376+ const metaHttpEquivMatch = headerText . match ( / < m e t a [ ^ > ] * h t t p - e q u i v = [ " ' ] c o n t e n t - t y p e [ " ' ] [ ^ > ] * c o n t e n t = [ " ' ] [ ^ " ' ] * c h a r s e t = ( [ ^ " ' ] + ) [ " ' ] / i) ;
377+ if ( metaHttpEquivMatch ) return normalizeEncodingName ( metaHttpEquivMatch [ 1 ] ) ;
378+
379+ return null ;
380+ }
381+
382+ function normalizeEncodingName ( name ) {
383+ if ( ! name ) return null ;
384+ const cleaned = String ( name ) . trim ( ) . toLowerCase ( ) . replace ( / _ / g, '-' ) ;
385+ if ( cleaned === 'utf8' ) return 'utf-8' ;
386+ return cleaned ;
387+ }
388+
345389 function resolveZipPath ( opfDir , href ) {
346390 const cleaned = href . split ( '#' ) [ 0 ] ;
347391 if ( ! cleaned ) return null ;
@@ -610,7 +654,17 @@ document.addEventListener('DOMContentLoaded', () => {
610654 return combined ;
611655 }
612656
613- function collectTextSegments ( element , inPre = false , segments = [ ] , state = null ) {
657+ /**
658+ * Recursive function to traverse the DOM and collect text segments.
659+ * Mirrors the logic in the Python script's `get_clean_text`.
660+ *
661+ * @param {Node } element - The DOM node to traverse.
662+ * @param {boolean } inPre - Whether the current node is inside a <pre> tag.
663+ * @param {Array } segments - Accumulator for text segments.
664+ * @param {Object } state - Tracks state across recursion (e.g., hasContent, lastWasSeparator).
665+ * @param {number } listDepth - Current nesting level of lists (for indentation).
666+ */
667+ function collectTextSegments ( element , inPre = false , segments = [ ] , state = null , listDepth = 0 ) {
614668 if ( ! element ) return segments ;
615669 if ( ! state ) {
616670 state = { hasContent : false , lastWasSeparator : false } ;
@@ -645,6 +699,25 @@ document.addEventListener('DOMContentLoaded', () => {
645699 return ;
646700 }
647701
702+ // Handle Lists
703+ if ( tagName === 'UL' || tagName === 'OL' ) {
704+ if ( ! inPre ) pushSegment ( "\n" , false ) ;
705+ collectTextSegments ( node , inPre , segments , state , listDepth + 1 ) ;
706+ if ( ! inPre ) pushSegment ( "\n" , false ) ;
707+ return ;
708+ }
709+
710+ if ( tagName === 'LI' ) {
711+ if ( ! inPre ) {
712+ pushSegment ( "\n" , false ) ;
713+ const indent = " " . repeat ( Math . max ( 0 , listDepth - 1 ) ) ;
714+ pushSegment ( indent + "- " , true ) ;
715+ }
716+ collectTextSegments ( node , inPre , segments , state , listDepth ) ;
717+ if ( ! inPre ) pushSegment ( "\n" , false ) ;
718+ return ;
719+ }
720+
648721 const headingLevel = HEADING_TAGS [ tagName ] ;
649722 if ( headingLevel && ! inPre ) {
650723 const headingText = node . textContent . replace ( / \s + / g, ' ' ) . trim ( ) ;
@@ -664,7 +737,7 @@ document.addEventListener('DOMContentLoaded', () => {
664737 pushSegment ( "\n" , false ) ;
665738 }
666739
667- collectTextSegments ( node , nextPre , segments , state ) ;
740+ collectTextSegments ( node , nextPre , segments , state , listDepth ) ;
668741
669742 if ( isBlock && ! inPre ) {
670743 pushSegment ( "\n" , false ) ;
@@ -698,6 +771,10 @@ document.addEventListener('DOMContentLoaded', () => {
698771 return elements . length ? elements [ 0 ] : null ;
699772 }
700773
774+ /**
775+ * Handles the creation of a temporary Object URL for downloading.
776+ * Revokes any existing URL to prevent memory leaks before creating a new one.
777+ */
701778 function prepareBlobDownload ( blob , filename , downloadType ) {
702779 safeRevokeBlob ( ) ;
703780 currentBlobUrl = URL . createObjectURL ( blob ) ;
@@ -769,6 +846,10 @@ document.addEventListener('DOMContentLoaded', () => {
769846 }
770847 }
771848
849+ /**
850+ * Generates a unique filename by appending a counter if the name already exists.
851+ * e.g., "book.txt" -> "book (2).txt" -> "book (3).txt"
852+ */
772853 function makeUniqueFilename ( name , usedNames ) {
773854 if ( ! usedNames . has ( name ) ) return name ;
774855 const dotIndex = name . lastIndexOf ( '.' ) ;
0 commit comments