Files
minio-explorer/script.js
Bertrand Benjamin cc6ae5d606
All checks were successful
Deploy MinIO Explorer / deploy (push) Successful in 47s
feat: improve preview style
2025-09-06 16:58:17 +02:00

1111 lines
31 KiB
JavaScript

// 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 = `
<strong>Configuration actuelle:</strong><br>
URL de base: ${config.baseUrl}<br>
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) {
pathDisplay.textContent = "/" + config.currentPath;
}
}
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.innerHTML = `
<td>
<span class="icon-parent"></span>
<a href="#" class="folder-link parent-dir" data-path="${parentPath}">..</a>
</td>
<td>-</td>
<td>-</td>
<td>-</td>
`;
const link = row.querySelector(".folder-link");
link.addEventListener("click", (e) => {
e.preventDefault();
loadBucketContents(parentPath, true);
});
return row;
}
function createFolderRow(folder) {
const row = document.createElement("tr");
row.innerHTML = `
<td>
<span class="icon-folder"></span>
<a href="#" class="folder-link" data-path="${folder.path}">${folder.name}/</a>
</td>
<td>-</td>
<td>-</td>
<td>-</td>
`;
const link = row.querySelector(".folder-link");
link.addEventListener("click", (e) => {
e.preventDefault();
loadBucketContents(folder.path, true);
});
return row;
}
function createFileRow(file) {
const row = document.createElement("tr");
const fileUrl = `${config.baseUrl}/${file.fullKey}`;
row.innerHTML = `
<td>
<span class="icon-file"></span>
<a href="#" class="file-link" data-file-url="${fileUrl}" data-file-name="${file.name}">${file.name}</a>
</td>
<td class="file-size">${formatFileSize(file.size)}</td>
<td class="file-date">${formatDate(file.modified)}</td>
<td>
<button class="copy-btn" data-url="${fileUrl}">Copier lien</button>
</td>
`;
const fileLink = row.querySelector(".file-link");
fileLink.addEventListener("click", (e) => {
e.preventDefault();
showPreview(fileUrl, file.name);
});
const copyBtn = row.querySelector(".copy-btn");
copyBtn.addEventListener("click", () => copyToClipboard(fileUrl));
return row;
}
function formatFileSize(bytes) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
}
function formatDate(date) {
return date.toLocaleString("fr-FR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
console.log("Lien copié:", text);
// Feedback visuel temporaire
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = "Copié !";
setTimeout(() => {
btn.textContent = originalText;
}, 1000);
} catch (error) {
console.error("Erreur copie clipboard:", error);
// Fallback pour les navigateurs plus anciens
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
}
}
// Variables globales pour la prévisualisation
let currentPreviewFile = {
url: "",
name: "",
};
function detectFileType(fileName) {
const extension = fileName.toLowerCase().split(".").pop();
const fileTypes = {
images: ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"],
text: ["txt", "json", "xml", "csv", "log"],
code: [
"js",
"css",
"html",
"htm",
"py",
"java",
"cpp",
"c",
"php",
"rb",
"go",
"rs",
"ts",
"jsx",
"tsx",
"vue",
"svelte",
"tex",
"latex",
],
markdown: ["md", "markdown"],
rst: ["rst", "rest"],
pdf: ["pdf"],
video: ["mp4", "avi", "mov", "wmv", "flv", "webm", "mkv"],
audio: ["mp3", "wav", "ogg", "aac", "flac", "m4a"],
archive: ["zip", "rar", "7z", "tar", "gz", "bz2"],
};
for (const [type, extensions] of Object.entries(fileTypes)) {
if (extensions.includes(extension)) {
return type;
}
}
return "unknown";
}
function showPreview(fileUrl, fileName) {
currentPreviewFile = { url: fileUrl, name: fileName };
const previewSection = document.getElementById("preview-section");
const previewTitle = document.getElementById("preview-title");
const previewContent = document.getElementById("preview-content");
if (!previewSection || !previewTitle || !previewContent) {
console.error("Éléments de prévisualisation non trouvés");
return;
}
previewTitle.textContent = `Prévisualisation: ${fileName}`;
previewContent.innerHTML =
'<div style="text-align: center; padding: 20px;">Chargement...</div>';
previewSection.style.display = "flex";
const fileType = detectFileType(fileName);
switch (fileType) {
case "images":
showImagePreview(fileUrl, previewContent);
break;
case "text":
showTextPreview(fileUrl, previewContent);
break;
case "code":
showCodePreview(fileUrl, fileName, previewContent);
break;
case "markdown":
showMarkdownPreview(fileUrl, previewContent);
break;
case "rst":
showRstPreview(fileUrl, previewContent);
break;
case "pdf":
showPdfPreview(fileUrl, previewContent);
break;
case "video":
showVideoPreview(fileUrl, previewContent);
break;
case "audio":
showAudioPreview(fileUrl, previewContent);
break;
default:
showUnsupportedPreview(fileName, previewContent);
}
}
function showImagePreview(fileUrl, container) {
const img = document.createElement("img");
img.src = fileUrl;
img.alt = "Prévisualisation image";
img.onload = () => {
container.innerHTML = "";
container.appendChild(img);
};
img.onerror = () => {
container.innerHTML =
'<div class="preview-error">Erreur: Impossible de charger l\'image</div>';
};
}
function showTextPreview(fileUrl, container) {
fetch(fileUrl)
.then((response) => {
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
return response.text();
})
.then((text) => {
const pre = document.createElement("pre");
pre.textContent = text;
container.innerHTML = "";
container.appendChild(pre);
})
.catch((error) => {
console.error("Erreur chargement texte:", error);
container.innerHTML =
'<div class="preview-error">Erreur: Impossible de charger le fichier texte</div>';
});
}
function detectLanguageFromExtension(fileName) {
const extension = fileName.toLowerCase().split(".").pop();
const languageMap = {
js: "javascript",
jsx: "jsx",
ts: "typescript",
tsx: "tsx",
py: "python",
java: "java",
c: "c",
cpp: "cpp",
cxx: "cpp",
cc: "cpp",
php: "php",
rb: "ruby",
go: "go",
rs: "rust",
html: "html",
htm: "html",
css: "css",
scss: "scss",
sass: "sass",
less: "less",
json: "json",
xml: "xml",
vue: "vue",
svelte: "svelte",
sh: "bash",
bash: "bash",
zsh: "bash",
fish: "bash",
ps1: "powershell",
sql: "sql",
r: "r",
matlab: "matlab",
m: "matlab",
swift: "swift",
kt: "kotlin",
scala: "scala",
clj: "clojure",
hs: "haskell",
lua: "lua",
pl: "perl",
tex: "latex",
latex: "latex",
md: "markdown",
markdown: "markdown",
yml: "yaml",
yaml: "yaml",
toml: "toml",
ini: "ini",
conf: "bash",
config: "bash",
};
return languageMap[extension] || "text";
}
function showCodePreview(fileUrl, fileName, container) {
fetch(fileUrl)
.then((response) => {
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
return response.text();
})
.then((text) => {
const language = detectLanguageFromExtension(fileName);
const pre = document.createElement("pre");
const code = document.createElement("code");
if (language !== "text") {
code.className = `language-${language}`;
}
code.textContent = text;
pre.appendChild(code);
container.innerHTML = "";
container.appendChild(pre);
// Appliquer la coloration syntaxique si Prism est disponible
if (window.Prism && language !== "text") {
try {
Prism.highlightElement(code);
} catch (error) {
console.warn("Erreur coloration syntaxique:", error);
}
}
})
.catch((error) => {
console.error("Erreur chargement code:", error);
container.innerHTML =
'<div class="preview-error">Erreur: Impossible de charger le fichier</div>';
});
}
function showPdfPreview(fileUrl, container) {
const iframe = document.createElement("iframe");
iframe.src = fileUrl;
iframe.style.width = "100%";
iframe.style.height = "100%";
container.innerHTML = "";
container.appendChild(iframe);
}
function showVideoPreview(fileUrl, container) {
const video = document.createElement("video");
video.src = fileUrl;
video.controls = true;
video.style.maxWidth = "100%";
container.innerHTML = "";
container.appendChild(video);
}
function showAudioPreview(fileUrl, container) {
const audio = document.createElement("audio");
audio.src = fileUrl;
audio.controls = true;
audio.style.width = "100%";
container.innerHTML = "";
container.appendChild(audio);
}
function showUnsupportedPreview(fileName, container) {
const extension = fileName.toLowerCase().split(".").pop();
container.innerHTML = `
<div class="preview-error">
<p>Prévisualisation non disponible pour ce type de fichier (.${extension})</p>
<p>Utilisez le bouton "Télécharger" ci-dessous pour obtenir le fichier.</p>
</div>
`;
}
function hidePreview() {
const previewSection = document.getElementById("preview-section");
if (previewSection) {
previewSection.style.display = "none";
}
currentPreviewFile = { url: "", name: "" };
}
function downloadCurrentFile() {
if (currentPreviewFile.url) {
const link = document.createElement("a");
link.href = currentPreviewFile.url;
link.download = currentPreviewFile.name;
link.target = "_blank";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
// Fonction pour ouvrir automatiquement index.rst s'il existe
function autoOpenIndexRst(files) {
// Chercher un fichier index.rst dans la liste
const indexRstFile = files.find(
(file) => file.name.toLowerCase() === "index.rst",
);
if (indexRstFile) {
// Construire l'URL complète du fichier
const fileUrl = `${config.baseUrl}/${indexRstFile.fullKey}`;
// Ouvrir automatiquement la prévisualisation
console.log("Auto-ouverture de index.rst trouvé:", indexRstFile.name);
showPreview(fileUrl, indexRstFile.name);
} else {
// Pas d'index.rst trouvé, fermer le preview s'il est ouvert
console.log("Aucun index.rst trouvé, fermeture du preview");
hidePreview();
}
}
// Fonction simple de parsing Markdown vers HTML
function parseMarkdown(text) {
let html = text;
// Headers
html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
// Bold
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
html = html.replace(/__(.*?)__/g, "<strong>$1</strong>");
// Italic
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
html = html.replace(/_(.*?)_/g, "<em>$1</em>");
// Code inline
html = html.replace(/`(.*?)`/g, "<code>$1</code>");
// Code blocks
html = html.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
// Links
html = html.replace(
/\[([^\]]+)\]\(([^)]+)\)/g,
'<a href="$2" target="_blank">$1</a>',
);
// Images
html = html.replace(
/!\[([^\]]*)\]\(([^)]+)\)/g,
'<img alt="$1" src="$2" style="max-width: 100%; height: auto;">',
);
// Lists
html = html.replace(/^\* (.*$)/gim, "<li>$1</li>");
html = html.replace(/^- (.*$)/gim, "<li>$1</li>");
html = html.replace(/^\d+\. (.*$)/gim, "<li>$1</li>");
// Wrap consecutive <li> items in <ul>
html = html.replace(/(<li>.*<\/li>)/gs, (match) => {
return "<ul>" + match + "</ul>";
});
// Line breaks
html = html.replace(/\n\n/g, "</p><p>");
html = "<p>" + html + "</p>";
// Clean up empty paragraphs
html = html.replace(/<p><\/p>/g, "");
html = html.replace(/<p>(<h[1-6]>)/g, "$1");
html = html.replace(/(<\/h[1-6]>)<\/p>/g, "$1");
html = html.replace(/<p>(<ul>)/g, "$1");
html = html.replace(/(<\/ul>)<\/p>/g, "$1");
html = html.replace(/<p>(<pre>)/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,
`<a href="${linkUrl}" target="_blank">${linkText}</a>`,
);
return placeholder;
};
// Headers (RST style with underlines) - ordre important pour éviter les conflits
html = html.replace(/^(.*)\n#{3,}$/gim, "<h1>$1</h1>");
html = html.replace(/^(.*)\n={3,}$/gim, "<h2>$1</h2>");
html = html.replace(/^(.*)\n-{3,}$/gim, "<h3>$1</h3>");
html = html.replace(/^(.*)\n~{3,}$/gim, "<h4>$1</h4>");
html = html.replace(/^(.*)\n\^{3,}$/gim, "<h5>$1</h5>");
// 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 `<div class="rst-metadata"><span class="metadata-key">${key}:</span> <span class="metadata-value">${processedValue}</span></div>`;
});
// 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 = `<div class="${className}" data-directive="${directive}">`;
if (block && block.trim()) {
const lines = block.split("\n");
let options = "";
let content = "";
let inContentSection = false;
// Séparer d'abord les lignes d'options et de contenu
let optionLines = [];
let contentLines = [];
let isInOptions = true;
for (const line of lines) {
if (!line.trim()) {
// Ligne vide - si on est en section contenu, l'ajouter
if (!isInOptions) {
contentLines.push(line);
}
continue;
}
// Vérifier si c'est une option (:key: value)
const optionMatch = line.match(/^[ \t]*:([^:]+):\s*(.*)$/);
if (optionMatch && isInOptions) {
// C'est une option et on est toujours dans la section options
optionLines.push(line);
} else {
// C'est du contenu - marquer qu'on est sorti de la section options
isInOptions = false;
contentLines.push(line);
}
}
// Traiter les options
for (const line of optionLines) {
const optionMatch = line.match(/^[ \t]*:([^:]+):\s*(.*)$/);
if (optionMatch) {
const [, key, value] = optionMatch;
const processedValue = value.replace(
/`([^<]+) <([^>]+)>`_/g,
(match, text, url) => {
return createLinkPlaceholder(text, url);
},
);
options += `<div class="directive-option" data-option="${key}" data-value="${value}"><span class="option-key">${key}:</span> <span class="option-value">${processedValue}</span></div>`;
}
}
// Traiter le contenu - trouver l'indentation minimale du contenu uniquement
if (contentLines.length > 0) {
// Trouver l'indentation minimale parmi les lignes de contenu non vides
let minContentIndent = null;
for (const line of contentLines) {
if (line.trim()) {
const indentMatch = line.match(/^([ \t]*)/);
if (indentMatch) {
const indent = indentMatch[1];
if (
minContentIndent === null ||
indent.length < minContentIndent.length
) {
minContentIndent = indent;
}
}
}
}
// Construire le contenu en préservant l'indentation relative
for (const line of contentLines) {
if (!line.trim()) {
content += "\n";
} else {
let contentLine = line;
if (minContentIndent && line.startsWith(minContentIndent)) {
contentLine = line.substring(minContentIndent.length);
} else {
// Fallback : enlever toute indentation de début
contentLine = line.replace(/^[ \t]+/, "");
}
content += contentLine + "\n";
}
}
}
result += options;
if (content.trim()) {
const processedContent = content
.trim()
.replace(/`([^<]+) <([^>]+)>`_/g, (match, text, url) => {
return createLinkPlaceholder(text, url);
});
result += `<div class="directive-content">${processedContent}</div>`;
}
}
result += "</div>";
return result;
},
);
// Bold
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
// Italic
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
// Code inline
html = html.replace(/``(.*?)``/g, "<code>$1</code>");
// Code blocks (:: at end of line followed by indented block)
html = html.replace(
/::\s*\n\n((?: .*\n?)+)/g,
"<pre><code>$1</code></pre>",
);
// Links restants dans le texte principal
html = html.replace(/`([^<]+) <([^>]+)>`_/g, (match, text, url) => {
return createLinkPlaceholder(text, url);
});
// Lists
html = html.replace(/^\* (.*$)/gim, "<li>$1</li>");
html = html.replace(/^- (.*$)/gim, "<li>$1</li>");
html = html.replace(/^\d+\. (.*$)/gim, "<li>$1</li>");
// Wrap consecutive <li> items in <ul>
html = html.replace(/(<li>.*<\/li>)/gs, (match) => {
return "<ul>" + match + "</ul>";
});
// Line breaks
html = html.replace(/\n\n/g, "</p><p>");
html = "<p>" + html + "</p>";
// Clean up empty paragraphs and fix structure
html = html.replace(/<p><\/p>/g, "");
html = html.replace(/<p>(<h[1-6]>)/g, "$1");
html = html.replace(/(<\/h[1-6]>)<\/p>/g, "$1");
html = html.replace(/<p>(<div class="rst-)/g, '<div class="rst-');
html = html.replace(/(<\/div>)<\/p>/g, "$1");
html = html.replace(/<p>(<ul>)/g, "$1");
html = html.replace(/(<\/ul>)<\/p>/g, "$1");
html = html.replace(/<p>(<pre>)/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 =
'<div class="preview-error">Erreur: Impossible de charger le fichier Markdown</div>';
});
}
function showRstPreview(fileUrl, container) {
fetch(fileUrl)
.then((response) => {
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
return response.text();
})
.then((text) => {
const htmlContent = parseRst(text);
const div = document.createElement("div");
div.className = "rst-content";
div.innerHTML = htmlContent;
container.innerHTML = "";
container.appendChild(div);
})
.catch((error) => {
console.error("Erreur chargement RST:", error);
container.innerHTML =
'<div class="preview-error">Erreur: Impossible de charger le fichier reStructuredText</div>';
});
}