feat(export): v2.1.0 - export multi-formats HTML/MD-bundle/ePub
- backend/export.py: 3 exporters (HTML standalone, ZIP MD, ePub 3)
- HTML: CSS inliné, images base64, navigation wikilinks
- MD bundle: ZIP préserve structure répertoire
- ePub: zipfile + mistune, zero nouvelle dépendance
- 3 endpoints API: /api/export/{html,md-bundle,epub}
- UI: dropdown Export dans toolbar viewer (HTML/MD/ePub)
- 20 tests (validité, contenu, erreurs, API)
- 439 tests passent au total
This commit is contained in:
+67
-2
@@ -1,5 +1,5 @@
|
||||
/* ObsiGate — Viewer module */
|
||||
import { api } from './auth.js';
|
||||
import { api, AuthManager } from './auth.js';
|
||||
import { state } from './state.js';
|
||||
import { escapeHtml, safeCreateIcons, safeHighlight, getFileIcon, openEditor, copyToClipboard } from './utils.js';
|
||||
import { TabManager, closeMobileSidebar, ContextMenuManager, RightSidebarManager, showToast, buildFrontmatterCard } from './ui.js';
|
||||
@@ -13,6 +13,41 @@ import { openShareDialog } from './config.js';
|
||||
import { cacheViewedFile, getCachedFile } from './offline.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
// ── Multi-format export ────────────────────────────────────────────────────
|
||||
// Downloads a file export (HTML / MD bundle / ePub) via the authenticated
|
||||
// export endpoints, showing a loading toast while the request is in flight.
|
||||
async function downloadExport(vault, path, format, fallbackName) {
|
||||
showToast(t("viewer.export_start"), "info");
|
||||
const url = `/api/export/${format}?vault=${encodeURIComponent(vault)}&path=${encodeURIComponent(path)}`;
|
||||
try {
|
||||
const headers = AuthManager.getAuthHeaders() || {};
|
||||
const res = await fetch(url, { credentials: "include", headers });
|
||||
if (!res.ok) {
|
||||
let detail = "";
|
||||
try { detail = (await res.json()).detail || ""; } catch (_) { /* ignore */ }
|
||||
throw new Error(detail || "HTTP " + res.status);
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = fallbackName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
|
||||
showToast(t("viewer.export_done"), "success");
|
||||
} catch (err) {
|
||||
console.error("Export error:", err);
|
||||
showToast(t("viewer.export_error") + " " + err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Close any open export dropdown when clicking elsewhere.
|
||||
document.addEventListener("click", function (e) {
|
||||
if (e.target && e.target.closest && e.target.closest(".export-dropdown")) return;
|
||||
document.querySelectorAll(".export-menu").forEach(function (m) { m.style.display = "none"; });
|
||||
});
|
||||
|
||||
// Pane-aware content area helper — delegates to PaneManager when split
|
||||
function getContentArea() {
|
||||
// Override set by PaneTabManager._renderFileInPane for cross-module rendering
|
||||
@@ -635,6 +670,36 @@ export function renderFile(data) {
|
||||
window.open(pdfUrl, "_blank");
|
||||
});
|
||||
|
||||
// Export dropdown button (HTML / MD bundle / ePub)
|
||||
const exportWrap = el("div", { class: "export-dropdown", style: "position:relative;display:inline-block" });
|
||||
const exportBtn = el("button", { class: "btn-action", title: t("viewer.export_title") }, [icon("download", 14), document.createTextNode(t("viewer.export"))]);
|
||||
const exportMenu = el("div", { class: "export-menu", style: "display:none;position:absolute;right:0;top:100%;z-index:1000;min-width:200px;padding:4px;background:var(--bg,#fff);border:1px solid #ccc;border-radius:6px;box-shadow:0 6px 16px rgba(0,0,0,.18)" });
|
||||
const exportOptions = [
|
||||
{ format: "html", icon: "file-code", label: t("viewer.export_html"), ext: ".html" },
|
||||
{ format: "md-bundle", icon: "archive", label: t("viewer.export_md_bundle"), ext: ".zip" },
|
||||
{ format: "epub", icon: "book-open", label: t("viewer.export_epub"), ext: ".epub" },
|
||||
];
|
||||
exportOptions.forEach((opt) => {
|
||||
const item = el("button", { class: "export-menu-item", style: "display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;border:none;background:none;cursor:pointer;border-radius:4px;text-align:left;font-size:13px" }, [
|
||||
icon(opt.icon, 14),
|
||||
document.createTextNode(opt.label),
|
||||
]);
|
||||
item.addEventListener("mouseenter", () => { item.style.background = "rgba(128,128,128,.15)"; });
|
||||
item.addEventListener("mouseleave", () => { item.style.background = "none"; });
|
||||
item.addEventListener("click", () => {
|
||||
exportMenu.style.display = "none";
|
||||
const baseName = data.path.split("/").pop().replace(/\.[^.]+$/, "") || "document";
|
||||
downloadExport(data.vault, data.path, opt.format, baseName + opt.ext);
|
||||
});
|
||||
exportMenu.appendChild(item);
|
||||
});
|
||||
exportBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
exportMenu.style.display = exportMenu.style.display === "block" ? "none" : "block";
|
||||
});
|
||||
exportWrap.appendChild(exportBtn);
|
||||
exportWrap.appendChild(exportMenu);
|
||||
|
||||
const editBtn = el("button", { class: "btn-action", title: "Editer" }, [icon("edit", 14), document.createTextNode("Editer")]);
|
||||
editBtn.addEventListener("click", () => {
|
||||
openEditor(data.vault, data.path);
|
||||
@@ -754,7 +819,7 @@ export function renderFile(data) {
|
||||
// Assemble
|
||||
area.innerHTML = "";
|
||||
area.appendChild(breadcrumb);
|
||||
area.appendChild(el("div", { class: "file-header" }, [el("div", { class: "file-title" }, [document.createTextNode(data.title)]), tagsDiv, el("div", { class: "file-actions" }, [copyBtn, sourceBtn, mdBtn, pdfBtn, editBtn, forgeBtn, openNewWindowBtn, tocBtn, shareBtn, bookmarkBtn])]));
|
||||
area.appendChild(el("div", { class: "file-header" }, [el("div", { class: "file-title" }, [document.createTextNode(data.title)]), tagsDiv, el("div", { class: "file-actions" }, [copyBtn, sourceBtn, mdBtn, pdfBtn, exportWrap, editBtn, forgeBtn, openNewWindowBtn, tocBtn, shareBtn, bookmarkBtn])]));
|
||||
if (fmSection) area.appendChild(fmSection);
|
||||
area.appendChild(mdDiv);
|
||||
area.appendChild(rawDiv);
|
||||
|
||||
Reference in New Issue
Block a user