feat: add EPUB and MHTML document support

This commit is contained in:
FFFFFFpy
2026-06-16 10:34:58 +09:00
committed by lyingbug
parent fe7aded648
commit e21a5da422
20 changed files with 770 additions and 18 deletions
+332
View File
@@ -0,0 +1,332 @@
"""EPUB parser.
Parses EPUB files into markdown text and optional embedded images.
"""
import base64
import logging
import os
import posixpath
import tempfile
from urllib.parse import unquote
import uuid
from typing import Dict
from bs4 import BeautifulSoup
import ebooklib
from ebooklib import epub
from docreader.models.document import Document
from docreader.parser.base_parser import BaseParser
logger = logging.getLogger(__name__)
class EPUBParser(BaseParser):
"""Parser for EPUB e-book files."""
def __init__(self, *args, extract_images: bool = True, **kwargs):
super().__init__(*args, **kwargs)
self.extract_images = extract_images
def parse_into_text(self, content: bytes) -> Document:
logger.info(
"Parsing EPUB file: %s, size: %d bytes", self.file_name, len(content)
)
try:
with tempfile.NamedTemporaryFile(
suffix=".epub", delete=False, mode="wb"
) as epub_file:
epub_file.write(content)
epub_path = epub_file.name
try:
book = epub.read_epub(epub_path)
metadata = self._extract_metadata(book)
markdown_content, images = self._extract_content(book)
metadata["source_format"] = "epub"
metadata["file_size"] = len(content)
metadata["chapter_count"] = len(
[part for part in markdown_content.split("\n## ") if part.strip()]
)
metadata["image_count"] = len(images)
return Document(
content=markdown_content, images=images, metadata=metadata
)
finally:
if os.path.exists(epub_path):
os.unlink(epub_path)
except ImportError:
logger.error("ebooklib not installed")
raise
except Exception as e:
logger.warning(
"ebooklib failed to parse EPUB: %s, trying ZIP fallback", str(e)
)
return self._parse_epub_fallback(content)
def _parse_epub_fallback(self, content: bytes) -> Document:
"""Parse EPUB directly as a ZIP when ebooklib cannot read it."""
import re
import zipfile
from io import BytesIO
metadata = {"source_format": "epub", "file_size": len(content)}
images: Dict[str, str] = {}
image_aliases: Dict[str, str] = {}
markdown_parts = []
with zipfile.ZipFile(BytesIO(content), "r") as epub_zip:
html_files = [
f
for f in epub_zip.namelist()
if f.endswith((".html", ".xhtml", ".htm"))
]
def chapter_num(filename: str) -> int:
match = re.search(r"chapter(\d+)", filename, re.IGNORECASE)
return int(match.group(1)) if match else 999999
html_files.sort(key=chapter_num)
if self.extract_images:
img_exts = (".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg")
for img_file in epub_zip.namelist():
if img_file.lower().endswith(img_exts):
try:
img_data = epub_zip.read(img_file)
ext = os.path.splitext(img_file)[1]
img_path = f"images/{uuid.uuid4().hex}{ext}"
images[img_path] = base64.b64encode(img_data).decode(
"utf-8"
)
self._add_image_aliases(image_aliases, img_file, img_path)
except Exception as e:
logger.warning(
"Failed to extract image %s: %s", img_file, e
)
for html_file in html_files:
try:
html_content = epub_zip.read(html_file).decode(
"utf-8", errors="ignore"
)
chapter_md = self._html_to_markdown(
html_content,
image_aliases=image_aliases,
base_path=posixpath.dirname(html_file),
)
base = os.path.basename(html_file)
title = base.replace(".html", "").replace(".xhtml", "")
title = re.sub(
r"chapter[_-]?", "Chapter ", title, flags=re.IGNORECASE
)
title = title.replace("_", " ").replace("-", " ").title()
if chapter_md.strip():
markdown_parts.append(f"## {title}\n\n{chapter_md}")
except Exception as e:
logger.warning("Failed to process %s: %s", html_file, e)
metadata["chapter_count"] = len(markdown_parts)
metadata["image_count"] = len(images)
return Document(
content="\n\n".join(markdown_parts), images=images, metadata=metadata
)
def _extract_metadata(self, book) -> Dict[str, str]:
metadata: Dict[str, str] = {}
mapping = {
"title": "title",
"creator": "author",
"publisher": "publisher",
"language": "language",
"description": "description",
"date": "date",
"identifier": "isbn",
}
for dc_key, out_key in mapping.items():
try:
values = book.get_metadata("DC", dc_key)
except Exception:
values = None
if values:
if out_key == "author":
metadata[out_key] = ", ".join(value[0] for value in values)
else:
metadata[out_key] = values[0][0]
return metadata
def _extract_content(self, book) -> tuple[str, Dict[str, str]]:
markdown_parts = []
images: Dict[str, str] = {}
image_aliases: Dict[str, str] = {}
try:
toc = book.get_table_of_contents()
except Exception as e:
logger.debug("Failed to get TOC: %s, processing all HTML items", e)
toc = []
html_items = {}
for item in book.get_items():
if item.get_type() == ebooklib.ITEM_DOCUMENT:
html_items[item.get_name()] = item
if self.extract_images:
for item in book.get_items():
if item.get_type() == ebooklib.ITEM_IMAGE:
img_data = item.get_content()
ext = os.path.splitext(item.get_name())[1]
img_path = f"images/{uuid.uuid4().hex}{ext}"
images[img_path] = base64.b64encode(img_data).decode("utf-8")
self._add_image_aliases(image_aliases, item.get_name(), img_path)
if toc:
for item in toc:
entries = item if isinstance(item, tuple) else (item,)
for sub in entries:
if hasattr(sub, "get_name") and sub.get_name() in html_items:
markdown_parts.append(
self._process_chapter(
html_items[sub.get_name()],
toc_index=len(markdown_parts),
image_aliases=image_aliases,
)
)
if not markdown_parts:
for _name, item in html_items.items():
markdown_parts.append(
self._process_chapter(
item,
toc_index=len(markdown_parts),
image_aliases=image_aliases,
)
)
return "\n\n".join(part for part in markdown_parts if part.strip()), images
def _process_chapter(
self,
html_item,
toc_index: int = 0,
image_aliases: Dict[str, str] | None = None,
) -> str:
try:
html_content = html_item.get_content()
soup = BeautifulSoup(html_content, "lxml")
title_tag = soup.find(["h1", "h2"])
if title_tag:
chapter_title = title_tag.get_text().strip()
title_tag.decompose()
else:
chapter_title = html_item.get_name().replace("/", "")
chapter_title = chapter_title.replace(".xhtml", "")
chapter_title = chapter_title.replace("-", " ").title()
body_html = str(soup.body) if soup.body else str(html_content)
chapter_md = self._html_to_markdown(
body_html,
image_aliases=image_aliases,
base_path=posixpath.dirname(html_item.get_name()),
)
return f"## {chapter_title}\n\n{chapter_md}"
except Exception as e:
logger.error(
"Failed to process chapter %s: %s", html_item.get_name(), e
)
return f"## Chapter {toc_index + 1}\n\n[Error processing chapter: {e}]"
def _html_to_markdown(
self,
html_content: str,
image_aliases: Dict[str, str] | None = None,
base_path: str = "",
) -> str:
try:
from bs4 import Comment
from markdownify import markdownify as md
soup = BeautifulSoup(html_content, "lxml")
for element in soup(["script", "style"]):
element.decompose()
for comment in soup.find_all(
string=lambda text: isinstance(text, Comment)
):
comment.extract()
self._strip_internal_links(soup)
if image_aliases:
self._rewrite_image_sources(soup, image_aliases, base_path)
markdown_text = md(str(soup), heading_style="ATX")
return "\n".join(
line.strip() for line in markdown_text.split("\n") if line.strip()
)
except ImportError:
logger.warning("markdownify not available, using HTML as-is")
return f"```html\n{html_content}\n```"
except Exception as e:
logger.error("HTML to Markdown conversion failed: %s", e)
return f"```html\n{html_content}\n```"
@staticmethod
def _strip_internal_links(soup: BeautifulSoup) -> None:
"""Unwrap links that don't point to an external resource.
EPUB internal links (other chapter files, ``#fragment`` anchors, TOC
entries) become dead links after extraction. Keep only external links
and replace everything else with its text.
"""
external = ("http://", "https://", "mailto:", "tel:")
for link in soup.find_all("a"):
href = (link.get("href") or "").strip().lower()
if not href or not href.startswith(external):
link.unwrap()
@staticmethod
def _add_image_aliases(
image_aliases: Dict[str, str],
original_path: str,
image_path: str,
) -> None:
normalized = EPUBParser._normalize_epub_path(original_path)
aliases = {
original_path,
normalized,
unquote(original_path),
unquote(normalized),
posixpath.basename(normalized),
}
for alias in aliases:
if alias:
image_aliases[alias] = image_path
@staticmethod
def _rewrite_image_sources(
soup: BeautifulSoup,
image_aliases: Dict[str, str],
base_path: str = "",
) -> None:
for img in soup.find_all("img"):
src = (img.get("src") or "").strip()
if not src:
continue
normalized_src = EPUBParser._normalize_epub_path(src)
candidates = [
src,
normalized_src,
unquote(src),
unquote(normalized_src),
posixpath.basename(normalized_src),
]
if base_path:
joined = EPUBParser._normalize_epub_path(posixpath.join(base_path, src))
candidates.extend([joined, unquote(joined)])
for candidate in candidates:
if candidate in image_aliases:
img["src"] = image_aliases[candidate]
break
@staticmethod
def _normalize_epub_path(path: str) -> str:
path = unquote(path).split("#", 1)[0].split("?", 1)[0].replace("\\", "/")
normalized = posixpath.normpath(path)
return "" if normalized == "." else normalized.lstrip("/")
+222
View File
@@ -0,0 +1,222 @@
"""MHTML parser.
Parses MIME HTML web archives into markdown text and optional embedded images.
"""
import base64
import email
import html
import logging
import os
from urllib.parse import unquote, urljoin
import uuid
from typing import Dict
from bs4 import BeautifulSoup
from docreader.models.document import Document
from docreader.parser.base_parser import BaseParser
logger = logging.getLogger(__name__)
_AD_DOMAINS = (
"googleads",
"doubleclick",
"googlesyndication",
"facebook.com/tr",
"analytics",
"pixel",
)
class MHTMLParser(BaseParser):
"""Parser for MHTML web archives."""
def __init__(self, *args, extract_images: bool = True, **kwargs):
super().__init__(*args, **kwargs)
self.extract_images = extract_images
def parse_into_text(self, content: bytes) -> Document:
logger.info(
"Parsing MHTML file: %s, size: %d bytes", self.file_name, len(content)
)
msg = email.message_from_bytes(content)
html_parts = []
images: Dict[str, str] = {}
image_aliases: Dict[str, str] = {}
metadata: Dict[str, object] = {}
for part in msg.walk():
content_type = part.get_content_type()
location = part.get("Content-Location", "")
if content_type == "text/html":
payload = part.get_payload(decode=True)
if not payload:
continue
charset = part.get_content_charset() or "utf-8"
try:
html_text = payload.decode(charset, errors="ignore")
except LookupError:
html_text = payload.decode("utf-8", errors="ignore")
html_parts.append(
{
"content": html_text,
"location": location,
"size": len(html_text),
}
)
elif content_type.startswith("image/") and self.extract_images:
image_data = part.get_payload(decode=True)
if image_data:
ext = self._image_extension(content_type)
image_path = f"images/{uuid.uuid4().hex}{ext}"
images[image_path] = base64.b64encode(image_data).decode("utf-8")
self._add_image_aliases(image_aliases, part, image_path)
main_html = self._select_main_html(html_parts)
if not main_html:
logger.warning("No HTML content found in MHTML file")
return Document(
content="", images=images, metadata={"source_format": "mhtml"}
)
html_content = main_html["content"]
try:
markdown_text = self._html_to_markdown(
html_content,
image_aliases=image_aliases,
base_location=main_html.get("location", ""),
)
except Exception as e:
logger.error("Failed to convert HTML to Markdown: %s", e)
markdown_text = f"```html\n{html_content}\n```"
metadata["source_format"] = "mhtml"
metadata["file_size"] = len(content)
metadata["image_count"] = len(images)
return Document(content=markdown_text, images=images, metadata=metadata)
def _select_main_html(self, html_parts) -> dict:
"""Pick the largest non-ad HTML part as the main document body."""
if not html_parts:
return {}
def is_ad(location: str) -> bool:
if not location:
return False
loc = location.lower()
return any(ad in loc for ad in _AD_DOMAINS)
non_ad = sorted(
(part for part in html_parts if not is_ad(part.get("location", ""))),
key=lambda part: part["size"],
reverse=True,
)
if non_ad:
logger.info("Selected main HTML: %d bytes", non_ad[0]["size"])
return non_ad[0]
largest = max(html_parts, key=lambda part: part["size"])
logger.warning("Only ad content found, using largest: %d bytes", largest["size"])
return largest
@staticmethod
def _add_image_aliases(image_aliases: Dict[str, str], part, image_path: str) -> None:
"""Register the refs an MHTML document may use for an image part."""
for raw in (
part.get("Content-Location", ""),
part.get("Content-ID", ""),
part.get("X-Attachment-Id", ""),
):
raw = raw.strip()
if not raw:
continue
values = {raw, html.unescape(raw), unquote(html.unescape(raw))}
cid = raw.strip("<>")
if cid:
values.add(f"cid:{cid}")
values.add(f"cid:{unquote(cid)}")
for value in values:
if value:
image_aliases[value] = image_path
@staticmethod
def _image_extension(content_type: str) -> str:
return {
"image/png": ".png",
"image/jpeg": ".jpg",
"image/gif": ".gif",
"image/webp": ".webp",
"image/bmp": ".bmp",
"image/tiff": ".tiff",
"image/x-icon": ".ico",
}.get(content_type, ".png")
def _html_to_markdown(
self,
html_content: str,
image_aliases: Dict[str, str] | None = None,
base_location: str = "",
) -> str:
try:
from markdownify import markdownify as md
soup = BeautifulSoup(html_content, "lxml")
for tag in soup(["script", "style", "noscript", "iframe"]):
tag.decompose()
self._strip_internal_links(soup)
if image_aliases:
self._rewrite_image_sources(soup, image_aliases, base_location)
text_fallback = soup.get_text(separator="\n", strip=True)
markdown_text = md(str(soup), heading_style="ATX")
result = "\n".join(
line.strip() for line in markdown_text.split("\n") if line.strip()
)
if not result and text_fallback:
logger.warning("Markdown empty, falling back to text extraction")
return text_fallback
if not result:
return f"```html\n{html_content[:50000]}\n```"
return result
except ImportError:
logger.warning("markdownify not available, returning raw HTML")
return f"```html\n{html_content}\n```"
except Exception as e:
logger.error("HTML to Markdown conversion failed: %s", e)
return f"```html\n{html_content}\n```"
@staticmethod
def _strip_internal_links(soup: BeautifulSoup) -> None:
"""Unwrap links that don't point to an external resource."""
external = ("http://", "https://", "mailto:", "tel:")
for link in soup.find_all("a"):
href = (link.get("href") or "").strip().lower()
if not href or not href.startswith(external):
link.unwrap()
@staticmethod
def _rewrite_image_sources(
soup: BeautifulSoup,
image_aliases: Dict[str, str],
base_location: str = "",
) -> None:
for img in soup.find_all("img"):
src = (img.get("src") or "").strip()
if not src:
continue
candidates = [
src,
html.unescape(src),
unquote(html.unescape(src)),
]
if base_location:
candidates.append(urljoin(base_location, src))
base_name = os.path.basename(unquote(html.unescape(src)))
if base_name:
candidates.append(base_name)
for candidate in candidates:
if candidate in image_aliases:
img["src"] = image_aliases[candidate]
break
+4
View File
@@ -4,10 +4,12 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Type
from docreader.parser.base_parser import BaseParser
from docreader.parser.doc_parser import DocParser
from docreader.parser.docx2_parser import Docx2Parser
from docreader.parser.epub_parser import EPUBParser
from docreader.parser.excel_parser import ExcelParser
from docreader.parser.image_parser import ImageParser
from docreader.parser.markdown_parser import MarkdownParser
from docreader.parser.markitdown_parser import MarkitdownParser
from docreader.parser.mhtml_parser import MHTMLParser
from docreader.parser.opendataloader_parser import (
OpenDataLoaderParser,
opendataloader_available,
@@ -131,6 +133,8 @@ def _build_default_registry() -> ParserEngineRegistry:
"markdown": MarkdownParser,
"xlsx": ExcelParser,
"xls": ExcelParser,
"epub": EPUBParser,
"mhtml": MHTMLParser,
**_image_types,
},
description="内置解析引擎",
+2
View File
@@ -6,10 +6,12 @@ readme = "README.md"
requires-python = ">=3.10.18"
dependencies = [
"beautifulsoup4>=4.14.2",
"ebooklib>=0.18",
"grpcio>=1.78.0",
"grpcio-health-checking>=1.78.0",
"grpcio-tools>=1.78.0",
"lxml>=6.1.0",
"markdownify>=0.13.1",
"markitdown[docx,pdf,xls,xlsx]>=0.1.3",
"opendataloader-pdf>=2.4.7",
"openpyxl>=3.1.0",
+92
View File
@@ -0,0 +1,92 @@
import os
import tempfile
import unittest
from ebooklib import epub
from docreader.parser.epub_parser import EPUBParser
from docreader.parser.registry import registry
def _minimal_epub_bytes() -> bytes:
book = epub.EpubBook()
book.set_identifier("test-epub")
book.set_title("Tiny EPUB")
book.set_language("en")
book.add_author("WeKnora")
chapter = epub.EpubHtml(
title="Chapter One", file_name="text/chapter_01.xhtml", lang="en"
)
chapter.content = (
"<html><body><h1>Chapter One</h1>"
"<p>Hello EPUB world.</p>"
'<p><a href="chapter_02.xhtml#sec2">Chapter 2</a> '
'<a href="#footnote1">note</a> '
'<a href="https://example.com">the site</a></p>'
'<img alt="cover" src="../images/pic.png">'
"</body></html>"
)
book.add_item(chapter)
book.add_item(
epub.EpubItem(
uid="pic",
file_name="images/pic.png",
media_type="image/png",
content=b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR",
)
)
book.toc = (epub.Link("text/chapter_01.xhtml", "Chapter One", "chapter-one"),)
book.spine = ["nav", chapter]
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
with tempfile.NamedTemporaryFile(suffix=".epub", delete=False) as handle:
path = handle.name
try:
epub.write_epub(path, book)
with open(path, "rb") as handle:
return handle.read()
finally:
if os.path.exists(path):
os.unlink(path)
class EPUBParserTest(unittest.TestCase):
def test_parse_minimal_epub(self):
document = EPUBParser(
file_name="tiny.epub", file_type="epub"
).parse_into_text(_minimal_epub_bytes())
self.assertIn("Hello EPUB world", document.content)
self.assertEqual(document.metadata["source_format"], "epub")
self.assertEqual(len(document.images), 1)
image_ref = next(iter(document.images))
self.assertTrue(image_ref.startswith("images/"))
self.assertIn(image_ref, document.content)
self.assertNotIn("../images/pic.png", document.content)
def test_internal_links_are_unwrapped_but_external_links_remain(self):
document = EPUBParser(
file_name="tiny.epub", file_type="epub"
).parse_into_text(_minimal_epub_bytes())
self.assertIn("Chapter 2", document.content)
self.assertIn("note", document.content)
self.assertNotIn("chapter_02.xhtml#sec2", document.content)
self.assertNotIn("#footnote1", document.content)
self.assertIn("[the site](https://example.com)", document.content)
def test_parse_without_images(self):
document = EPUBParser(
file_name="tiny.epub", file_type="epub", extract_images=False
).parse_into_text(_minimal_epub_bytes())
self.assertEqual(document.images, {})
def test_registry_resolves_epub(self):
self.assertIs(registry.get_parser_class("", "epub"), EPUBParser)
if __name__ == "__main__":
unittest.main()
+92
View File
@@ -0,0 +1,92 @@
import unittest
from email.message import EmailMessage
from docreader.parser.mhtml_parser import MHTMLParser
from docreader.parser.registry import registry
def _minimal_mhtml_bytes() -> bytes:
root = EmailMessage()
root["Subject"] = "Tiny MHTML"
root.make_related()
main = EmailMessage()
main.set_content(
"<html><body><h1>Main Article</h1>"
"<p>Hello MHTML world.</p>"
'<p><a href="chapter03.xhtml#sec2">Chapter 3</a> '
'<a href="#footnote1">note</a> '
'<a href="https://example.com">the site</a></p>'
'<img alt="tiny" src="cid:tiny-image">'
"<script>window.noise = true</script>"
"</body></html>",
subtype="html",
)
main["Content-Location"] = "https://example.com/article"
root.attach(main)
ad = EmailMessage()
ad.set_content(
"<html><body><h1>Advertisement</h1>"
"<p>Buy this unrelated thing.</p></body></html>",
subtype="html",
)
ad["Content-Location"] = "https://googleads.example/frame.html"
root.attach(ad)
image = EmailMessage()
image.set_content(
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR",
maintype="image",
subtype="png",
)
image["Content-Location"] = "cid:tiny-image"
root.attach(image)
return root.as_bytes()
class MHTMLParserTest(unittest.TestCase):
def test_parse_selects_main_html_and_filters_noise(self):
document = MHTMLParser(
file_name="article.mhtml", file_type="mhtml"
).parse_into_text(_minimal_mhtml_bytes())
self.assertIn("Main Article", document.content)
self.assertIn("Hello MHTML world", document.content)
self.assertNotIn("Advertisement", document.content)
self.assertNotIn("window.noise", document.content)
self.assertEqual(document.metadata["source_format"], "mhtml")
def test_internal_links_are_unwrapped_but_external_links_remain(self):
document = MHTMLParser(
file_name="article.mhtml", file_type="mhtml"
).parse_into_text(_minimal_mhtml_bytes())
self.assertIn("Chapter 3", document.content)
self.assertIn("note", document.content)
self.assertNotIn("chapter03.xhtml#sec2", document.content)
self.assertNotIn("#footnote1", document.content)
self.assertIn("[the site](https://example.com)", document.content)
def test_image_extraction_toggle(self):
with_images = MHTMLParser(
file_name="article.mhtml", file_type="mhtml", extract_images=True
).parse_into_text(_minimal_mhtml_bytes())
without_images = MHTMLParser(
file_name="article.mhtml", file_type="mhtml", extract_images=False
).parse_into_text(_minimal_mhtml_bytes())
self.assertEqual(len(with_images.images), 1)
image_ref = next(iter(with_images.images))
self.assertTrue(image_ref.startswith("images/"))
self.assertIn(image_ref, with_images.content)
self.assertNotIn("cid:tiny-image", with_images.content)
self.assertEqual(without_images.images, {})
def test_registry_resolves_mhtml(self):
self.assertIs(registry.get_parser_class("", "mhtml"), MHTMLParser)
if __name__ == "__main__":
unittest.main()
+4
View File
@@ -460,10 +460,12 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "beautifulsoup4" },
{ name = "ebooklib" },
{ name = "grpcio" },
{ name = "grpcio-health-checking" },
{ name = "grpcio-tools" },
{ name = "lxml" },
{ name = "markdownify" },
{ name = "markitdown", extra = ["docx", "pdf", "xls", "xlsx"] },
{ name = "opendataloader-pdf" },
{ name = "openpyxl" },
@@ -484,10 +486,12 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "beautifulsoup4", specifier = ">=4.14.2" },
{ name = "ebooklib", specifier = ">=0.18" },
{ name = "grpcio", specifier = ">=1.78.0" },
{ name = "grpcio-health-checking", specifier = ">=1.78.0" },
{ name = "grpcio-tools", specifier = ">=1.78.0" },
{ name = "lxml", specifier = ">=6.1.0" },
{ name = "markdownify", specifier = ">=0.13.1" },
{ name = "markitdown", extras = ["docx", "pdf", "xls", "xlsx"], specifier = ">=0.1.3" },
{ name = "opendataloader-pdf", specifier = ">=2.4.7" },
{ name = "openpyxl", specifier = ">=3.1.0" },
+2 -1
View File
@@ -31,7 +31,7 @@ const fileInputRef = ref<HTMLInputElement>();
// Supported file types (matching backend)
const SUPPORTED_TYPES = [
// Documents
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.epub', '.mhtml',
// Text
'.txt', '.md', '.csv', '.json', '.xml', '.html',
// Audio
@@ -116,6 +116,7 @@ const getFileIcon = (fileName: string): string => {
if (['doc', 'docx'].includes(ext || '')) return 'file-word';
if (['xls', 'xlsx'].includes(ext || '')) return 'file-excel';
if (['ppt', 'pptx'].includes(ext || '')) return 'file-powerpoint';
if (['epub', 'mhtml'].includes(ext || '')) return 'file';
if (['txt', 'md'].includes(ext || '')) return 'file';
if (['mp3', 'wav', 'm4a', 'flac', 'ogg', 'aac'].includes(ext || '')) return 'sound';
return 'file';
+2
View File
@@ -4786,6 +4786,8 @@ export default {
fileTypeText: 'Plain Text',
fileTypeJson: 'JSON Files',
fileTypeImage: 'Images',
fileTypeEbook: 'E-books',
fileTypeWebArchive: 'Web Archives',
fileTypeAudiovisual: 'Audio',
engines: {
builtin: {
+2
View File
@@ -4817,6 +4817,8 @@ export default {
fileTypeText: '일반 텍스트',
fileTypeJson: 'JSON 파일',
fileTypeImage: '이미지',
fileTypeEbook: '전자책',
fileTypeWebArchive: '웹 아카이브',
fileTypeAudiovisual: '오디오',
engines: {
builtin: {
+2
View File
@@ -4317,6 +4317,8 @@ export default {
fileTypeText: 'Текстовые файлы',
fileTypeJson: 'Файлы JSON',
fileTypeImage: 'Изображения',
fileTypeEbook: 'Электронные книги',
fileTypeWebArchive: 'Веб-архивы',
fileTypeAudiovisual: 'Аудио',
engines: {
builtin: {
+2
View File
@@ -4780,6 +4780,8 @@ export default {
fileTypeText: "纯文本",
fileTypeJson: "JSON 文件",
fileTypeImage: "图片",
fileTypeEbook: "电子书",
fileTypeWebArchive: "网页存档",
fileTypeAudiovisual: "音频",
engines: {
builtin: {
+1 -1
View File
@@ -40,7 +40,7 @@ export function formatStringDate(date: any) {
year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second
);
}
const DEFAULT_VALID_TYPES = new Set(["pdf", "txt", "md", "docx", "doc", "pptx", "ppt", "jpg", "jpeg", "png", "csv", "xlsx", "xls", "mp3", "wav", "m4a", "flac", "ogg"]);
const DEFAULT_VALID_TYPES = new Set(["pdf", "txt", "md", "docx", "doc", "pptx", "ppt", "epub", "mhtml", "jpg", "jpeg", "png", "csv", "xlsx", "xls", "mp3", "wav", "m4a", "flac", "ogg"]);
/**
* Returns true when the file should be **rejected**.
@@ -436,6 +436,8 @@ const fileTypeOptions = computed(() => [
{ label: 'DOC', value: 'doc' },
{ label: 'PPTX', value: 'pptx' },
{ label: 'PPT', value: 'ppt' },
{ label: 'EPUB', value: 'epub' },
{ label: 'MHTML', value: 'mhtml' },
{ label: 'TXT', value: 'txt' },
{ label: 'MD', value: 'md' },
{ label: 'URL', value: 'url' },
@@ -162,6 +162,8 @@ const fileTypeGroups = computed(() => {
const officeExts = ['docx', 'doc'].filter(e => ft.has(e))
const pptExts = ['pptx', 'ppt'].filter(e => ft.has(e))
const excelExts = ['xlsx', 'xls'].filter(e => ft.has(e))
const ebookExts = ['epub'].filter(e => ft.has(e))
const webArchiveExts = ['mhtml'].filter(e => ft.has(e))
const csvExts = ['csv'].filter(e => ft.has(e))
const mdExts = ['md', 'markdown'].filter(e => ft.has(e))
const txtExts = ['txt'].filter(e => ft.has(e))
@@ -174,6 +176,8 @@ const fileTypeGroups = computed(() => {
if (officeExts.length) groups.push({ key: 'office', label: t('kbSettings.parser.fileTypeWord'), icon: 'file-word', extensions: officeExts })
if (pptExts.length) groups.push({ key: 'ppt', label: t('kbSettings.parser.fileTypePpt'), icon: 'file-powerpoint', extensions: pptExts })
if (excelExts.length) groups.push({ key: 'excel', label: t('kbSettings.parser.fileTypeExcel'), icon: 'file-excel', extensions: excelExts })
if (ebookExts.length) groups.push({ key: 'ebook', label: t('kbSettings.parser.fileTypeEbook'), icon: 'file', extensions: ebookExts })
if (webArchiveExts.length) groups.push({ key: 'webarchive', label: t('kbSettings.parser.fileTypeWebArchive'), icon: 'file', extensions: webArchiveExts })
if (csvExts.length) groups.push({ key: 'csv', label: t('kbSettings.parser.fileTypeCsv'), icon: 'file-excel', extensions: csvExts })
if (mdExts.length) groups.push({ key: 'markdown', label: 'Markdown', icon: 'file-code', extensions: mdExts })
if (txtExts.length) groups.push({ key: 'text', label: t('kbSettings.parser.fileTypeText'), icon: 'file', extensions: txtExts })
+2 -2
View File
@@ -5,7 +5,7 @@
<RouterView />
</div>
<div class="upload-mask" v-show="ismask">
<input type="file" style="display: none" ref="uploadInput" accept=".pdf,.docx,.doc,.pptx,.ppt,.txt,.md,.jpg,.jpeg,.png,.csv,.xls,.xlsx" />
<input type="file" style="display: none" ref="uploadInput" accept=".pdf,.docx,.doc,.pptx,.ppt,.epub,.mhtml,.txt,.md,.jpg,.jpeg,.png,.csv,.xls,.xlsx" />
<UploadMask></UploadMask>
</div>
<!-- 全局设置模态框,供所有 platform 子路由使用 -->
@@ -279,4 +279,4 @@ img {
-o-user-drag: none;
user-drag: none;
}
</style>
</style>
-11
View File
@@ -112,17 +112,6 @@ export default defineConfig({
if (id.includes('highlight.js')) {
return 'vendor-highlight'
}
if (id.includes('tdesign-vue-next')) {
return 'vendor-tdesign'
}
if (
id.includes('/vue/') ||
id.includes('/vue-router/') ||
id.includes('/pinia/') ||
id.includes('/vue-i18n/')
) {
return 'vendor-vue'
}
},
},
},
@@ -21,7 +21,7 @@ import (
// isValidFileType checks if a file type is supported
func isValidFileType(filename string) bool {
switch strings.ToLower(getFileType(filename)) {
case "pdf", "txt", "docx", "doc", "md", "markdown", "png", "jpg", "jpeg", "gif", "csv", "xlsx", "xls", "pptx", "ppt", "json",
case "pdf", "txt", "docx", "doc", "epub", "mhtml", "md", "markdown", "png", "jpg", "jpeg", "gif", "csv", "xlsx", "xls", "pptx", "ppt", "json",
"mp3", "wav", "m4a", "flac", "ogg":
return true
default:
@@ -288,7 +288,7 @@ func isValidFileType(fileName string) bool {
supportedTypes := []string{
// documents
"docx", "doc", "pdf", "ppt", "pptx",
"docx", "doc", "pdf", "ppt", "pptx", "epub", "mhtml",
// spreadsheets
"xlsx", "xls",
// text / markup
@@ -45,7 +45,7 @@ func (e *builtinEngine) Description() string {
return "DocReader built-in parser engine"
}
func (e *builtinEngine) FileTypes(_ bool) []string {
return []string{"docx", "doc", "pdf", "md", "markdown", "xlsx", "xls", "jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp", "mp3", "wav", "m4a", "flac", "ogg"}
return []string{"docx", "doc", "pdf", "md", "markdown", "xlsx", "xls", "epub", "mhtml", "jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp", "mp3", "wav", "m4a", "flac", "ogg"}
}
func (e *builtinEngine) CheckAvailable(docreaderConnected bool, _ map[string]string) (bool, string) {
if docreaderConnected {