release: v0.1.11 - BabelDOC 翻譯引擎與穩定性改進
## 新增功能 - BabelDOC PDF 翻譯引擎:支援 PDF/Markdown/HTML 輸出,15 種目標語言 - Dockerfile 更新:pipx 安裝 babeldoc,warmup 預載資源 ## 修復 - 修正 user.tsx XSS 安全警告(label 包裹 safe span) ## 品質保證 - 173 個測試全數通過 - TypeScript / ESLint / Prettier / Knip 檢查通過
This commit is contained in:
parent
6d4ed58637
commit
4ca92e3a22
5 changed files with 852 additions and 19 deletions
34
Dockerfile
34
Dockerfile
|
|
@ -234,9 +234,15 @@ RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||||
# 階段 12:安裝 Python 工具(pipx)+ huggingface_hub(用於模型下載)
|
# 階段 12:安裝 Python 工具(pipx)+ huggingface_hub(用於模型下載)
|
||||||
# 注意:Debian bookworm 使用 PEP 668,需要 --break-system-packages 來安裝系統級套件
|
# 注意:Debian bookworm 使用 PEP 668,需要 --break-system-packages 來安裝系統級套件
|
||||||
RUN pipx install "markitdown[all]" \
|
RUN pipx install "markitdown[all]" \
|
||||||
&& pipx install "pdf2zh" \
|
|
||||||
&& pip3 install --no-cache-dir --break-system-packages huggingface_hub
|
&& pip3 install --no-cache-dir --break-system-packages huggingface_hub
|
||||||
|
|
||||||
|
# 階段 12-A:安裝 pdf2zh(PDFMathTranslate 引擎)
|
||||||
|
RUN pipx install "pdf2zh"
|
||||||
|
|
||||||
|
# 階段 12-B:安裝 babeldoc(BabelDOC 引擎)
|
||||||
|
# BabelDOC 是一個 PDF 翻譯工具,與 pdf2zh 類似但使用不同的翻譯方式
|
||||||
|
RUN pipx install "babeldoc" || echo "⚠️ babeldoc 安裝失敗,跳過..."
|
||||||
|
|
||||||
# 階段 13:安裝 mineru(可能在 arm64 上有問題,加入錯誤處理)
|
# 階段 13:安裝 mineru(可能在 arm64 上有問題,加入錯誤處理)
|
||||||
RUN pipx install "mineru[all]" || echo "⚠️ mineru 安裝失敗(可能是 arm64 相容性問題),跳過..."
|
RUN pipx install "mineru[all]" || echo "⚠️ mineru 安裝失敗(可能是 arm64 相容性問題),跳過..."
|
||||||
|
|
||||||
|
|
@ -294,11 +300,16 @@ RUN mkdir -p /models/pdfmathtranslate && \
|
||||||
# 階段 14-B:BabelDOC Warmup(預載入所有資源)
|
# 階段 14-B:BabelDOC Warmup(預載入所有資源)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# 說明:babeldoc --warmup 會下載所有必要的字型和模型資源
|
# 說明:babeldoc --warmup 會下載所有必要的字型和模型資源
|
||||||
# 這確保 pdf2zh 執行時不會有任何隱式下載
|
# 這確保 BabelDOC 執行時不會有任何隱式下載
|
||||||
|
# 注意:分開執行以避免記憶體壓力
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
RUN echo "📥 [2/6] 執行 BabelDOC warmup..." && \
|
RUN echo "📥 [2/6] 執行 BabelDOC warmup..." && \
|
||||||
babeldoc --warmup 2>&1 || echo "⚠️ BabelDOC warmup 可能已完成或無需 warmup" && \
|
if command -v babeldoc >/dev/null 2>&1; then \
|
||||||
echo "✅ BabelDOC warmup 完成"
|
babeldoc --warmup 2>&1 || echo "⚠️ BabelDOC warmup 失敗或無需 warmup"; \
|
||||||
|
else \
|
||||||
|
echo "⚠️ babeldoc 命令不存在,跳過 warmup"; \
|
||||||
|
fi && \
|
||||||
|
echo "✅ BabelDOC warmup 步驟完成"
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# 階段 14-C:PDFMathTranslate 字型下載
|
# 階段 14-C:PDFMathTranslate 字型下載
|
||||||
|
|
@ -374,6 +385,14 @@ RUN echo "📥 [6/6] 驗證模型並清理快取..." && \
|
||||||
echo "🔹 PDFMathTranslate 字型:" && \
|
echo "🔹 PDFMathTranslate 字型:" && \
|
||||||
ls -lh /app/*.ttf 2>/dev/null || echo " ⚠️ 無字型檔案" && \
|
ls -lh /app/*.ttf 2>/dev/null || echo " ⚠️ 無字型檔案" && \
|
||||||
echo "" && \
|
echo "" && \
|
||||||
|
echo "🔹 BabelDOC 快取:" && \
|
||||||
|
if [ -d "/root/.cache/babeldoc" ]; then \
|
||||||
|
echo " ✅ BabelDOC 快取目錄存在"; \
|
||||||
|
du -sh /root/.cache/babeldoc 2>/dev/null || true; \
|
||||||
|
else \
|
||||||
|
echo " ⚠️ BabelDOC 快取目錄不存在(可能需要 runtime 下載)"; \
|
||||||
|
fi && \
|
||||||
|
echo "" && \
|
||||||
echo "🔹 MinerU 模型目錄:" && \
|
echo "🔹 MinerU 模型目錄:" && \
|
||||||
if [ -d "/root/.cache/huggingface/hub/PDF-Extract-Kit-1.0" ]; then \
|
if [ -d "/root/.cache/huggingface/hub/PDF-Extract-Kit-1.0" ]; then \
|
||||||
echo " ✅ MinerU Pipeline 模型目錄存在"; \
|
echo " ✅ MinerU Pipeline 模型目錄存在"; \
|
||||||
|
|
@ -381,9 +400,6 @@ RUN echo "📥 [6/6] 驗證模型並清理快取..." && \
|
||||||
else \
|
else \
|
||||||
echo " ⚠️ MinerU Pipeline 模型目錄不存在(可能需要 runtime 下載)"; \
|
echo " ⚠️ MinerU Pipeline 模型目錄不存在(可能需要 runtime 下載)"; \
|
||||||
fi && \
|
fi && \
|
||||||
echo "" && \
|
|
||||||
echo "🔹 BabelDOC 快取:" && \
|
|
||||||
ls -la /root/.cache/babeldoc 2>/dev/null || echo " 快取位置可能不同" && \
|
|
||||||
echo "========================================" && \
|
echo "========================================" && \
|
||||||
# 清理 pip 快取(保留模型)
|
# 清理 pip 快取(保留模型)
|
||||||
rm -rf /root/.cache/pip && \
|
rm -rf /root/.cache/pip && \
|
||||||
|
|
@ -393,6 +409,10 @@ RUN echo "📥 [6/6] 驗證模型並清理快取..." && \
|
||||||
ENV PDFMATHTRANSLATE_MODELS_PATH="/models/pdfmathtranslate"
|
ENV PDFMATHTRANSLATE_MODELS_PATH="/models/pdfmathtranslate"
|
||||||
ENV NOTO_FONT_PATH="/app/GoNotoKurrent-Regular.ttf"
|
ENV NOTO_FONT_PATH="/app/GoNotoKurrent-Regular.ttf"
|
||||||
|
|
||||||
|
# BabelDOC 環境變數
|
||||||
|
ENV BABELDOC_CACHE_PATH="/root/.cache/babeldoc"
|
||||||
|
ENV BABELDOC_SERVICE="google"
|
||||||
|
|
||||||
# MinerU 環境變數(強制使用本地模型)
|
# MinerU 環境變數(強制使用本地模型)
|
||||||
ENV MINERU_MODEL_SOURCE="local"
|
ENV MINERU_MODEL_SOURCE="local"
|
||||||
ENV HF_HUB_OFFLINE="1"
|
ENV HF_HUB_OFFLINE="1"
|
||||||
|
|
|
||||||
352
src/converters/babeldoc.ts
Normal file
352
src/converters/babeldoc.ts
Normal file
|
|
@ -0,0 +1,352 @@
|
||||||
|
import { execFile as execFileOriginal } from "node:child_process";
|
||||||
|
import {
|
||||||
|
mkdirSync,
|
||||||
|
existsSync,
|
||||||
|
readdirSync,
|
||||||
|
unlinkSync,
|
||||||
|
rmdirSync,
|
||||||
|
copyFileSync,
|
||||||
|
statSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { join, basename, dirname } from "node:path";
|
||||||
|
import { ExecFileFn } from "./types";
|
||||||
|
import { getArchiveFileName } from "../transfer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BabelDOC Content Engine
|
||||||
|
*
|
||||||
|
* 用於翻譯 PDF 文件的引擎,基於 BabelDOC CLI 工具。
|
||||||
|
* 與 PDFMathTranslate 類似,但使用 babeldoc 命令進行翻譯。
|
||||||
|
*
|
||||||
|
* 所有輸出一律打包為 .tar 檔案,包含:
|
||||||
|
* - translated-<lang>.pdf(翻譯後的 PDF)
|
||||||
|
* - 其他 BabelDOC 產生的輔助檔案
|
||||||
|
*
|
||||||
|
* 必須在 Docker build 階段預先下載所需資源(--warmup),
|
||||||
|
* 不允許在 runtime 隱式下載。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 支援的目標語言列表(與 PDFMathTranslate 保持一致)
|
||||||
|
const SUPPORTED_LANGUAGES = [
|
||||||
|
"en", // English
|
||||||
|
"zh", // Chinese (Simplified)
|
||||||
|
"zh-TW", // Chinese (Traditional)
|
||||||
|
"ja", // Japanese
|
||||||
|
"ko", // Korean
|
||||||
|
"de", // German
|
||||||
|
"fr", // French
|
||||||
|
"es", // Spanish
|
||||||
|
"it", // Italian
|
||||||
|
"pt", // Portuguese
|
||||||
|
"ru", // Russian
|
||||||
|
"ar", // Arabic
|
||||||
|
"hi", // Hindi
|
||||||
|
"vi", // Vietnamese
|
||||||
|
"th", // Thai
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// 支援的輸出格式
|
||||||
|
const SUPPORTED_OUTPUT_FORMATS = ["pdf", "md", "html"] as const;
|
||||||
|
type OutputFormat = (typeof SUPPORTED_OUTPUT_FORMATS)[number];
|
||||||
|
|
||||||
|
// BabelDOC 快取路徑(Docker 環境中)
|
||||||
|
const BABELDOC_CACHE_PATH = process.env.BABELDOC_CACHE_PATH || "/root/.cache/babeldoc";
|
||||||
|
|
||||||
|
// 生成 from/to 格式映射
|
||||||
|
function generateLanguageMappings(): {
|
||||||
|
from: Record<string, string[]>;
|
||||||
|
to: Record<string, string[]>;
|
||||||
|
} {
|
||||||
|
// BabelDOC 輸出格式:
|
||||||
|
// - pdf-<lang>: 輸出 PDF
|
||||||
|
// - md-<lang>: 輸出 Markdown
|
||||||
|
// - html-<lang>: 輸出 HTML
|
||||||
|
const outputFormats: string[] = [];
|
||||||
|
|
||||||
|
for (const format of SUPPORTED_OUTPUT_FORMATS) {
|
||||||
|
for (const lang of SUPPORTED_LANGUAGES) {
|
||||||
|
outputFormats.push(`${format}-${lang}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: {
|
||||||
|
document: ["pdf"],
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
document: outputFormats,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const properties = {
|
||||||
|
...generateLanguageMappings(),
|
||||||
|
outputMode: "archive" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 從 convertTo 格式中提取目標語言和輸出格式
|
||||||
|
* @param convertTo 格式如 "pdf-zh"、"md-en"、"html-ja"
|
||||||
|
* @returns { lang: 目標語言代碼, format: 輸出格式 }
|
||||||
|
*/
|
||||||
|
function extractTargetInfo(convertTo: string): { lang: string; format: OutputFormat } {
|
||||||
|
// convertTo 格式: <format>-<lang>
|
||||||
|
// 例如: pdf-zh, md-en, html-ja
|
||||||
|
const match = convertTo.match(/^(pdf|md|html)-(.+)$/);
|
||||||
|
if (!match || !match[1] || !match[2]) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid convertTo format: ${convertTo}. Expected <format>-<lang> (format: pdf/md/html)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
format: match[1] as OutputFormat,
|
||||||
|
lang: match[2],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 檢查 BabelDOC 資源是否已預先下載
|
||||||
|
* @returns 資源是否存在
|
||||||
|
*/
|
||||||
|
function checkResourcesExist(): boolean {
|
||||||
|
if (!existsSync(BABELDOC_CACHE_PATH)) {
|
||||||
|
console.warn(`[BabelDOC] Cache directory not found: ${BABELDOC_CACHE_PATH}`);
|
||||||
|
console.warn(`[BabelDOC] Resources should be pre-downloaded via --warmup during Docker build.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a .tar archive from a directory (no compression)
|
||||||
|
*
|
||||||
|
* ⚠️ 重要:僅使用 .tar 格式,禁止 .tar.gz / .tgz / .zip
|
||||||
|
*/
|
||||||
|
function createTarArchive(
|
||||||
|
sourceDir: string,
|
||||||
|
outputTar: string,
|
||||||
|
execFile: ExecFileFn,
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Use tar command to create archive (without gzip compression)
|
||||||
|
// tar -cf <output.tar> -C <sourceDir> .
|
||||||
|
// 注意:使用 -cf 而非 -czf,避免 gzip 壓縮
|
||||||
|
execFile("tar", ["-cf", outputTar, "-C", sourceDir, "."], (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(`tar error: ${error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stdout) {
|
||||||
|
console.log(`tar stdout: ${stdout}`);
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`tar stderr: ${stderr}`);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to remove a directory recursively
|
||||||
|
*/
|
||||||
|
function removeDir(dirPath: string): void {
|
||||||
|
if (existsSync(dirPath)) {
|
||||||
|
const files = readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = join(dirPath, file.name);
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
removeDir(filePath);
|
||||||
|
} else {
|
||||||
|
unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rmdirSync(dirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BabelDOC 語言代碼轉換
|
||||||
|
* BabelDOC 可能使用不同的語言代碼格式
|
||||||
|
*/
|
||||||
|
function toBabelDocLang(lang: string): string {
|
||||||
|
// BabelDOC 語言代碼映射
|
||||||
|
const langMap: Record<string, string> = {
|
||||||
|
"zh-TW": "zh-Hant",
|
||||||
|
zh: "zh-Hans",
|
||||||
|
};
|
||||||
|
return langMap[lang] || lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取得 BabelDOC 輸出格式對應的副檔名
|
||||||
|
*/
|
||||||
|
function getOutputExtension(format: OutputFormat): string {
|
||||||
|
const extMap: Record<OutputFormat, string> = {
|
||||||
|
pdf: "pdf",
|
||||||
|
md: "md",
|
||||||
|
html: "html",
|
||||||
|
};
|
||||||
|
return extMap[format];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 執行 babeldoc 命令進行 PDF 翻譯
|
||||||
|
*
|
||||||
|
* @param inputPath 輸入 PDF 路徑
|
||||||
|
* @param outputPath 輸出檔案路徑
|
||||||
|
* @param targetLang 目標語言
|
||||||
|
* @param outputFormat 輸出格式(pdf/md/html)
|
||||||
|
* @param execFile 執行函數
|
||||||
|
*/
|
||||||
|
function runBabelDoc(
|
||||||
|
inputPath: string,
|
||||||
|
outputPath: string,
|
||||||
|
targetLang: string,
|
||||||
|
outputFormat: OutputFormat,
|
||||||
|
execFile: ExecFileFn,
|
||||||
|
): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// babeldoc CLI 參數:
|
||||||
|
// -i <input>: 輸入 PDF
|
||||||
|
// -o <output>: 輸出檔案
|
||||||
|
// -l <lang>: 目標語言
|
||||||
|
// --output-format <format>: 輸出格式(pdf/md/html)
|
||||||
|
// --service <service>: 翻譯服務(預設 google)
|
||||||
|
|
||||||
|
const babelLang = toBabelDocLang(targetLang);
|
||||||
|
const service = process.env.BABELDOC_SERVICE || "google";
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
"-i",
|
||||||
|
inputPath,
|
||||||
|
"-o",
|
||||||
|
outputPath,
|
||||||
|
"-l",
|
||||||
|
babelLang,
|
||||||
|
"--output-format",
|
||||||
|
outputFormat,
|
||||||
|
"--service",
|
||||||
|
service,
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(`[BabelDOC] Running: babeldoc ${args.join(" ")}`);
|
||||||
|
|
||||||
|
execFile("babeldoc", args, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(`babeldoc error: ${error}\nstderr: ${stderr}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
console.log(`[BabelDOC] stdout: ${stdout}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.log(`[BabelDOC] stderr: ${stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查輸出檔案是否存在
|
||||||
|
if (!existsSync(outputPath)) {
|
||||||
|
reject(`BabelDOC output file not found: ${outputPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(outputPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主要轉換函數
|
||||||
|
*
|
||||||
|
* @param filePath 輸入 PDF 檔案路徑
|
||||||
|
* @param fileType 檔案類型(應為 "pdf")
|
||||||
|
* @param convertTo 目標格式(如 "pdf-babel-zh"、"md-babel-en"、"html-babel-ja")
|
||||||
|
* @param targetPath 輸出路徑
|
||||||
|
* @param _options 額外選項
|
||||||
|
* @param execFile 執行函數覆寫
|
||||||
|
*/
|
||||||
|
export async function convert(
|
||||||
|
filePath: string,
|
||||||
|
fileType: string,
|
||||||
|
convertTo: string,
|
||||||
|
targetPath: string,
|
||||||
|
_options?: unknown,
|
||||||
|
execFile: ExecFileFn = execFileOriginal,
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
// 1. 檢查資源(警告但不阻止)
|
||||||
|
checkResourcesExist();
|
||||||
|
|
||||||
|
// 2. 提取目標語言和輸出格式
|
||||||
|
const { lang: targetLang, format: outputFormat } = extractTargetInfo(convertTo);
|
||||||
|
const outputExt = getOutputExtension(outputFormat);
|
||||||
|
console.log(`[BabelDOC] Translating to: ${targetLang}, format: ${outputFormat}`);
|
||||||
|
|
||||||
|
// 3. 建立臨時輸出目錄
|
||||||
|
const outputDir = dirname(targetPath);
|
||||||
|
const inputFileName = basename(filePath, `.${fileType}`);
|
||||||
|
const tempDir = join(outputDir, `${inputFileName}_babeldoc_${Date.now()}`);
|
||||||
|
|
||||||
|
if (!existsSync(tempDir)) {
|
||||||
|
mkdirSync(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 建立封裝用目錄
|
||||||
|
const archiveDir = join(tempDir, "archive");
|
||||||
|
mkdirSync(archiveDir, { recursive: true });
|
||||||
|
|
||||||
|
// 5. 設定 BabelDOC 輸出路徑(依輸出格式決定副檔名)
|
||||||
|
const translatedFilePath = join(tempDir, `${inputFileName}-translated.${outputExt}`);
|
||||||
|
|
||||||
|
// 6. 執行 babeldoc 翻譯
|
||||||
|
await runBabelDoc(filePath, translatedFilePath, targetLang, outputFormat, execFile);
|
||||||
|
|
||||||
|
// 7. 複製翻譯後的檔案到封裝目錄
|
||||||
|
const translatedDest = join(archiveDir, `translated-${targetLang}.${outputExt}`);
|
||||||
|
copyFileSync(translatedFilePath, translatedDest);
|
||||||
|
console.log(`[BabelDOC] Copied translated ${outputFormat.toUpperCase()} to archive`);
|
||||||
|
|
||||||
|
// 8. 檢查是否有其他 BabelDOC 產生的輔助檔案
|
||||||
|
const translatedBaseName = `${inputFileName}-translated.${outputExt}`;
|
||||||
|
const tempFiles = readdirSync(tempDir);
|
||||||
|
for (const file of tempFiles) {
|
||||||
|
const fileTempPath = join(tempDir, file);
|
||||||
|
// 跳過 archive 目錄和已處理的主檔案
|
||||||
|
if (file === "archive" || file === translatedBaseName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 複製其他產生的檔案(如 debug 輸出、中間結果等)
|
||||||
|
const destPath = join(archiveDir, file);
|
||||||
|
if (existsSync(fileTempPath) && !existsSync(destPath)) {
|
||||||
|
try {
|
||||||
|
const stats = statSync(fileTempPath);
|
||||||
|
if (stats.isFile()) {
|
||||||
|
copyFileSync(fileTempPath, destPath);
|
||||||
|
console.log(`[BabelDOC] Copied auxiliary file: ${file}`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略複製失敗的輔助檔案
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 建立 .tar 封裝
|
||||||
|
const tarPath = getArchiveFileName(targetPath);
|
||||||
|
const tarDir = dirname(tarPath);
|
||||||
|
if (!existsSync(tarDir)) {
|
||||||
|
mkdirSync(tarDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await createTarArchive(archiveDir, tarPath, execFile);
|
||||||
|
console.log(`[BabelDOC] Created archive: ${tarPath}`);
|
||||||
|
|
||||||
|
// 10. 清理臨時目錄
|
||||||
|
removeDir(tempDir);
|
||||||
|
|
||||||
|
return "Done";
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`BabelDOC error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ import {
|
||||||
convert as convertPDFMathTranslate,
|
convert as convertPDFMathTranslate,
|
||||||
properties as propertiesPDFMathTranslate,
|
properties as propertiesPDFMathTranslate,
|
||||||
} from "./pdfmathtranslate";
|
} from "./pdfmathtranslate";
|
||||||
|
import { convert as convertBabelDoc, properties as propertiesBabelDoc } from "./babeldoc";
|
||||||
|
|
||||||
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
|
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
|
||||||
|
|
||||||
|
|
@ -151,6 +152,10 @@ const properties: Record<
|
||||||
properties: propertiesPDFMathTranslate,
|
properties: propertiesPDFMathTranslate,
|
||||||
converter: convertPDFMathTranslate,
|
converter: convertPDFMathTranslate,
|
||||||
},
|
},
|
||||||
|
BabelDOC: {
|
||||||
|
properties: propertiesBabelDoc,
|
||||||
|
converter: convertBabelDoc,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function chunks<T>(arr: T[], size: number): T[][] {
|
function chunks<T>(arr: T[], size: number): T[][] {
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,8 @@ export const user = new Elysia()
|
||||||
</header>
|
</header>
|
||||||
<form method="post" action={`${WEBROOT}/register`} class="p-4">
|
<form method="post" action={`${WEBROOT}/register`} class="p-4">
|
||||||
<fieldset class="mb-4 flex flex-col gap-4">
|
<fieldset class="mb-4 flex flex-col gap-4">
|
||||||
<label class="flex flex-col gap-1" safe>
|
<label class="flex flex-col gap-1">
|
||||||
{t("auth", "email")}
|
<span safe>{t("auth", "email")}</span>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
|
|
@ -135,8 +135,8 @@ export const user = new Elysia()
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1" safe>
|
<label class="flex flex-col gap-1">
|
||||||
{t("auth", "password")}
|
<span safe>{t("auth", "password")}</span>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
|
@ -191,8 +191,8 @@ export const user = new Elysia()
|
||||||
<article class="article">
|
<article class="article">
|
||||||
<form method="post" class="flex flex-col gap-4">
|
<form method="post" class="flex flex-col gap-4">
|
||||||
<fieldset class="mb-4 flex flex-col gap-4">
|
<fieldset class="mb-4 flex flex-col gap-4">
|
||||||
<label class="flex flex-col gap-1" safe>
|
<label class="flex flex-col gap-1">
|
||||||
{t("auth", "email")}
|
<span safe>{t("auth", "email")}</span>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
|
|
@ -202,8 +202,8 @@ export const user = new Elysia()
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1" safe>
|
<label class="flex flex-col gap-1">
|
||||||
{t("auth", "password")}
|
<span safe>{t("auth", "password")}</span>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
|
@ -313,8 +313,8 @@ export const user = new Elysia()
|
||||||
<article class="article">
|
<article class="article">
|
||||||
<form method="post" class="flex flex-col gap-4">
|
<form method="post" class="flex flex-col gap-4">
|
||||||
<fieldset class="mb-4 flex flex-col gap-4">
|
<fieldset class="mb-4 flex flex-col gap-4">
|
||||||
<label class="flex flex-col gap-1" safe>
|
<label class="flex flex-col gap-1">
|
||||||
{t("auth", "email")}
|
<span safe>{t("auth", "email")}</span>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
|
|
@ -324,8 +324,8 @@ export const user = new Elysia()
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex flex-col gap-1" safe>
|
<label class="flex flex-col gap-1">
|
||||||
{t("auth", "password")}
|
<span safe>{t("auth", "password")}</span>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
|
|
||||||
456
tests/converters/babeldoc.test.ts
Normal file
456
tests/converters/babeldoc.test.ts
Normal file
|
|
@ -0,0 +1,456 @@
|
||||||
|
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
||||||
|
import { convert, properties } from "../../src/converters/babeldoc";
|
||||||
|
import type { ExecFileException } from "node:child_process";
|
||||||
|
import { ExecFileFn } from "../../src/converters/types";
|
||||||
|
import { mkdirSync, existsSync, writeFileSync, rmSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
|
||||||
|
// Skip common tests as BabelDOC has different behavior (archive output)
|
||||||
|
test.skip("dummy - required to trigger test detection", () => {});
|
||||||
|
|
||||||
|
describe("BabelDOC converter properties", () => {
|
||||||
|
test("should have correct input formats", () => {
|
||||||
|
expect(properties.from.document).toContain("pdf");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should have correct output formats for PDF with multiple languages", () => {
|
||||||
|
expect(properties.to.document).toContain("pdf-en");
|
||||||
|
expect(properties.to.document).toContain("pdf-zh");
|
||||||
|
expect(properties.to.document).toContain("pdf-zh-TW");
|
||||||
|
expect(properties.to.document).toContain("pdf-ja");
|
||||||
|
expect(properties.to.document).toContain("pdf-ko");
|
||||||
|
expect(properties.to.document).toContain("pdf-de");
|
||||||
|
expect(properties.to.document).toContain("pdf-fr");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should have correct output formats for Markdown with multiple languages", () => {
|
||||||
|
expect(properties.to.document).toContain("md-en");
|
||||||
|
expect(properties.to.document).toContain("md-zh");
|
||||||
|
expect(properties.to.document).toContain("md-ja");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should have correct output formats for HTML with multiple languages", () => {
|
||||||
|
expect(properties.to.document).toContain("html-en");
|
||||||
|
expect(properties.to.document).toContain("html-zh");
|
||||||
|
expect(properties.to.document).toContain("html-ja");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should have archive output mode", () => {
|
||||||
|
expect(properties.outputMode).toBe("archive");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("BabelDOC converter - Chinese translation", () => {
|
||||||
|
const testDir = "./test-output-babeldoc-zh";
|
||||||
|
const testInputFile = join(testDir, "input.pdf");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
// Create a dummy PDF file for testing
|
||||||
|
writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should call babeldoc with correct language argument for zh", async () => {
|
||||||
|
let babeldocArgs: string[] = [];
|
||||||
|
let babeldocCalled = false;
|
||||||
|
|
||||||
|
const mockExecFile: ExecFileFn = (
|
||||||
|
cmd: string,
|
||||||
|
args: string[],
|
||||||
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
|
) => {
|
||||||
|
if (cmd === "babeldoc") {
|
||||||
|
babeldocCalled = true;
|
||||||
|
babeldocArgs = args;
|
||||||
|
|
||||||
|
// Simulate babeldoc creating output file
|
||||||
|
const outputIndex = args.indexOf("-o");
|
||||||
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
|
const outputPath = args[outputIndex + 1];
|
||||||
|
// Create parent directory if needed
|
||||||
|
const outputDir = dirname(outputPath);
|
||||||
|
if (!existsSync(outputDir)) {
|
||||||
|
mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(outputPath, "%PDF-1.4\n%Translated content");
|
||||||
|
}
|
||||||
|
callback(null, "Translation complete", "");
|
||||||
|
} else if (cmd === "tar") {
|
||||||
|
// Simulate .tar creation (no compression)
|
||||||
|
// Verify it's creating .tar not .tar.gz
|
||||||
|
const tarArgs = args;
|
||||||
|
expect(tarArgs[0]).toBe("-cf"); // -cf for tar (not -czf for gzip)
|
||||||
|
callback(null, "Archive created", "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetPath = join(testDir, "output.tar");
|
||||||
|
await convert(testInputFile, "pdf", "pdf-zh", targetPath, undefined, mockExecFile);
|
||||||
|
|
||||||
|
expect(babeldocCalled).toBe(true);
|
||||||
|
expect(babeldocArgs).toContain("-i");
|
||||||
|
expect(babeldocArgs).toContain("-o");
|
||||||
|
expect(babeldocArgs).toContain("-l");
|
||||||
|
expect(babeldocArgs).toContain("zh-Hans"); // BabelDOC uses zh-Hans for simplified Chinese
|
||||||
|
expect(babeldocArgs).toContain("--output-format");
|
||||||
|
expect(babeldocArgs).toContain("pdf");
|
||||||
|
expect(babeldocArgs).toContain("--service");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("BabelDOC converter - English translation", () => {
|
||||||
|
const testDir = "./test-output-babeldoc-en";
|
||||||
|
const testInputFile = join(testDir, "input.pdf");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should call babeldoc with correct language argument for en", async () => {
|
||||||
|
let babeldocArgs: string[] = [];
|
||||||
|
|
||||||
|
const mockExecFile: ExecFileFn = (
|
||||||
|
cmd: string,
|
||||||
|
args: string[],
|
||||||
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
|
) => {
|
||||||
|
if (cmd === "babeldoc") {
|
||||||
|
babeldocArgs = args;
|
||||||
|
const outputIndex = args.indexOf("-o");
|
||||||
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
|
const outputPath = args[outputIndex + 1];
|
||||||
|
const outputDir = dirname(outputPath);
|
||||||
|
if (!existsSync(outputDir)) {
|
||||||
|
mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(outputPath, "%PDF-1.4\n%Translated");
|
||||||
|
}
|
||||||
|
callback(null, "Translation complete", "");
|
||||||
|
} else if (cmd === "tar") {
|
||||||
|
callback(null, "Archive created", "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetPath = join(testDir, "output.tar");
|
||||||
|
await convert(testInputFile, "pdf", "pdf-en", targetPath, undefined, mockExecFile);
|
||||||
|
|
||||||
|
expect(babeldocArgs).toContain("-l");
|
||||||
|
expect(babeldocArgs).toContain("en");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("BabelDOC converter - Traditional Chinese", () => {
|
||||||
|
const testDir = "./test-output-babeldoc-zhtw";
|
||||||
|
const testInputFile = join(testDir, "input.pdf");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should call babeldoc with correct language argument for zh-TW", async () => {
|
||||||
|
let babeldocArgs: string[] = [];
|
||||||
|
|
||||||
|
const mockExecFile: ExecFileFn = (
|
||||||
|
cmd: string,
|
||||||
|
args: string[],
|
||||||
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
|
) => {
|
||||||
|
if (cmd === "babeldoc") {
|
||||||
|
babeldocArgs = args;
|
||||||
|
const outputIndex = args.indexOf("-o");
|
||||||
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
|
const outputPath = args[outputIndex + 1];
|
||||||
|
const outputDir = dirname(outputPath);
|
||||||
|
if (!existsSync(outputDir)) {
|
||||||
|
mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(outputPath, "%PDF-1.4\n%Translated");
|
||||||
|
}
|
||||||
|
callback(null, "Translation complete", "");
|
||||||
|
} else if (cmd === "tar") {
|
||||||
|
callback(null, "Archive created", "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetPath = join(testDir, "output.tar");
|
||||||
|
await convert(testInputFile, "pdf", "pdf-zh-TW", targetPath, undefined, mockExecFile);
|
||||||
|
|
||||||
|
expect(babeldocArgs).toContain("-l");
|
||||||
|
expect(babeldocArgs).toContain("zh-Hant"); // BabelDOC uses zh-Hant for traditional Chinese
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("BabelDOC converter - Output structure", () => {
|
||||||
|
const testDir = "./test-output-babeldoc-structure";
|
||||||
|
const testInputFile = join(testDir, "input.pdf");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should create archive with translated-<lang>.pdf", async () => {
|
||||||
|
let tarSourceDir = "";
|
||||||
|
|
||||||
|
const mockExecFile: ExecFileFn = (
|
||||||
|
cmd: string,
|
||||||
|
args: string[],
|
||||||
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
|
) => {
|
||||||
|
if (cmd === "babeldoc") {
|
||||||
|
const outputIndex = args.indexOf("-o");
|
||||||
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
|
const outputPath = args[outputIndex + 1];
|
||||||
|
const outputDir = dirname(outputPath);
|
||||||
|
if (!existsSync(outputDir)) {
|
||||||
|
mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(outputPath, "%PDF-1.4\n%Translated content");
|
||||||
|
}
|
||||||
|
callback(null, "Translation complete", "");
|
||||||
|
} else if (cmd === "tar") {
|
||||||
|
// Capture the source directory for the tar archive
|
||||||
|
const cIndex = args.indexOf("-C");
|
||||||
|
if (cIndex !== -1 && args[cIndex + 1]) {
|
||||||
|
tarSourceDir = args[cIndex + 1];
|
||||||
|
}
|
||||||
|
callback(null, "Archive created", "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetPath = join(testDir, "output.tar");
|
||||||
|
await convert(testInputFile, "pdf", "pdf-ko", targetPath, undefined, mockExecFile);
|
||||||
|
|
||||||
|
// The archive source directory should end with "archive"
|
||||||
|
expect(tarSourceDir).toContain("archive");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should only use .tar format, not .tar.gz", async () => {
|
||||||
|
let tarCommand: string[] = [];
|
||||||
|
|
||||||
|
const mockExecFile: ExecFileFn = (
|
||||||
|
cmd: string,
|
||||||
|
args: string[],
|
||||||
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
|
) => {
|
||||||
|
if (cmd === "babeldoc") {
|
||||||
|
const outputIndex = args.indexOf("-o");
|
||||||
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
|
const outputPath = args[outputIndex + 1];
|
||||||
|
const outputDir = dirname(outputPath);
|
||||||
|
if (!existsSync(outputDir)) {
|
||||||
|
mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(outputPath, "%PDF-1.4\n%Translated");
|
||||||
|
}
|
||||||
|
callback(null, "Translation complete", "");
|
||||||
|
} else if (cmd === "tar") {
|
||||||
|
tarCommand = args;
|
||||||
|
callback(null, "Archive created", "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetPath = join(testDir, "output.tar");
|
||||||
|
await convert(testInputFile, "pdf", "pdf-de", targetPath, undefined, mockExecFile);
|
||||||
|
|
||||||
|
// Should use -cf (no gzip) not -czf
|
||||||
|
expect(tarCommand[0]).toBe("-cf");
|
||||||
|
expect(tarCommand).not.toContain("-czf");
|
||||||
|
expect(tarCommand).not.toContain("-z");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("BabelDOC converter - Error handling", () => {
|
||||||
|
const testDir = "./test-output-babeldoc-error";
|
||||||
|
const testInputFile = join(testDir, "input.pdf");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reject with error when babeldoc fails", async () => {
|
||||||
|
const mockExecFile: ExecFileFn = (
|
||||||
|
cmd: string,
|
||||||
|
_args: string[],
|
||||||
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
|
) => {
|
||||||
|
if (cmd === "babeldoc") {
|
||||||
|
const error = new Error("Translation failed") as ExecFileException;
|
||||||
|
error.code = 1;
|
||||||
|
callback(error, "", "babeldoc: Translation error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetPath = join(testDir, "output.tar");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
convert(testInputFile, "pdf", "pdf-zh", targetPath, undefined, mockExecFile),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reject when no output PDF is generated", async () => {
|
||||||
|
const mockExecFile: ExecFileFn = (
|
||||||
|
cmd: string,
|
||||||
|
_args: string[],
|
||||||
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
|
) => {
|
||||||
|
if (cmd === "babeldoc") {
|
||||||
|
// Don't create output file, simulating failed translation
|
||||||
|
callback(null, "Complete", "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetPath = join(testDir, "output.tar");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
convert(testInputFile, "pdf", "pdf-zh", targetPath, undefined, mockExecFile),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("BabelDOC converter - Markdown output format", () => {
|
||||||
|
const testDir = "./test-output-babeldoc-md";
|
||||||
|
const testInputFile = join(testDir, "input.pdf");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should call babeldoc with md output format", async () => {
|
||||||
|
let babeldocArgs: string[] = [];
|
||||||
|
|
||||||
|
const mockExecFile: ExecFileFn = (
|
||||||
|
cmd: string,
|
||||||
|
args: string[],
|
||||||
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
|
) => {
|
||||||
|
if (cmd === "babeldoc") {
|
||||||
|
babeldocArgs = args;
|
||||||
|
const outputIndex = args.indexOf("-o");
|
||||||
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
|
const outputPath = args[outputIndex + 1];
|
||||||
|
const outputDir = dirname(outputPath);
|
||||||
|
if (!existsSync(outputDir)) {
|
||||||
|
mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(outputPath, "# Translated Markdown Content");
|
||||||
|
}
|
||||||
|
callback(null, "Translation complete", "");
|
||||||
|
} else if (cmd === "tar") {
|
||||||
|
callback(null, "Archive created", "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetPath = join(testDir, "output.tar");
|
||||||
|
await convert(testInputFile, "pdf", "md-zh", targetPath, undefined, mockExecFile);
|
||||||
|
|
||||||
|
expect(babeldocArgs).toContain("-l");
|
||||||
|
expect(babeldocArgs).toContain("zh-Hans");
|
||||||
|
expect(babeldocArgs).toContain("--output-format");
|
||||||
|
expect(babeldocArgs).toContain("md");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("BabelDOC converter - HTML output format", () => {
|
||||||
|
const testDir = "./test-output-babeldoc-html";
|
||||||
|
const testInputFile = join(testDir, "input.pdf");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
if (!existsSync(testDir)) {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(testInputFile, "%PDF-1.4\n%Test PDF content");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(testDir)) {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should call babeldoc with html output format", async () => {
|
||||||
|
let babeldocArgs: string[] = [];
|
||||||
|
|
||||||
|
const mockExecFile: ExecFileFn = (
|
||||||
|
cmd: string,
|
||||||
|
args: string[],
|
||||||
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
|
) => {
|
||||||
|
if (cmd === "babeldoc") {
|
||||||
|
babeldocArgs = args;
|
||||||
|
const outputIndex = args.indexOf("-o");
|
||||||
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
|
const outputPath = args[outputIndex + 1];
|
||||||
|
const outputDir = dirname(outputPath);
|
||||||
|
if (!existsSync(outputDir)) {
|
||||||
|
mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(outputPath, "<html><body>Translated HTML</body></html>");
|
||||||
|
}
|
||||||
|
callback(null, "Translation complete", "");
|
||||||
|
} else if (cmd === "tar") {
|
||||||
|
callback(null, "Archive created", "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetPath = join(testDir, "output.tar");
|
||||||
|
await convert(testInputFile, "pdf", "html-ja", targetPath, undefined, mockExecFile);
|
||||||
|
|
||||||
|
expect(babeldocArgs).toContain("-l");
|
||||||
|
expect(babeldocArgs).toContain("ja");
|
||||||
|
expect(babeldocArgs).toContain("--output-format");
|
||||||
|
expect(babeldocArgs).toContain("html");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue