// Configuration globale
let config = {
baseUrl: "",
currentPath: "",
};
// Initialisation de l'application
document.addEventListener("DOMContentLoaded", function () {
initializeApp();
setupEventListeners();
});
function initializeApp() {
// Tentative de configuration automatique depuis l'URL courante
autoConfigureFromURL();
// Affichage du formulaire de développement
showDevForm();
}
function setupEventListeners() {
// Formulaire de développement
const urlForm = document.getElementById("url-form");
if (urlForm) {
urlForm.addEventListener("submit", handleUrlFormSubmit);
}
// Gestion de la navigation arrière/avant du navigateur
window.addEventListener("popstate", handlePopState);
// Gestionnaires pour la prévisualisation
const closePreview = document.getElementById("close-preview");
if (closePreview) {
closePreview.addEventListener("click", hidePreview);
}
const downloadFile = document.getElementById("download-file");
if (downloadFile) {
downloadFile.addEventListener("click", downloadCurrentFile);
}
}
function handlePopState(event) {
console.log("Navigation arrière/avant détectée:", event.state);
if (event.state && event.state.baseUrl) {
// Restaurer la configuration
config.baseUrl = event.state.baseUrl;
config.currentPath = event.state.path || "";
updateConfigDisplay();
// Charger le contenu sans ajouter à l'historique (pour éviter les boucles)
loadBucketContents(config.currentPath, false);
}
}
function autoConfigureFromURL() {
const currentUrl = window.location.href;
const url = new URL(currentUrl);
// Configuration simplifiée : on prend juste le base URL sans le bucket
config.baseUrl = `${url.protocol}//${url.host}${url.pathname.replace("/index.html", "")}`;
// Vérifier s'il y a un fragment d'URL pour définir le chemin initial
if (url.hash) {
const hashPath = url.hash.replace("#/", "");
config.currentPath = hashPath;
}
updateConfigDisplay();
console.log("Configuration automatique:", config);
// Charger automatiquement le contenu si on a une config valide
if (config.baseUrl) {
loadBucketContents(config.currentPath, false);
}
}
function handleUrlFormSubmit(event) {
event.preventDefault();
const urlInput = document.getElementById("minio-url");
if (!urlInput) return;
const minioUrl = urlInput.value.trim();
if (!minioUrl) {
showError("Veuillez saisir une URL MinIO");
return;
}
parseMinioURL(minioUrl);
}
function parseMinioURL(url) {
try {
const urlObj = new URL(url);
// Configuration simplifiée : on prend l'URL complète comme base
config.baseUrl = url.replace(/\/$/, ""); // Supprimer le slash final si présent
config.currentPath = "";
updateConfigDisplay();
// Ajouter l'état initial à l'historique
const initialState = { path: "", baseUrl: config.baseUrl };
history.replaceState(initialState, "", window.location.href);
loadBucketContents();
} catch (error) {
showError("URL invalide: " + error.message);
console.error("Erreur de parsing URL:", error);
}
}
function updateConfigDisplay() {
const configInfo = document.getElementById("config-info");
if (configInfo) {
configInfo.innerHTML = `
Configuration actuelle:
URL de base: ${config.baseUrl}
Chemin actuel: /${config.currentPath}
`;
}
}
function showDevForm() {
const devForm = document.getElementById("dev-form");
if (devForm) {
devForm.style.display = "block";
}
}
function showError(message) {
const errorSection = document.getElementById("error-section");
const errorMessage = document.getElementById("error-message");
if (errorMessage) {
errorMessage.textContent = message;
}
if (errorSection) {
errorSection.style.display = "block";
// Masquer l'erreur après 5 secondes
setTimeout(() => {
errorSection.style.display = "none";
}, 5000);
}
}
function showLoading(show = true) {
const loading = document.getElementById("loading");
const table = document.getElementById("files-table");
if (loading) {
loading.style.display = show ? "block" : "none";
}
if (table) {
table.style.display = show ? "none" : "table";
}
}
function updateCurrentPathDisplay() {
const pathDisplay = document.getElementById("current-path");
if (!pathDisplay) return;
// Créer la structure de navigation breadcrumb
let breadcrumbHTML = '';
// Toujours commencer par la racine
if (config.currentPath) {
// Si on n'est pas à la racine, rendre "/" cliquable
breadcrumbHTML += '/ ';
} else {
// Si on est à la racine, afficher "/" sans lien
breadcrumbHTML += '/';
}
// Si on a un chemin, découper et créer les segments
if (config.currentPath) {
const segments = config.currentPath.split('/').filter(segment => segment.length > 0);
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const segmentPath = segments.slice(0, i + 1).join('/');
// Le dernier segment (actuel) n'est pas cliquable
if (i === segments.length - 1) {
breadcrumbHTML += segment;
} else {
// Les segments précédents sont cliquables
breadcrumbHTML += `${segment} / `;
}
}
}
pathDisplay.innerHTML = breadcrumbHTML;
// Ajouter les event listeners aux liens de navigation
const pathLinks = pathDisplay.querySelectorAll('.path-segment');
pathLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetPath = link.getAttribute('data-path');
loadBucketContents(targetPath, true);
});
});
}
async function loadBucketContents(path = "", pushToHistory = true) {
console.log(
"Debug loadBucketContents - path received:",
JSON.stringify(path),
);
if (!config.baseUrl) {
showError("Configuration URL manquante");
return;
}
showLoading(true);
config.currentPath = path;
updateCurrentPathDisplay();
// Ajouter à l'historique du navigateur si demandé
if (pushToHistory) {
const state = { path: path, baseUrl: config.baseUrl };
const url = path ? `#/${path}` : "#/";
history.pushState(state, "", url);
}
try {
// Construction de l'URL API avec delimiter
let apiUrl = `${config.baseUrl}?list-type=2&delimiter=/`;
// Construire le prefix pour l'API MinIO
if (path) {
// Pour un sous-dossier
const cleanPath = path.startsWith("/") ? path.substring(1) : path;
const fullPrefix = cleanPath + "/";
apiUrl += `&prefix=${encodeURIComponent(fullPrefix)}`;
}
console.log("Appel API MinIO:", apiUrl);
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const xmlText = await response.text();
console.log("Réponse XML reçue:", xmlText);
const contents = parseMinioXML(xmlText);
displayContents(contents);
} catch (error) {
console.error("Erreur lors du chargement:", error);
showError("Erreur de chargement: " + error.message);
} finally {
showLoading(false);
}
}
function parseMinioXML(xmlText) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
const contents = {
folders: [],
files: [],
};
// Déterminer le préfixe attendu basé sur la navigation actuelle
let expectedPrefix = "";
if (config.currentPath) {
expectedPrefix = config.currentPath + "/";
}
// Parse CommonPrefixes (dossiers)
const prefixes = xmlDoc.getElementsByTagName("CommonPrefixes");
for (let prefix of prefixes) {
const prefixElement = prefix.getElementsByTagName("Prefix")[0];
if (prefixElement) {
const fullPrefix = prefixElement.textContent;
// Extraire le nom du dossier (dernière partie avant le /)
const folderName = fullPrefix.replace(/\/$/, "").split("/").pop();
if (folderName) {
// Construire le chemin pour la navigation
let folderPath;
if (config.currentPath) {
folderPath = config.currentPath + "/" + folderName;
} else {
folderPath = folderName;
}
console.log(
"Debug parsing - fullPrefix:",
fullPrefix,
"folderName:",
folderName,
"currentPath:",
config.currentPath,
"folderPath:",
folderPath,
);
contents.folders.push({
name: folderName,
path: folderPath,
});
}
}
}
// Parse Contents (fichiers)
const objects = xmlDoc.getElementsByTagName("Contents");
for (let obj of objects) {
const keyElement = obj.getElementsByTagName("Key")[0];
const sizeElement = obj.getElementsByTagName("Size")[0];
const modifiedElement = obj.getElementsByTagName("LastModified")[0];
if (keyElement) {
const fullKey = keyElement.textContent;
const fileName = fullKey.split("/").pop();
// Vérifier que le fichier correspond au niveau de navigation actuel
let shouldInclude = false;
if (config.currentPath) {
// Dans un sous-dossier : vérifier que la clé commence par le bon préfixe
const currentPrefix = config.currentPath + "/";
if (fullKey.startsWith(currentPrefix)) {
// S'assurer qu'on n'affiche que les fichiers directs (pas dans des sous-sous-dossiers)
const relativePath = fullKey.replace(currentPrefix, "");
if (!relativePath.includes("/")) {
shouldInclude = true;
}
}
} else {
// À la racine : afficher tous les fichiers qui ne sont pas dans des dossiers
if (!fullKey.includes("/")) {
shouldInclude = true;
}
}
// Exclure les fichiers de l'application
if (
shouldInclude &&
fileName &&
!["index.html", "style.css", "script.js"].includes(fileName)
) {
contents.files.push({
name: fileName,
size: sizeElement ? parseInt(sizeElement.textContent) : 0,
modified: modifiedElement
? new Date(modifiedElement.textContent)
: new Date(),
fullKey: fullKey,
});
}
}
}
return contents;
}
function displayContents(contents) {
const tbody = document.getElementById("files-tbody");
if (!tbody) return;
tbody.innerHTML = "";
// Bouton parent directory si on n'est pas à la racine
if (config.currentPath) {
const parentPath = config.currentPath.split("/").slice(0, -1).join("/");
const parentRow = createParentRow(parentPath);
tbody.appendChild(parentRow);
}
// Afficher les dossiers
contents.folders.forEach((folder) => {
const row = createFolderRow(folder);
tbody.appendChild(row);
});
// Afficher les fichiers
contents.files.forEach((file) => {
const row = createFileRow(file);
tbody.appendChild(row);
});
// Afficher le tableau
const filesTable = document.getElementById("files-table");
if (filesTable) {
filesTable.style.display = "table";
}
// Chercher et ouvrir automatiquement index.rst s'il existe
autoOpenIndexRst(contents.files);
}
function createParentRow(parentPath) {
const row = document.createElement("tr");
row.className = "clickable-row";
row.innerHTML = `
Prévisualisation non disponible pour ce type de fichier (.${extension})
Utilisez le bouton "Télécharger" ci-dessous pour obtenir le fichier.
$1");
// Code blocks
html = html.replace(/```([\s\S]*?)```/g, "$1");
// Links
html = html.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
'$1',
);
// Images
html = html.replace(
/!\[([^\]]*)\]\(([^)]+)\)/g,
'"); html = "
" + html + "
"; // Clean up empty paragraphs html = html.replace(/<\/p>/g, ""); html = html.replace(/
( ( ()/g, "$1");
html = html.replace(/(<\/ul>)<\/p>/g, "$1");
html = html.replace(/
)/g, "$1");
html = html.replace(/(<\/pre>)<\/p>/g, "$1");
return html;
}
// Fonction simple de parsing reStructuredText vers HTML
function parseRst(text) {
let html = text;
// Créer un système de placeholders pour éviter les liens imbriqués
const linkPlaceholders = new Map();
let placeholderIndex = 0;
// Fonction pour créer un placeholder unique
const createLinkPlaceholder = (linkText, linkUrl) => {
const placeholder = `__LINK_PLACEHOLDER_${placeholderIndex++}__`;
linkPlaceholders.set(
placeholder,
`${linkText}`,
);
return placeholder;
};
// Headers (RST style with underlines) - ordre important pour éviter les conflits
html = html.replace(/^(.*)\n#{3,}$/gim, "$1
");
html = html.replace(/^(.*)\n={3,}$/gim, "$1
");
html = html.replace(/^(.*)\n-{3,}$/gim, "$1
");
html = html.replace(/^(.*)\n~{3,}$/gim, "$1
");
html = html.replace(/^(.*)\n\^{3,}$/gim, "$1
");
// Metadata fields
html = html.replace(/^:([^:]+):\s*(.*)$/gim, (match, key, value) => {
// Remplacer les liens par des placeholders dans les métadonnées
const processedValue = value.replace(
/`([^<]+) <([^>]+)>`_/g,
(match, text, url) => {
return createLinkPlaceholder(text, url);
},
);
return ``;
});
// Process directives with their options and content - regex corrigée pour s'arrêter aux lignes non indentées
html = html.replace(
/\.\. ([^:]+)::([\s\S]*?)(?=\n\S|\.\.|$)/g,
(match, directive, block) => {
// Gestion spéciale pour big_button
const isBigButton = directive === "big_button";
const className = isBigButton
? "rst-big-button"
: `rst-directive rst-directive-${directive}`;
let result = `$1");
// Code blocks (:: at end of line followed by indented block)
html = html.replace(
/::\s*\n\n((?: .*\n?)+)/g,
"
",
);
// Links restants dans le texte principal
html = html.replace(/`([^<]+) <([^>]+)>`_/g, (match, text, url) => {
return createLinkPlaceholder(text, url);
});
// Lists
html = html.replace(/^\* (.*$)/gim, "$1
html = html.replace(/(
" + match + "
";
});
// Line breaks
html = html.replace(/\n\n/g, "
"); html = "
" + html + "
"; // Clean up empty paragraphs and fix structure html = html.replace(/<\/p>/g, ""); html = html.replace(/
( ( ( ()/g, "$1");
html = html.replace(/(<\/ul>)<\/p>/g, "$1");
html = html.replace(/
)/g, "$1");
html = html.replace(/(<\/pre>)<\/p>/g, "$1");
// Restaurer tous les liens à partir des placeholders
for (const [placeholder, linkHtml] of linkPlaceholders) {
html = html.replace(new RegExp(placeholder, "g"), linkHtml);
}
return html;
}
function showMarkdownPreview(fileUrl, container) {
fetch(fileUrl)
.then((response) => {
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
return response.text();
})
.then((text) => {
const htmlContent = parseMarkdown(text);
const div = document.createElement("div");
div.className = "markdown-content";
div.innerHTML = htmlContent;
container.innerHTML = "";
container.appendChild(div);
})
.catch((error) => {
console.error("Erreur chargement markdown:", error);
container.innerHTML =
'